perf: keep heavy work off UI thread

This commit is contained in:
Hakan Seven 2026-07-25 18:49:18 +03:00
commit deec48ad99
75 changed files with 11703 additions and 519 deletions

1
.gitignore vendored
View file

@ -5,6 +5,7 @@ target
# Trunk web build output (wasm/js bundle)
dist
web/worker_pkg
# These are backup files generated by rustfmt
**/*.rs.bk

16
Cargo.lock generated
View file

@ -17,6 +17,7 @@ name = "OpenCADStudio"
version = "0.8.7"
dependencies = [
"acadrust",
"bincode",
"bytemuck",
"clap",
"console_error_panic_hook",
@ -74,7 +75,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
[[package]]
name = "acadrust"
version = "0.4.0"
source = "git+https://github.com/OpenAEC-Foundation/acadifc?rev=cd90c256a5e7d115f6e995275d6b3117d5775da9#cd90c256a5e7d115f6e995275d6b3117d5775da9"
source = "git+https://github.com/OpenAEC-Foundation/acadifc.git?rev=bee1a58#bee1a5857d444a32b67f98e6babc1ad6483f6865"
dependencies = [
"ahash 0.8.12",
"anyhow",
@ -2305,8 +2306,6 @@ dependencies = [
[[package]]
name = "iced_wgpu"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff144a999b0ca0f8a10257934500060240825c42e950ec0ebee9c8ae30561c13"
dependencies = [
"bitflags 2.13.1",
"bytemuck",
@ -3667,6 +3666,17 @@ dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "ocs_web_worker"
version = "0.1.0"
dependencies = [
"acadrust",
"bincode",
"getrandom 0.3.4",
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "once_cell"
version = "1.21.4"

View file

@ -11,6 +11,7 @@ build = "build.rs"
# verifies it.
members = [
"crates/ocs_plugin_api",
"crates/ocs_web_worker",
"crates/dwg-thumbnailer",
"crates/dwg-thumbnailer-win",
]
@ -57,7 +58,7 @@ rfd = "0.17"
clap = { version = "4", features = ["derive"] }
# Opt-in logging via --log / RUST_LOG (surfaces wgpu / iced / winit diagnostics).
env_logger = "0.11"
acadrust = "0.4"
acadrust = { version = "0.4", features = ["serde"] }
# Shared DWG embedded-preview extraction (Start-page + file-manager thumbnails).
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
flate2 = "1"
@ -90,7 +91,11 @@ windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_Window
[patch.crates-io]
# Track the verified DWG round-trip, I/O, and unified PERF fixes.
acadrust = { git = "https://github.com/OpenAEC-Foundation/acadifc", rev = "cd90c256a5e7d115f6e995275d6b3117d5775da9" }
acadrust = { git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "bee1a58" }
# iced_wgpu 0.14 requests WebGL2 limits on every wasm adapter, including
# BrowserWebGpu. Keep the release source local with the one adapter-aware limit
# fix so WebGPU can expose storage buffers while WebGL2 remains the fallback.
iced_wgpu = { path = "crates/iced_wgpu" }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
# Native enables the plugin host runtime (out-of-process plugins).
@ -119,6 +124,7 @@ wasm-bindgen = "0.2"
# web font loader (#141).
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
bincode = "1.3"
# window.open (external URLs) + Blob/anchor file downloads (Save) + fetch()
# (Response) for the lazy per-script web font loader (#141).
web-sys = { version = "0.3", features = [
@ -130,6 +136,11 @@ web-sys = { version = "0.3", features = [
"Blob",
"Url",
"Response",
"Worker",
"WorkerOptions",
"WorkerType",
"MessageEvent",
"ErrorEvent",
# Async clipboard read for paste into the text/MText editors (iced's own
# clipboard read is a no-op on the web).
"Clipboard",

View file

@ -12,6 +12,11 @@ target = "index.html"
# on the rust <link> in index.html (truck-meshalgo → lzma-sys C deps cannot
# cross-compile to wasm).
[[hooks]]
stage = "pre_build"
command = "sh"
command_arguments = ["scripts/build-web-worker.sh"]
[serve]
# Cross-origin isolation headers enable SharedArrayBuffer, which wasm threads
# (rayon) need. GitHub Pages cannot set these; a self-hosted server (e.g. the

146
crates/iced_wgpu/Cargo.toml Normal file
View file

@ -0,0 +1,146 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2024"
name = "iced_wgpu"
version = "0.14.0"
authors = ["Héctor Ramón Jiménez <hector@hecrj.dev>"]
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "A renderer for iced on top of wgpu"
homepage = "https://iced.rs"
readme = "README.md"
keywords = [
"gui",
"ui",
"graphics",
"interface",
"widgets",
]
categories = ["gui"]
license = "MIT"
repository = "https://github.com/iced-rs/iced"
[package.metadata.docs.rs]
rustdoc-args = [
"--cfg",
"docsrs",
]
all-features = true
[features]
default = ["wgpu/default"]
geometry = [
"iced_graphics/geometry",
"lyon",
]
image = ["iced_graphics/image"]
strict-assertions = []
svg = [
"iced_graphics/svg",
"resvg/text",
]
web-colors = ["iced_graphics/web-colors"]
webgl = ["wgpu/webgl"]
[lib]
name = "iced_wgpu"
path = "src/lib.rs"
[dependencies.bitflags]
version = "2.0"
[dependencies.bytemuck]
version = "1.0"
features = ["derive"]
[dependencies.cryoglyph]
version = "0.1"
[dependencies.futures]
version = "0.3"
features = [
"std",
"async-await",
]
default-features = false
[dependencies.glam]
version = "0.25"
[dependencies.guillotiere]
version = "0.6"
[dependencies.iced_debug]
version = "0.14.0"
[dependencies.iced_graphics]
version = "0.14.0"
[dependencies.log]
version = "0.4"
[dependencies.lyon]
version = "1.0"
optional = true
[dependencies.resvg]
version = "0.45"
optional = true
[dependencies.rustc-hash]
version = "2.0"
[dependencies.thiserror]
version = "2"
[dependencies.wgpu]
version = "27.0"
features = [
"std",
"wgsl",
]
default-features = false
[lints.clippy]
default_trait_access = "deny"
filter_map_next = "deny"
from_over_into = "deny"
large-enum-variant = "allow"
manual_let_else = "deny"
map-entry = "allow"
match-wildcard-for-single-variants = "deny"
needless_borrow = "deny"
new_without_default = "deny"
redundant-closure-for-method-calls = "deny"
result_large_err = "allow"
semicolon_if_nothing_returned = "deny"
trivially-copy-pass-by-ref = "deny"
type-complexity = "allow"
unused_async = "deny"
useless_conversion = "deny"
[lints.rust]
missing_docs = "deny"
unsafe_code = "deny"
unused_results = "deny"
[lints.rust.rust_2018_idioms]
level = "deny"
priority = -1
[lints.rustdoc]
broken_intra_doc_links = "forbid"

19
crates/iced_wgpu/LICENSE Normal file
View file

@ -0,0 +1,19 @@
Copyright 2019 Héctor Ramón, Iced contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,8 @@
# OpenCADStudio patch
This directory vendors `iced_wgpu` 0.14.0 under its MIT license.
OpenCADStudio changes only the WASM device-limit selection in
`src/window/compositor.rs`: Browser WebGPU first requests normal WebGPU limits
so storage-buffer pipelines are available, then falls back to WebGL2 limits.
An actual WebGL adapter continues to request WebGL2 limits directly.

View file

@ -0,0 +1,20 @@
# `iced_wgpu`
[![Documentation](https://docs.rs/iced_wgpu/badge.svg)][documentation]
[![Crates.io](https://img.shields.io/crates/v/iced_wgpu.svg)](https://crates.io/crates/iced_wgpu)
[![License](https://img.shields.io/crates/l/iced_wgpu.svg)](https://github.com/iced-rs/iced/blob/master/LICENSE)
[![Discord Server](https://img.shields.io/discord/628993209984614400?label=&labelColor=6A7EC2&logo=discord&logoColor=ffffff&color=7389D8)](https://discord.gg/3xZJ65GAhd)
`iced_wgpu` is a [`wgpu`] renderer for [`iced_runtime`]. For now, it is the default renderer of Iced on [native platforms].
[`wgpu`] supports most modern graphics backends: Vulkan, Metal, DX12, OpenGL, and WebGPU.
<p align="center">
<img alt="The native target" src="../docs/graphs/native.png" width="80%">
</p>
[documentation]: https://docs.rs/iced_wgpu
[`iced_runtime`]: ../runtime
[`wgpu`]: https://github.com/gfx-rs/wgpu
[native platforms]: https://github.com/gfx-rs/wgpu#supported-platforms
[WebGPU API]: https://gpuweb.github.io/gpuweb/
[`wgpu_glyph`]: https://github.com/hecrj/wgpu_glyph

View file

@ -0,0 +1,132 @@
use std::marker::PhantomData;
use std::num::NonZeroU64;
use std::ops::RangeBounds;
pub const MAX_WRITE_SIZE: usize = 100 * 1024;
const MAX_WRITE_SIZE_U64: NonZeroU64 = NonZeroU64::new(MAX_WRITE_SIZE as u64)
.expect("MAX_WRITE_SIZE must be non-zero");
#[derive(Debug)]
pub struct Buffer<T> {
label: &'static str,
size: u64,
usage: wgpu::BufferUsages,
pub(crate) raw: wgpu::Buffer,
type_: PhantomData<T>,
}
impl<T: bytemuck::Pod> Buffer<T> {
pub fn new(
device: &wgpu::Device,
label: &'static str,
amount: usize,
usage: wgpu::BufferUsages,
) -> Self {
let size = next_copy_size::<T>(amount);
let raw = device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size,
usage,
mapped_at_creation: false,
});
Self {
label,
size,
usage,
raw,
type_: PhantomData,
}
}
pub fn resize(&mut self, device: &wgpu::Device, new_count: usize) -> bool {
let new_size = next_copy_size::<T>(new_count);
if self.size < new_size {
self.raw = device.create_buffer(&wgpu::BufferDescriptor {
label: Some(self.label),
size: new_size,
usage: self.usage,
mapped_at_creation: false,
});
self.size = new_size;
true
} else {
false
}
}
/// Returns the size of the written bytes.
pub fn write(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
offset: usize,
contents: &[T],
) -> usize {
let bytes: &[u8] = bytemuck::cast_slice(contents);
let mut bytes_written = 0;
// Split write into multiple chunks if necessary
while bytes_written + MAX_WRITE_SIZE < bytes.len() {
belt.write_buffer(
encoder,
&self.raw,
(offset + bytes_written) as u64,
MAX_WRITE_SIZE_U64,
device,
)
.copy_from_slice(
&bytes[bytes_written..bytes_written + MAX_WRITE_SIZE],
);
bytes_written += MAX_WRITE_SIZE;
}
// There will always be some bytes left, since the previous
// loop guarantees `bytes_written < bytes.len()`
let bytes_left = ((bytes.len() - bytes_written) as u64)
.try_into()
.expect("non-empty write");
// Write them
belt.write_buffer(
encoder,
&self.raw,
(offset + bytes_written) as u64,
bytes_left,
device,
)
.copy_from_slice(&bytes[bytes_written..]);
bytes.len()
}
pub fn slice(
&self,
bounds: impl RangeBounds<wgpu::BufferAddress>,
) -> wgpu::BufferSlice<'_> {
self.raw.slice(bounds)
}
pub fn range(&self, start: usize, end: usize) -> wgpu::BufferSlice<'_> {
self.slice(
start as u64 * std::mem::size_of::<T>() as u64
..end as u64 * std::mem::size_of::<T>() as u64,
)
}
}
fn next_copy_size<T>(amount: usize) -> u64 {
let align_mask = wgpu::COPY_BUFFER_ALIGNMENT - 1;
(((std::mem::size_of::<T>() * amount).next_power_of_two() as u64
+ align_mask)
& !align_mask)
.max(wgpu::COPY_BUFFER_ALIGNMENT)
}

View file

@ -0,0 +1,205 @@
use std::borrow::Cow;
use wgpu::util::DeviceExt;
pub fn convert(
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
source: wgpu::Texture,
format: wgpu::TextureFormat,
) -> wgpu::Texture {
if source.format() == format {
return source;
}
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("iced_wgpu.offscreen.sampler"),
..wgpu::SamplerDescriptor::default()
});
#[derive(Debug, Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
#[repr(C)]
struct Ratio {
u: f32,
v: f32,
// Padding field for 16-byte alignment.
// See https://docs.rs/wgpu/latest/wgpu/struct.DownlevelFlags.html#associatedconstant.BUFFER_BINDINGS_NOT_16_BYTE_ALIGNED
_padding: [f32; 2],
}
let ratio = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("iced-wgpu::triangle::msaa ratio"),
contents: bytemuck::bytes_of(&Ratio {
u: 1.0,
v: 1.0,
_padding: [0.0; 2],
}),
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM,
});
let constant_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("iced_wgpu.offscreen.blit.sampler_layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(
wgpu::SamplerBindingType::NonFiltering,
),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let constant_bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu.offscreen.sampler.bind_group"),
layout: &constant_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Sampler(&sampler),
},
wgpu::BindGroupEntry {
binding: 1,
resource: ratio.as_entire_binding(),
},
],
});
let texture_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("iced_wgpu.offscreen.blit.texture_layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float {
filterable: false,
},
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
}],
});
let pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("iced_wgpu.offscreen.blit.pipeline_layout"),
bind_group_layouts: &[&constant_layout, &texture_layout],
push_constant_ranges: &[],
});
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("iced_wgpu.offscreen.blit.shader"),
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
"shader/blit.wgsl"
))),
});
let pipeline =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("iced_wgpu.offscreen.blit.pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(
),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
}),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: wgpu::PipelineCompilationOptions::default(
),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
front_face: wgpu::FrontFace::Cw,
..wgpu::PrimitiveState::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview: None,
cache: None,
});
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("iced_wgpu.offscreen.conversion.source_texture"),
size: source.size(),
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = &texture.create_view(&wgpu::TextureViewDescriptor::default());
let texture_bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu.offscreen.blit.texture_bind_group"),
layout: &texture_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(
&source
.create_view(&wgpu::TextureViewDescriptor::default()),
),
}],
});
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("iced_wgpu.offscreen.blit.render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
pass.set_pipeline(&pipeline);
pass.set_bind_group(0, &constant_bind_group, &[]);
pass.set_bind_group(1, &texture_bind_group, &[]);
pass.draw(0..6, 0..1);
texture
}

View file

@ -0,0 +1,78 @@
use crate::graphics::{Antialiasing, Shell};
use crate::primitive;
use crate::quad;
use crate::text;
use crate::triangle;
use std::sync::{Arc, RwLock};
#[derive(Clone)]
pub struct Engine {
pub(crate) device: wgpu::Device,
pub(crate) queue: wgpu::Queue,
pub(crate) format: wgpu::TextureFormat,
pub(crate) quad_pipeline: quad::Pipeline,
pub(crate) text_pipeline: text::Pipeline,
pub(crate) triangle_pipeline: triangle::Pipeline,
#[cfg(any(feature = "image", feature = "svg"))]
pub(crate) image_pipeline: crate::image::Pipeline,
pub(crate) primitive_storage: Arc<RwLock<primitive::Storage>>,
_shell: Shell,
}
impl Engine {
pub fn new(
_adapter: &wgpu::Adapter,
device: wgpu::Device,
queue: wgpu::Queue,
format: wgpu::TextureFormat,
antialiasing: Option<Antialiasing>, // TODO: Initialize AA pipelines lazily
shell: Shell,
) -> Self {
Self {
format,
quad_pipeline: quad::Pipeline::new(&device, format),
text_pipeline: text::Pipeline::new(&device, &queue, format),
triangle_pipeline: triangle::Pipeline::new(
&device,
format,
antialiasing,
),
#[cfg(any(feature = "image", feature = "svg"))]
image_pipeline: {
let backend = _adapter.get_info().backend;
crate::image::Pipeline::new(&device, format, backend)
},
primitive_storage: Arc::new(RwLock::new(
primitive::Storage::default(),
)),
device,
queue,
_shell: shell,
}
}
#[cfg(any(feature = "image", feature = "svg"))]
pub fn create_image_cache(&self) -> crate::image::Cache {
self.image_pipeline.create_cache(
&self.device,
&self.queue,
&self._shell,
)
}
pub fn trim(&mut self) {
self.text_pipeline.trim();
self.primitive_storage
.write()
.expect("primitive storage should be writable")
.trim();
}
}

View file

@ -0,0 +1,785 @@
//! Build and draw geometry.
use crate::core::text::LineHeight;
use crate::core::{
self, Pixels, Point, Radians, Rectangle, Size, Svg, Transformation, Vector,
};
use crate::graphics::cache::{self, Cached};
use crate::graphics::color;
use crate::graphics::geometry::fill::{self, Fill};
use crate::graphics::geometry::{
self, LineCap, LineDash, LineJoin, Path, Stroke, Style,
};
use crate::graphics::gradient::{self, Gradient};
use crate::graphics::mesh::{self, Mesh};
use crate::graphics::{Image, Text};
use crate::text;
use lyon::geom::euclid;
use lyon::tessellation;
use std::borrow::Cow;
use std::sync::Arc;
#[derive(Debug)]
pub enum Geometry {
Live {
meshes: Vec<Mesh>,
images: Vec<Image>,
text: Vec<Text>,
},
Cached(Cache),
}
#[derive(Debug, Clone, Default)]
pub struct Cache {
pub meshes: Option<mesh::Cache>,
pub images: Option<Arc<[Image]>>,
pub text: Option<text::Cache>,
}
impl Cached for Geometry {
type Cache = Cache;
fn load(cache: &Self::Cache) -> Self {
Geometry::Cached(cache.clone())
}
fn cache(
self,
group: cache::Group,
previous: Option<Self::Cache>,
) -> Self::Cache {
match self {
Self::Live {
meshes,
images,
text,
} => {
let images = if images.is_empty() {
None
} else {
Some(Arc::from(images))
};
let meshes = Arc::from(meshes);
if let Some(mut previous) = previous {
if let Some(cache) = &mut previous.meshes {
cache.update(meshes);
} else {
previous.meshes = if meshes.is_empty() {
None
} else {
Some(mesh::Cache::new(meshes))
};
}
if let Some(cache) = &mut previous.text {
cache.update(text);
} else {
previous.text = text::Cache::new(group, text);
}
previous.images = images;
previous
} else {
Cache {
meshes: if meshes.is_empty() {
None
} else {
Some(mesh::Cache::new(meshes))
},
images,
text: text::Cache::new(group, text),
}
}
}
Self::Cached(cache) => cache,
}
}
}
/// A frame for drawing some geometry.
pub struct Frame {
clip_bounds: Rectangle,
buffers: BufferStack,
meshes: Vec<Mesh>,
images: Vec<Image>,
text: Vec<Text>,
transforms: Transforms,
fill_tessellator: tessellation::FillTessellator,
stroke_tessellator: tessellation::StrokeTessellator,
}
impl Frame {
/// Creates a new [`Frame`] with the given clip bounds.
pub fn new(bounds: Rectangle) -> Frame {
Frame {
clip_bounds: bounds,
buffers: BufferStack::new(),
meshes: Vec::new(),
images: Vec::new(),
text: Vec::new(),
transforms: Transforms {
previous: Vec::new(),
current: Transform(lyon::math::Transform::identity()),
},
fill_tessellator: tessellation::FillTessellator::new(),
stroke_tessellator: tessellation::StrokeTessellator::new(),
}
}
}
impl geometry::frame::Backend for Frame {
type Geometry = Geometry;
#[inline]
fn width(&self) -> f32 {
self.clip_bounds.width
}
#[inline]
fn height(&self) -> f32 {
self.clip_bounds.height
}
#[inline]
fn size(&self) -> Size {
self.clip_bounds.size()
}
#[inline]
fn center(&self) -> Point {
Point::new(self.clip_bounds.width / 2.0, self.clip_bounds.height / 2.0)
}
fn fill(&mut self, path: &Path, fill: impl Into<Fill>) {
let Fill { style, rule } = fill.into();
let mut buffer = self
.buffers
.get_fill(&self.transforms.current.transform_style(style));
let options = tessellation::FillOptions::default()
.with_fill_rule(into_fill_rule(rule));
if self.transforms.current.is_identity() {
self.fill_tessellator.tessellate_path(
path.raw(),
&options,
buffer.as_mut(),
)
} else {
let path = path.transform(&self.transforms.current.0);
self.fill_tessellator.tessellate_path(
path.raw(),
&options,
buffer.as_mut(),
)
}
.expect("Tessellate path.");
}
fn fill_rectangle(
&mut self,
top_left: Point,
size: Size,
fill: impl Into<Fill>,
) {
let Fill { style, rule } = fill.into();
let mut buffer = self
.buffers
.get_fill(&self.transforms.current.transform_style(style));
let top_left = self
.transforms
.current
.0
.transform_point(lyon::math::Point::new(top_left.x, top_left.y));
let size =
self.transforms.current.0.transform_vector(
lyon::math::Vector::new(size.width, size.height),
);
let options = tessellation::FillOptions::default()
.with_fill_rule(into_fill_rule(rule));
self.fill_tessellator
.tessellate_rectangle(
&lyon::math::Box2D::new(top_left, top_left + size),
&options,
buffer.as_mut(),
)
.expect("Fill rectangle");
}
fn stroke<'a>(&mut self, path: &Path, stroke: impl Into<Stroke<'a>>) {
let stroke = stroke.into();
let mut buffer = self
.buffers
.get_stroke(&self.transforms.current.transform_style(stroke.style));
let mut options = tessellation::StrokeOptions::default();
options.line_width = stroke.width;
options.start_cap = into_line_cap(stroke.line_cap);
options.end_cap = into_line_cap(stroke.line_cap);
options.line_join = into_line_join(stroke.line_join);
let path = if stroke.line_dash.segments.is_empty() {
Cow::Borrowed(path)
} else {
Cow::Owned(dashed(path, stroke.line_dash))
};
if self.transforms.current.is_identity() {
self.stroke_tessellator.tessellate_path(
path.raw(),
&options,
buffer.as_mut(),
)
} else {
let path = path.transform(&self.transforms.current.0);
self.stroke_tessellator.tessellate_path(
path.raw(),
&options,
buffer.as_mut(),
)
}
.expect("Stroke path");
}
fn stroke_rectangle<'a>(
&mut self,
top_left: Point,
size: Size,
stroke: impl Into<Stroke<'a>>,
) {
let stroke = stroke.into();
let mut buffer = self
.buffers
.get_stroke(&self.transforms.current.transform_style(stroke.style));
let top_left = self
.transforms
.current
.0
.transform_point(lyon::math::Point::new(top_left.x, top_left.y));
let size =
self.transforms.current.0.transform_vector(
lyon::math::Vector::new(size.width, size.height),
);
let mut options = tessellation::StrokeOptions::default();
options.line_width = stroke.width;
options.start_cap = into_line_cap(stroke.line_cap);
options.end_cap = into_line_cap(stroke.line_cap);
options.line_join = into_line_join(stroke.line_join);
self.stroke_tessellator
.tessellate_rectangle(
&lyon::math::Box2D::new(top_left, top_left + size),
&options,
buffer.as_mut(),
)
.expect("Stroke rectangle");
}
fn stroke_text<'a>(
&mut self,
text: impl Into<geometry::Text>,
stroke: impl Into<Stroke<'a>>,
) {
let text = text.into();
let stroke = stroke.into();
text.draw_with(|glyph, _color| self.stroke(&glyph, stroke));
}
fn fill_text(&mut self, text: impl Into<geometry::Text>) {
let text = text.into();
let (scale_x, scale_y) = self.transforms.current.scale();
if self.transforms.current.is_scale_translation()
&& scale_x == scale_y
&& scale_x > 0.0
&& scale_y > 0.0
{
let (bounds, size, line_height) =
if self.transforms.current.is_identity() {
(
Rectangle::new(
text.position,
Size::new(text.max_width, f32::INFINITY),
),
text.size,
text.line_height,
)
} else {
let position =
self.transforms.current.transform_point(text.position);
let size = Pixels(text.size.0 * scale_y);
let line_height = match text.line_height {
LineHeight::Absolute(size) => {
LineHeight::Absolute(Pixels(size.0 * scale_y))
}
LineHeight::Relative(factor) => {
LineHeight::Relative(factor)
}
};
(
Rectangle::new(
position,
Size::new(text.max_width, f32::INFINITY),
),
size,
line_height,
)
};
self.text.push(Text::Cached {
content: text.content,
bounds,
color: text.color,
size,
line_height: line_height.to_absolute(size),
font: text.font,
align_x: text.align_x,
align_y: text.align_y,
shaping: text.shaping,
clip_bounds: self.clip_bounds,
});
} else {
text.draw_with(|path, color| self.fill(&path, color));
}
}
#[inline]
fn translate(&mut self, translation: Vector) {
self.transforms.current.0 =
self.transforms
.current
.0
.pre_translate(lyon::math::Vector::new(
translation.x,
translation.y,
));
}
#[inline]
fn rotate(&mut self, angle: impl Into<Radians>) {
self.transforms.current.0 = self
.transforms
.current
.0
.pre_rotate(lyon::math::Angle::radians(angle.into().0));
}
#[inline]
fn scale(&mut self, scale: impl Into<f32>) {
let scale = scale.into();
self.scale_nonuniform(Vector { x: scale, y: scale });
}
#[inline]
fn scale_nonuniform(&mut self, scale: impl Into<Vector>) {
let scale = scale.into();
self.transforms.current.0 =
self.transforms.current.0.pre_scale(scale.x, scale.y);
}
fn push_transform(&mut self) {
self.transforms.previous.push(self.transforms.current);
}
fn pop_transform(&mut self) {
self.transforms.current = self.transforms.previous.pop().unwrap();
}
fn draft(&mut self, clip_bounds: Rectangle) -> Frame {
Frame::new(clip_bounds)
}
fn paste(&mut self, frame: Frame) {
self.meshes.extend(frame.meshes);
self.meshes
.extend(frame.buffers.into_meshes(frame.clip_bounds));
self.images.extend(frame.images);
self.text.extend(frame.text);
}
fn into_geometry(mut self) -> Self::Geometry {
self.meshes
.extend(self.buffers.into_meshes(self.clip_bounds));
Geometry::Live {
meshes: self.meshes,
images: self.images,
text: self.text,
}
}
fn draw_image(&mut self, bounds: Rectangle, image: impl Into<core::Image>) {
let mut image = image.into();
let (bounds, external_rotation) =
self.transforms.current.transform_rectangle(bounds);
image.rotation += external_rotation;
image.border_radius =
image.border_radius * self.transforms.current.scale().0;
self.images.push(Image::Raster {
image,
bounds,
clip_bounds: self.clip_bounds,
});
}
fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into<Svg>) {
let mut svg = svg.into();
let (bounds, external_rotation) =
self.transforms.current.transform_rectangle(bounds);
svg.rotation += external_rotation;
self.images.push(Image::Vector {
svg,
bounds,
clip_bounds: self.clip_bounds,
});
}
}
enum Buffer {
Solid(tessellation::VertexBuffers<mesh::SolidVertex2D, u32>),
Gradient(tessellation::VertexBuffers<mesh::GradientVertex2D, u32>),
}
struct BufferStack {
stack: Vec<Buffer>,
}
impl BufferStack {
fn new() -> Self {
Self { stack: Vec::new() }
}
fn get_mut(&mut self, style: &Style) -> &mut Buffer {
match style {
Style::Solid(_) => match self.stack.last() {
Some(Buffer::Solid(_)) => {}
_ => {
self.stack.push(Buffer::Solid(
tessellation::VertexBuffers::new(),
));
}
},
Style::Gradient(_) => match self.stack.last() {
Some(Buffer::Gradient(_)) => {}
_ => {
self.stack.push(Buffer::Gradient(
tessellation::VertexBuffers::new(),
));
}
},
}
self.stack.last_mut().unwrap()
}
fn get_fill<'a>(
&'a mut self,
style: &Style,
) -> Box<dyn tessellation::FillGeometryBuilder + 'a> {
match (style, self.get_mut(style)) {
(Style::Solid(color), Buffer::Solid(buffer)) => {
Box::new(tessellation::BuffersBuilder::new(
buffer,
TriangleVertex2DBuilder(color::pack(*color)),
))
}
(Style::Gradient(gradient), Buffer::Gradient(buffer)) => {
Box::new(tessellation::BuffersBuilder::new(
buffer,
GradientVertex2DBuilder {
gradient: gradient.pack(),
},
))
}
_ => unreachable!(),
}
}
fn get_stroke<'a>(
&'a mut self,
style: &Style,
) -> Box<dyn tessellation::StrokeGeometryBuilder + 'a> {
match (style, self.get_mut(style)) {
(Style::Solid(color), Buffer::Solid(buffer)) => {
Box::new(tessellation::BuffersBuilder::new(
buffer,
TriangleVertex2DBuilder(color::pack(*color)),
))
}
(Style::Gradient(gradient), Buffer::Gradient(buffer)) => {
Box::new(tessellation::BuffersBuilder::new(
buffer,
GradientVertex2DBuilder {
gradient: gradient.pack(),
},
))
}
_ => unreachable!(),
}
}
fn into_meshes(self, clip_bounds: Rectangle) -> impl Iterator<Item = Mesh> {
self.stack
.into_iter()
.filter_map(move |buffer| match buffer {
Buffer::Solid(buffer) if !buffer.indices.is_empty() => {
Some(Mesh::Solid {
buffers: mesh::Indexed {
vertices: buffer.vertices,
indices: buffer.indices,
},
clip_bounds,
transformation: Transformation::IDENTITY,
})
}
Buffer::Gradient(buffer) if !buffer.indices.is_empty() => {
Some(Mesh::Gradient {
buffers: mesh::Indexed {
vertices: buffer.vertices,
indices: buffer.indices,
},
clip_bounds,
transformation: Transformation::IDENTITY,
})
}
_ => None,
})
}
}
#[derive(Debug)]
struct Transforms {
previous: Vec<Transform>,
current: Transform,
}
#[derive(Debug, Clone, Copy)]
struct Transform(lyon::math::Transform);
impl Transform {
fn is_identity(&self) -> bool {
self.0 == lyon::math::Transform::identity()
}
fn is_scale_translation(&self) -> bool {
self.0.m12.abs() < 2.0 * f32::EPSILON
&& self.0.m21.abs() < 2.0 * f32::EPSILON
}
fn scale(&self) -> (f32, f32) {
(self.0.m11, self.0.m22)
}
fn transform_point(&self, point: Point) -> Point {
let transformed = self
.0
.transform_point(euclid::Point2D::new(point.x, point.y));
Point {
x: transformed.x,
y: transformed.y,
}
}
fn transform_style(&self, style: Style) -> Style {
match style {
Style::Solid(color) => Style::Solid(color),
Style::Gradient(gradient) => {
Style::Gradient(self.transform_gradient(gradient))
}
}
}
fn transform_gradient(&self, mut gradient: Gradient) -> Gradient {
match &mut gradient {
Gradient::Linear(linear) => {
linear.start = self.transform_point(linear.start);
linear.end = self.transform_point(linear.end);
}
}
gradient
}
fn transform_rectangle(
&self,
rectangle: Rectangle,
) -> (Rectangle, Radians) {
let top_left = self.transform_point(rectangle.position());
let top_right = self.transform_point(
rectangle.position() + Vector::new(rectangle.width, 0.0),
);
let bottom_left = self.transform_point(
rectangle.position() + Vector::new(0.0, rectangle.height),
);
Rectangle::with_vertices(top_left, top_right, bottom_left)
}
}
struct GradientVertex2DBuilder {
gradient: gradient::Packed,
}
impl tessellation::FillVertexConstructor<mesh::GradientVertex2D>
for GradientVertex2DBuilder
{
fn new_vertex(
&mut self,
vertex: tessellation::FillVertex<'_>,
) -> mesh::GradientVertex2D {
let position = vertex.position();
mesh::GradientVertex2D {
position: [position.x, position.y],
gradient: self.gradient,
}
}
}
impl tessellation::StrokeVertexConstructor<mesh::GradientVertex2D>
for GradientVertex2DBuilder
{
fn new_vertex(
&mut self,
vertex: tessellation::StrokeVertex<'_, '_>,
) -> mesh::GradientVertex2D {
let position = vertex.position();
mesh::GradientVertex2D {
position: [position.x, position.y],
gradient: self.gradient,
}
}
}
struct TriangleVertex2DBuilder(color::Packed);
impl tessellation::FillVertexConstructor<mesh::SolidVertex2D>
for TriangleVertex2DBuilder
{
fn new_vertex(
&mut self,
vertex: tessellation::FillVertex<'_>,
) -> mesh::SolidVertex2D {
let position = vertex.position();
mesh::SolidVertex2D {
position: [position.x, position.y],
color: self.0,
}
}
}
impl tessellation::StrokeVertexConstructor<mesh::SolidVertex2D>
for TriangleVertex2DBuilder
{
fn new_vertex(
&mut self,
vertex: tessellation::StrokeVertex<'_, '_>,
) -> mesh::SolidVertex2D {
let position = vertex.position();
mesh::SolidVertex2D {
position: [position.x, position.y],
color: self.0,
}
}
}
fn into_line_join(line_join: LineJoin) -> lyon::tessellation::LineJoin {
match line_join {
LineJoin::Miter => lyon::tessellation::LineJoin::Miter,
LineJoin::Round => lyon::tessellation::LineJoin::Round,
LineJoin::Bevel => lyon::tessellation::LineJoin::Bevel,
}
}
fn into_line_cap(line_cap: LineCap) -> lyon::tessellation::LineCap {
match line_cap {
LineCap::Butt => lyon::tessellation::LineCap::Butt,
LineCap::Square => lyon::tessellation::LineCap::Square,
LineCap::Round => lyon::tessellation::LineCap::Round,
}
}
fn into_fill_rule(rule: fill::Rule) -> lyon::tessellation::FillRule {
match rule {
fill::Rule::NonZero => lyon::tessellation::FillRule::NonZero,
fill::Rule::EvenOdd => lyon::tessellation::FillRule::EvenOdd,
}
}
pub(super) fn dashed(path: &Path, line_dash: LineDash<'_>) -> Path {
use lyon::algorithms::walk::{
RepeatedPattern, WalkerEvent, walk_along_path,
};
use lyon::path::iterator::PathIterator;
Path::new(|builder| {
let segments_odd = (line_dash.segments.len() % 2 == 1)
.then(|| [line_dash.segments, line_dash.segments].concat());
let mut draw_line = false;
walk_along_path(
path.raw().iter().flattened(
lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE,
),
0.0,
lyon::tessellation::StrokeOptions::DEFAULT_TOLERANCE,
&mut RepeatedPattern {
callback: |event: WalkerEvent<'_>| {
let point = Point {
x: event.position.x,
y: event.position.y,
};
if draw_line {
builder.line_to(point);
} else {
builder.move_to(point);
}
draw_line = !draw_line;
true
},
index: line_dash.offset,
intervals: segments_odd
.as_deref()
.unwrap_or(line_dash.segments),
},
);
})
}

View file

@ -0,0 +1,542 @@
pub mod entry;
mod allocation;
mod allocator;
mod layer;
pub use allocation::Allocation;
pub use entry::Entry;
pub use layer::Layer;
use allocator::Allocator;
pub const DEFAULT_SIZE: u32 = 2048;
pub const MAX_SIZE: u32 = 2048;
use crate::core::Size;
use crate::graphics::color;
use std::sync::Arc;
#[derive(Debug)]
pub struct Atlas {
size: u32,
backend: wgpu::Backend,
texture: wgpu::Texture,
texture_view: wgpu::TextureView,
texture_bind_group: Arc<wgpu::BindGroup>,
texture_layout: wgpu::BindGroupLayout,
layers: Vec<Layer>,
}
impl Atlas {
pub fn new(
device: &wgpu::Device,
backend: wgpu::Backend,
texture_layout: wgpu::BindGroupLayout,
) -> Self {
Self::with_size(device, backend, texture_layout, DEFAULT_SIZE)
}
pub fn with_size(
device: &wgpu::Device,
backend: wgpu::Backend,
texture_layout: wgpu::BindGroupLayout,
size: u32,
) -> Self {
let size = size.min(MAX_SIZE);
let layers = match backend {
// On the GL backend we start with 2 layers, to help wgpu figure
// out that this texture is `GL_TEXTURE_2D_ARRAY` rather than `GL_TEXTURE_2D`
// https://github.com/gfx-rs/wgpu/blob/004e3efe84a320d9331371ed31fa50baa2414911/wgpu-hal/src/gles/mod.rs#L371
wgpu::Backend::Gl => vec![Layer::Empty, Layer::Empty],
_ => vec![Layer::Empty],
};
let extent = wgpu::Extent3d {
width: size,
height: size,
depth_or_array_layers: layers.len() as u32,
};
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("iced_wgpu::image texture atlas"),
size: extent,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: if color::GAMMA_CORRECTION {
wgpu::TextureFormat::Rgba8UnormSrgb
} else {
wgpu::TextureFormat::Rgba8Unorm
},
usage: wgpu::TextureUsages::COPY_DST
| wgpu::TextureUsages::COPY_SRC
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor {
dimension: Some(wgpu::TextureViewDimension::D2Array),
..Default::default()
});
let texture_bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu::image texture atlas bind group"),
layout: &texture_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&texture_view),
}],
});
Atlas {
size,
backend,
texture,
texture_view,
texture_bind_group: Arc::new(texture_bind_group),
texture_layout,
layers,
}
}
pub fn bind_group(&self) -> &Arc<wgpu::BindGroup> {
&self.texture_bind_group
}
pub fn upload(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
width: u32,
height: u32,
pixels: &[u8],
) -> Option<Entry> {
let entry = {
let current_size = self.layers.len();
let entry = self.allocate(width, height)?;
// We grow the internal texture after allocating if necessary
let new_layers = self.layers.len() - current_size;
self.grow(new_layers, device, encoder, self.backend);
entry
};
log::debug!("Allocated atlas entry: {entry:?}");
match &entry {
Entry::Contiguous(allocation) => {
self.upload_allocation(
pixels, width, 0, allocation, device, encoder, belt,
);
}
Entry::Fragmented { fragments, .. } => {
for fragment in fragments {
let (x, y) = fragment.position;
let offset = 4 * (y * width + x) as usize;
self.upload_allocation(
pixels,
width,
offset,
&fragment.allocation,
device,
encoder,
belt,
);
}
}
}
if log::log_enabled!(log::Level::Debug) {
log::debug!(
"Atlas layers: {} (busy: {}, allocations: {})",
self.layers.len(),
self.layers.iter().filter(|layer| !layer.is_empty()).count(),
self.layers.iter().map(Layer::allocations).sum::<usize>(),
);
}
Some(entry)
}
pub fn remove(&mut self, entry: &Entry) {
log::debug!("Removing atlas entry: {entry:?}");
match entry {
Entry::Contiguous(allocation) => {
self.deallocate(allocation);
}
Entry::Fragmented { fragments, .. } => {
for fragment in fragments {
self.deallocate(&fragment.allocation);
}
}
}
}
fn allocate(&mut self, width: u32, height: u32) -> Option<Entry> {
// Allocate one layer if texture fits perfectly
if width == self.size && height == self.size {
let mut empty_layers = self
.layers
.iter_mut()
.enumerate()
.filter(|(_, layer)| layer.is_empty());
if let Some((i, layer)) = empty_layers.next() {
*layer = Layer::Full;
return Some(Entry::Contiguous(Allocation::Full {
layer: i,
size: self.size,
}));
}
self.layers.push(Layer::Full);
return Some(Entry::Contiguous(Allocation::Full {
layer: self.layers.len() - 1,
size: self.size,
}));
}
// Split big textures across multiple layers
if width > self.size || height > self.size {
let mut fragments = Vec::new();
let mut y = 0;
while y < height {
let height = std::cmp::min(height - y, self.size);
let mut x = 0;
while x < width {
let width = std::cmp::min(width - x, self.size);
let allocation = self.allocate(width, height)?;
if let Entry::Contiguous(allocation) = allocation {
fragments.push(entry::Fragment {
position: (x, y),
allocation,
});
}
x += width;
}
y += height;
}
return Some(Entry::Fragmented {
size: Size::new(width, height),
fragments,
});
}
// Try allocating on an existing layer
for (i, layer) in self.layers.iter_mut().enumerate() {
match layer {
Layer::Empty => {
let mut allocator = Allocator::new(self.size);
if let Some(region) = allocator.allocate(width, height) {
*layer = Layer::Busy(allocator);
return Some(Entry::Contiguous(Allocation::Partial {
region,
layer: i,
atlas_size: self.size,
}));
}
}
Layer::Busy(allocator) => {
if let Some(region) = allocator.allocate(width, height) {
return Some(Entry::Contiguous(Allocation::Partial {
region,
layer: i,
atlas_size: self.size,
}));
}
}
Layer::Full => {}
}
}
// Create new layer with atlas allocator
let mut allocator = Allocator::new(self.size);
if let Some(region) = allocator.allocate(width, height) {
self.layers.push(Layer::Busy(allocator));
return Some(Entry::Contiguous(Allocation::Partial {
region,
layer: self.layers.len() - 1,
atlas_size: self.size,
}));
}
// We ran out of memory (?)
None
}
fn deallocate(&mut self, allocation: &Allocation) {
log::debug!("Deallocating atlas: {allocation:?}");
match allocation {
Allocation::Full { layer, .. } => {
self.layers[*layer] = Layer::Empty;
}
Allocation::Partial { layer, region, .. } => {
let layer = &mut self.layers[*layer];
if let Layer::Busy(allocator) = layer {
allocator.deallocate(region);
if allocator.is_empty() {
*layer = Layer::Empty;
}
}
}
}
}
fn upload_allocation(
&self,
pixels: &[u8],
image_width: u32,
offset: usize,
allocation: &Allocation,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
) {
let (x, y) = allocation.position();
let Size { width, height } = allocation.size();
let layer = allocation.layer();
let padding = allocation.padding();
// It is a webgpu requirement that:
// BufferCopyView.layout.bytes_per_row % wgpu::COPY_BYTES_PER_ROW_ALIGNMENT == 0
// So we calculate bytes_per_row by rounding width up to the next
// multiple of wgpu::COPY_BYTES_PER_ROW_ALIGNMENT.
let bytes_per_row = (4 * (width + padding.width * 2))
.next_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT)
as usize;
let total_bytes =
bytes_per_row * (height + padding.height * 2) as usize;
let buffer_slice = belt.allocate(
wgpu::BufferSize::new(total_bytes as u64).unwrap(),
wgpu::BufferSize::new(8 * 4).unwrap(),
device,
);
const PIXEL: usize = 4;
let mut fragment = buffer_slice.get_mapped_range_mut();
let w = width as usize;
let h = height as usize;
let pad_w = padding.width as usize;
let pad_h = padding.height as usize;
let stride = PIXEL * w;
// Copy image rows
for row in 0..h {
let src = offset + row * PIXEL * image_width as usize;
let dst = (row + pad_h) * bytes_per_row;
fragment[dst + PIXEL * pad_w..dst + PIXEL * pad_w + stride]
.copy_from_slice(&pixels[src..src + stride]);
// Add padding to the sides, if needed
for i in 0..pad_w {
fragment[dst + PIXEL * i..dst + PIXEL * (i + 1)]
.copy_from_slice(&pixels[src..src + PIXEL]);
fragment[dst + stride + PIXEL * (pad_w + i)
..dst + stride + PIXEL * (pad_w + i + 1)]
.copy_from_slice(
&pixels[src + stride - PIXEL..src + stride],
);
}
}
// Add padding on top and bottom
for row in 0..pad_h {
let dst_top = row * bytes_per_row;
let dst_bottom = (pad_h + h + row) * bytes_per_row;
let src_top = offset;
let src_bottom = offset + (h - 1) * PIXEL * image_width as usize;
// Top
fragment[dst_top + PIXEL * pad_w..dst_top + PIXEL * (pad_w + w)]
.copy_from_slice(&pixels[src_top..src_top + PIXEL * w]);
// Bottom
fragment
[dst_bottom + PIXEL * pad_w..dst_bottom + PIXEL * (pad_w + w)]
.copy_from_slice(&pixels[src_bottom..src_bottom + PIXEL * w]);
// Corners
for i in 0..pad_w {
// Top left
fragment[dst_top + PIXEL * i..dst_top + PIXEL * (i + 1)]
.copy_from_slice(&pixels[offset..offset + PIXEL]);
// Top right
fragment[dst_top + PIXEL * (w + pad_w + i)
..dst_top + PIXEL * (w + pad_w + i + 1)]
.copy_from_slice(
&pixels[offset + PIXEL * (w - 1)..offset + PIXEL * w],
);
// Bottom left
fragment[dst_bottom + PIXEL * i..dst_bottom + PIXEL * (i + 1)]
.copy_from_slice(&pixels[src_bottom..src_bottom + PIXEL]);
// Bottom right
fragment[dst_bottom + PIXEL * (w + pad_w + i)
..dst_bottom + PIXEL * (w + pad_w + i + 1)]
.copy_from_slice(
&pixels[src_bottom + PIXEL * (w - 1)
..src_bottom + PIXEL * w],
);
}
}
// Copy actual image
encoder.copy_buffer_to_texture(
wgpu::TexelCopyBufferInfo {
buffer: buffer_slice.buffer(),
layout: wgpu::TexelCopyBufferLayout {
offset: buffer_slice.offset(),
bytes_per_row: Some(bytes_per_row as u32),
rows_per_image: Some(height + padding.height * 2),
},
},
wgpu::TexelCopyTextureInfo {
texture: &self.texture,
mip_level: 0,
origin: wgpu::Origin3d {
x: x - padding.width,
y: y - padding.height,
z: layer as u32,
},
aspect: wgpu::TextureAspect::default(),
},
wgpu::Extent3d {
width: width + padding.width * 2,
height: height + padding.height * 2,
depth_or_array_layers: 1,
},
);
}
fn grow(
&mut self,
amount: usize,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
backend: wgpu::Backend,
) {
if amount == 0 {
return;
}
// On the GL backend if layers.len() is a multiple of 6 we need to help wgpu figure out that this texture
// is still a `GL_TEXTURE_2D_ARRAY` rather than `GL_TEXTURE_CUBE_MAP` or `GL_TEXTURE_CUBE_ARRAY`.
// This will over-allocate some unused memory on GL, but it's better than not being able to
// grow the atlas past multiples of 6!
// https://github.com/gfx-rs/wgpu/blob/004e3efe84a320d9331371ed31fa50baa2414911/wgpu-hal/src/gles/mod.rs#L371
let depth_or_array_layers = match backend {
wgpu::Backend::Gl if self.layers.len().is_multiple_of(6) => {
self.layers.len() as u32 + 1
}
_ => self.layers.len() as u32,
};
let new_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("iced_wgpu::image texture atlas"),
size: wgpu::Extent3d {
width: self.size,
height: self.size,
depth_or_array_layers,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: if color::GAMMA_CORRECTION {
wgpu::TextureFormat::Rgba8UnormSrgb
} else {
wgpu::TextureFormat::Rgba8Unorm
},
usage: wgpu::TextureUsages::COPY_DST
| wgpu::TextureUsages::COPY_SRC
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let amount_to_copy = self.layers.len() - amount;
for (i, layer) in
self.layers.iter_mut().take(amount_to_copy).enumerate()
{
if layer.is_empty() {
continue;
}
encoder.copy_texture_to_texture(
wgpu::TexelCopyTextureInfo {
texture: &self.texture,
mip_level: 0,
origin: wgpu::Origin3d {
x: 0,
y: 0,
z: i as u32,
},
aspect: wgpu::TextureAspect::default(),
},
wgpu::TexelCopyTextureInfo {
texture: &new_texture,
mip_level: 0,
origin: wgpu::Origin3d {
x: 0,
y: 0,
z: i as u32,
},
aspect: wgpu::TextureAspect::default(),
},
wgpu::Extent3d {
width: self.size,
height: self.size,
depth_or_array_layers: 1,
},
);
}
self.texture = new_texture;
self.texture_view =
self.texture.create_view(&wgpu::TextureViewDescriptor {
dimension: Some(wgpu::TextureViewDimension::D2Array),
..Default::default()
});
self.texture_bind_group =
Arc::new(device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu::image texture atlas bind group"),
layout: &self.texture_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(
&self.texture_view,
),
}],
}));
}
}

View file

@ -0,0 +1,52 @@
use crate::core::Size;
use crate::image::atlas::allocator;
#[derive(Debug)]
pub enum Allocation {
Partial {
layer: usize,
region: allocator::Region,
atlas_size: u32,
},
Full {
layer: usize,
size: u32,
},
}
impl Allocation {
pub fn position(&self) -> (u32, u32) {
match self {
Allocation::Partial { region, .. } => region.position(),
Allocation::Full { .. } => (0, 0),
}
}
pub fn size(&self) -> Size<u32> {
match self {
Allocation::Partial { region, .. } => region.size(),
Allocation::Full { size, .. } => Size::new(*size, *size),
}
}
pub fn padding(&self) -> Size<u32> {
match self {
Allocation::Partial { region, .. } => region.padding(),
Allocation::Full { .. } => Size::new(0, 0),
}
}
pub fn layer(&self) -> usize {
match self {
Allocation::Partial { layer, .. } => *layer,
Allocation::Full { layer, .. } => *layer,
}
}
pub fn atlas_size(&self) -> u32 {
match self {
Allocation::Partial { atlas_size, .. } => *atlas_size,
Allocation::Full { size, .. } => *size,
}
}
}

View file

@ -0,0 +1,108 @@
use crate::core;
use guillotiere::{AtlasAllocator, Size};
pub struct Allocator {
raw: AtlasAllocator,
allocations: usize,
}
impl Allocator {
const PADDING: u32 = 1;
pub fn new(size: u32) -> Allocator {
let raw = AtlasAllocator::new(Size::new(size as i32, size as i32));
Allocator {
raw,
allocations: 0,
}
}
pub fn allocate(&mut self, width: u32, height: u32) -> Option<Region> {
let size = self.raw.size();
let padded_width = width + Self::PADDING * 2;
let padded_height = height + Self::PADDING * 2;
let pad_width = padded_width as i32 <= size.width;
let pad_height = padded_height as i32 <= size.height;
let mut allocation = self.raw.allocate(Size::new(
if pad_width { padded_width } else { width } as i32,
if pad_height { padded_height } else { height } as i32,
))?;
if pad_width {
allocation.rectangle.min.x += Self::PADDING as i32;
allocation.rectangle.max.x -= Self::PADDING as i32;
}
if pad_height {
allocation.rectangle.min.y += Self::PADDING as i32;
allocation.rectangle.max.y -= Self::PADDING as i32;
}
self.allocations += 1;
Some(Region {
allocation,
padding: core::Size::new(
if pad_width { Self::PADDING } else { 0 },
if pad_height { Self::PADDING } else { 0 },
),
})
}
pub fn deallocate(&mut self, region: &Region) {
self.raw.deallocate(region.allocation.id);
self.allocations = self.allocations.saturating_sub(1);
}
pub fn is_empty(&self) -> bool {
self.allocations == 0
}
pub fn allocations(&self) -> usize {
self.allocations
}
}
pub struct Region {
allocation: guillotiere::Allocation,
padding: core::Size<u32>,
}
impl Region {
pub fn position(&self) -> (u32, u32) {
let rectangle = &self.allocation.rectangle;
(rectangle.min.x as u32, rectangle.min.y as u32)
}
pub fn size(&self) -> core::Size<u32> {
let size = self.allocation.rectangle.size();
core::Size::new(size.width as u32, size.height as u32)
}
pub fn padding(&self) -> crate::core::Size<u32> {
self.padding
}
}
impl std::fmt::Debug for Allocator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Allocator")
}
}
impl std::fmt::Debug for Region {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Region")
.field("id", &self.allocation.id)
.field("rectangle", &self.allocation.rectangle)
.finish()
}
}

View file

@ -0,0 +1,27 @@
use crate::core::Size;
use crate::image::atlas;
#[derive(Debug)]
pub enum Entry {
Contiguous(atlas::Allocation),
Fragmented {
size: Size<u32>,
fragments: Vec<Fragment>,
},
}
impl Entry {
#[cfg(feature = "image")]
pub fn size(&self) -> Size<u32> {
match self {
Entry::Contiguous(allocation) => allocation.size(),
Entry::Fragmented { size, .. } => *size,
}
}
}
#[derive(Debug)]
pub struct Fragment {
pub position: (u32, u32),
pub allocation: atlas::Allocation,
}

View file

@ -0,0 +1,22 @@
use crate::image::atlas::Allocator;
#[derive(Debug)]
pub enum Layer {
Empty,
Busy(Allocator),
Full,
}
impl Layer {
pub fn is_empty(&self) -> bool {
matches!(self, Layer::Empty)
}
pub fn allocations(&self) -> usize {
match self {
Layer::Empty => 0,
Layer::Busy(allocator) => allocator.allocations(),
Layer::Full => 1,
}
}
}

View file

@ -0,0 +1,636 @@
use crate::core::{self, Size};
use crate::graphics::Shell;
use crate::image::atlas::{self, Atlas};
#[cfg(all(feature = "image", not(target_arch = "wasm32")))]
use worker::Worker;
#[cfg(feature = "image")]
use std::collections::HashMap;
use std::sync::Arc;
pub struct Cache {
atlas: Atlas,
#[cfg(feature = "image")]
raster: Raster,
#[cfg(feature = "svg")]
vector: crate::image::vector::Cache,
#[cfg(all(feature = "image", not(target_arch = "wasm32")))]
worker: Worker,
}
impl Cache {
pub fn new(
device: &wgpu::Device,
_queue: &wgpu::Queue,
backend: wgpu::Backend,
layout: wgpu::BindGroupLayout,
_shell: &Shell,
) -> Self {
#[cfg(all(feature = "image", not(target_arch = "wasm32")))]
let worker =
Worker::new(device, _queue, backend, layout.clone(), _shell);
Self {
atlas: Atlas::new(device, backend, layout),
#[cfg(feature = "image")]
raster: Raster {
cache: crate::image::raster::Cache::default(),
pending: HashMap::new(),
belt: wgpu::util::StagingBelt::new(2 * 1024 * 1024),
},
#[cfg(feature = "svg")]
vector: crate::image::vector::Cache::default(),
#[cfg(all(feature = "image", not(target_arch = "wasm32")))]
worker,
}
}
#[cfg(feature = "image")]
pub fn allocate_image(
&mut self,
handle: &core::image::Handle,
callback: impl FnOnce(Result<core::image::Allocation, core::image::Error>)
+ Send
+ 'static,
) {
use crate::image::raster::Memory;
let callback = Box::new(callback);
if let Some(callbacks) = self.raster.pending.get_mut(&handle.id()) {
callbacks.push(callback);
return;
}
if let Some(Memory::Device {
allocation, entry, ..
}) = self.raster.cache.get_mut(handle)
{
if let Some(allocation) = allocation
.as_ref()
.and_then(core::image::Allocation::upgrade)
{
callback(Ok(allocation));
return;
}
#[allow(unsafe_code)]
let new = unsafe { core::image::allocate(handle, entry.size()) };
*allocation = Some(new.downgrade());
callback(Ok(new));
return;
}
let _ = self.raster.pending.insert(handle.id(), vec![callback]);
#[cfg(not(target_arch = "wasm32"))]
self.worker.load(handle);
}
#[cfg(feature = "image")]
pub fn load_image(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
handle: &core::image::Handle,
) -> Result<core::image::Allocation, core::image::Error> {
use crate::image::raster::Memory;
if !self.raster.cache.contains(handle) {
self.raster.cache.insert(handle, Memory::load(handle));
}
match self.raster.cache.get_mut(handle).unwrap() {
Memory::Host(image) => {
let mut encoder = device.create_command_encoder(
&wgpu::CommandEncoderDescriptor {
label: Some("raster image upload"),
},
);
let entry = self.atlas.upload(
device,
&mut encoder,
&mut self.raster.belt,
image.width(),
image.height(),
image,
);
self.raster.belt.finish();
let submission = queue.submit([encoder.finish()]);
self.raster.belt.recall();
let Some(entry) = entry else {
return Err(core::image::Error::OutOfMemory);
};
let _ = device.poll(wgpu::PollType::Wait {
submission_index: Some(submission),
timeout: None,
});
#[allow(unsafe_code)]
let allocation = unsafe {
core::image::allocate(
handle,
Size::new(image.width(), image.height()),
)
};
self.raster.cache.insert(
handle,
Memory::Device {
entry,
bind_group: None,
allocation: Some(allocation.downgrade()),
},
);
Ok(allocation)
}
Memory::Device {
entry, allocation, ..
} => {
if let Some(allocation) = allocation
.as_ref()
.and_then(core::image::Allocation::upgrade)
{
return Ok(allocation);
}
#[allow(unsafe_code)]
let new =
unsafe { core::image::allocate(handle, entry.size()) };
*allocation = Some(new.downgrade());
Ok(new)
}
Memory::Error(error) => Err(error.clone()),
}
}
#[cfg(feature = "image")]
pub fn measure_image(
&mut self,
handle: &core::image::Handle,
) -> Option<Size<u32>> {
self.receive();
let image = load_image(
&mut self.raster.cache,
&mut self.raster.pending,
#[cfg(not(target_arch = "wasm32"))]
&self.worker,
handle,
None,
)?;
Some(image.dimensions())
}
#[cfg(feature = "svg")]
pub fn measure_svg(&mut self, handle: &core::svg::Handle) -> Size<u32> {
// TODO: Concurrency
self.vector.load(handle).viewport_dimensions()
}
#[cfg(feature = "image")]
pub fn upload_raster(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
handle: &core::image::Handle,
) -> Option<(&atlas::Entry, &Arc<wgpu::BindGroup>)> {
use crate::image::raster::Memory;
self.receive();
let memory = load_image(
&mut self.raster.cache,
&mut self.raster.pending,
#[cfg(not(target_arch = "wasm32"))]
&self.worker,
handle,
None,
)?;
if let Memory::Device {
entry, bind_group, ..
} = memory
{
return Some((
entry,
bind_group.as_ref().unwrap_or(self.atlas.bind_group()),
));
}
let image = memory.host()?;
const MAX_SYNC_SIZE: usize = 2 * 1024 * 1024;
// TODO: Concurrent Wasm support
if image.len() < MAX_SYNC_SIZE || cfg!(target_arch = "wasm32") {
let entry = self.atlas.upload(
device,
encoder,
belt,
image.width(),
image.height(),
&image,
)?;
*memory = Memory::Device {
entry,
bind_group: None,
allocation: None,
};
if let Memory::Device { entry, .. } = memory {
return Some((entry, self.atlas.bind_group()));
}
}
if !self.raster.pending.contains_key(&handle.id()) {
let _ = self.raster.pending.insert(handle.id(), Vec::new());
#[cfg(not(target_arch = "wasm32"))]
self.worker.upload(handle, image);
}
None
}
#[cfg(feature = "svg")]
pub fn upload_vector(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
handle: &core::svg::Handle,
color: Option<core::Color>,
size: Size,
scale: f32,
) -> Option<(&atlas::Entry, &Arc<wgpu::BindGroup>)> {
// TODO: Concurrency
self.vector
.upload(
device,
encoder,
belt,
handle,
color,
size,
scale,
&mut self.atlas,
)
.map(|entry| (entry, self.atlas.bind_group()))
}
pub fn trim(&mut self) {
#[cfg(feature = "image")]
{
self.receive();
self.raster.cache.trim(&mut self.atlas, |_bind_group| {
#[cfg(not(target_arch = "wasm32"))]
self.worker.drop(_bind_group);
});
}
#[cfg(feature = "svg")]
self.vector.trim(&mut self.atlas); // TODO: Concurrency
}
#[cfg(feature = "image")]
fn receive(&mut self) {
#[cfg(not(target_arch = "wasm32"))]
while let Ok(work) = self.worker.try_recv() {
use crate::image::raster::Memory;
match work {
worker::Work::Upload {
handle,
entry,
bind_group,
} => {
let callbacks = self.raster.pending.remove(&handle.id());
let allocation = if let Some(callbacks) = callbacks {
#[allow(unsafe_code)]
let allocation = unsafe {
core::image::allocate(&handle, entry.size())
};
let reference = allocation.downgrade();
for callback in callbacks {
callback(Ok(allocation.clone()));
}
Some(reference)
} else {
None
};
self.raster.cache.insert(
&handle,
Memory::Device {
entry,
bind_group: Some(bind_group),
allocation,
},
);
}
worker::Work::Error { handle, error } => {
let callbacks = self.raster.pending.remove(&handle.id());
if let Some(callbacks) = callbacks {
for callback in callbacks {
callback(Err(error.clone()));
}
}
self.raster.cache.insert(&handle, Memory::Error(error));
}
}
}
}
}
#[cfg(all(feature = "image", not(target_arch = "wasm32")))]
impl Drop for Cache {
fn drop(&mut self) {
self.worker.quit();
}
}
#[cfg(feature = "image")]
struct Raster {
cache: crate::image::raster::Cache,
pending: HashMap<core::image::Id, Vec<Callback>>,
belt: wgpu::util::StagingBelt,
}
#[cfg(feature = "image")]
type Callback =
Box<dyn FnOnce(Result<core::image::Allocation, core::image::Error>) + Send>;
#[cfg(feature = "image")]
fn load_image<'a>(
cache: &'a mut crate::image::raster::Cache,
pending: &mut HashMap<core::image::Id, Vec<Callback>>,
#[cfg(not(target_arch = "wasm32"))] worker: &Worker,
handle: &core::image::Handle,
callback: Option<Callback>,
) -> Option<&'a mut crate::image::raster::Memory> {
use crate::image::raster::Memory;
if !cache.contains(handle) {
if cfg!(target_arch = "wasm32") {
// TODO: Concurrent support for Wasm
cache.insert(handle, Memory::load(handle));
} else if let core::image::Handle::Rgba { .. } = handle {
// Load RGBA handles synchronously, since it's very cheap
cache.insert(handle, Memory::load(handle));
} else if !pending.contains_key(&handle.id()) {
let _ = pending.insert(handle.id(), Vec::from_iter(callback));
#[cfg(not(target_arch = "wasm32"))]
worker.load(handle);
}
}
cache.get_mut(handle)
}
#[cfg(all(feature = "image", not(target_arch = "wasm32")))]
mod worker {
use crate::core::Bytes;
use crate::core::image;
use crate::graphics::Shell;
use crate::image::atlas::{self, Atlas};
use crate::image::raster;
use std::sync::Arc;
use std::sync::mpsc;
use std::thread;
pub struct Worker {
jobs: mpsc::SyncSender<Job>,
quit: mpsc::SyncSender<()>,
work: mpsc::Receiver<Work>,
handle: Option<std::thread::JoinHandle<()>>,
}
impl Worker {
pub fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
backend: wgpu::Backend,
texture_layout: wgpu::BindGroupLayout,
shell: &Shell,
) -> Self {
let (jobs_sender, jobs_receiver) = mpsc::sync_channel(1_000);
let (quit_sender, quit_receiver) = mpsc::sync_channel(1);
let (work_sender, work_receiver) = mpsc::sync_channel(1_000);
let instance = Instance {
device: device.clone(),
queue: queue.clone(),
backend,
texture_layout,
shell: shell.clone(),
belt: wgpu::util::StagingBelt::new(4 * 1024 * 1024),
jobs: jobs_receiver,
output: work_sender,
quit: quit_receiver,
};
let handle = thread::spawn(move || instance.run());
Self {
jobs: jobs_sender,
quit: quit_sender,
work: work_receiver,
handle: Some(handle),
}
}
pub fn load(&self, handle: &image::Handle) {
let _ = self.jobs.send(Job::Load(handle.clone()));
}
pub fn upload(&self, handle: &image::Handle, image: raster::Image) {
let _ = self.jobs.send(Job::Upload {
handle: handle.clone(),
width: image.width(),
height: image.height(),
rgba: image.into_raw(),
});
}
pub fn drop(&self, bind_group: Arc<wgpu::BindGroup>) {
let _ = self.jobs.send(Job::Drop(bind_group));
}
pub fn try_recv(&self) -> Result<Work, mpsc::TryRecvError> {
self.work.try_recv()
}
pub fn quit(&mut self) {
let _ = self.quit.try_send(());
let _ = self.jobs.send(Job::Quit);
let _ = self.handle.take().map(thread::JoinHandle::join);
}
}
pub struct Instance {
device: wgpu::Device,
queue: wgpu::Queue,
backend: wgpu::Backend,
texture_layout: wgpu::BindGroupLayout,
shell: Shell,
belt: wgpu::util::StagingBelt,
jobs: mpsc::Receiver<Job>,
output: mpsc::SyncSender<Work>,
quit: mpsc::Receiver<()>,
}
#[derive(Debug)]
enum Job {
Load(image::Handle),
Upload {
handle: image::Handle,
rgba: Bytes,
width: u32,
height: u32,
},
Drop(Arc<wgpu::BindGroup>),
Quit,
}
pub enum Work {
Upload {
handle: image::Handle,
entry: atlas::Entry,
bind_group: Arc<wgpu::BindGroup>,
},
Error {
handle: image::Handle,
error: image::Error,
},
}
impl Instance {
fn run(mut self) {
loop {
if self.quit.try_recv().is_ok() {
return;
}
let Ok(job) = self.jobs.recv() else {
return;
};
match job {
Job::Load(handle) => {
match crate::graphics::image::load(&handle) {
Ok(image) => self.upload(
handle,
image.width(),
image.height(),
image.into_raw(),
Shell::invalidate_layout,
),
Err(error) => {
let _ = self
.output
.send(Work::Error { handle, error });
}
}
}
Job::Upload {
handle,
rgba,
width,
height,
} => {
self.upload(
handle,
width,
height,
rgba,
Shell::request_redraw,
);
}
Job::Drop(bind_group) => {
drop(bind_group);
}
Job::Quit => return,
}
}
}
fn upload(
&mut self,
handle: image::Handle,
width: u32,
height: u32,
rgba: Bytes,
callback: fn(&Shell),
) {
let mut encoder = self.device.create_command_encoder(
&wgpu::CommandEncoderDescriptor {
label: Some("raster image upload"),
},
);
let mut atlas = Atlas::with_size(
&self.device,
self.backend,
self.texture_layout.clone(),
width.max(height),
);
let Some(entry) = atlas.upload(
&self.device,
&mut encoder,
&mut self.belt,
width,
height,
&rgba,
) else {
return;
};
let output = self.output.clone();
let shell = self.shell.clone();
self.belt.finish();
let submission = self.queue.submit([encoder.finish()]);
self.belt.recall();
let bind_group = atlas.bind_group().clone();
self.queue.on_submitted_work_done(move || {
let _ = output.send(Work::Upload {
handle,
entry,
bind_group,
});
callback(&shell);
});
let _ = self.device.poll(wgpu::PollType::Wait {
submission_index: Some(submission),
timeout: None,
});
}
}
}

View file

@ -0,0 +1,758 @@
pub(crate) mod cache;
pub(crate) use cache::Cache;
mod atlas;
#[cfg(feature = "image")]
mod raster;
#[cfg(feature = "svg")]
mod vector;
use crate::Buffer;
use crate::core::border;
use crate::core::{Rectangle, Size, Transformation};
use crate::graphics::Shell;
use bytemuck::{Pod, Zeroable};
use std::mem;
use std::sync::Arc;
pub use crate::graphics::Image;
pub type Batch = Vec<Image>;
#[derive(Debug, Clone)]
pub struct Pipeline {
raw: wgpu::RenderPipeline,
backend: wgpu::Backend,
nearest_sampler: wgpu::Sampler,
linear_sampler: wgpu::Sampler,
texture_layout: wgpu::BindGroupLayout,
constant_layout: wgpu::BindGroupLayout,
}
impl Pipeline {
pub fn new(
device: &wgpu::Device,
format: wgpu::TextureFormat,
backend: wgpu::Backend,
) -> Self {
let nearest_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
min_filter: wgpu::FilterMode::Nearest,
mag_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
let linear_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
min_filter: wgpu::FilterMode::Linear,
mag_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let constant_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("iced_wgpu::image constants layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: wgpu::BufferSize::new(
mem::size_of::<Uniforms>() as u64,
),
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(
wgpu::SamplerBindingType::Filtering,
),
count: None,
},
],
});
let texture_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("iced_wgpu::image texture atlas layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float {
filterable: true,
},
view_dimension: wgpu::TextureViewDimension::D2Array,
multisampled: false,
},
count: None,
}],
});
let layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("iced_wgpu::image pipeline layout"),
push_constant_ranges: &[],
bind_group_layouts: &[&constant_layout, &texture_layout],
});
let shader =
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("iced_wgpu image shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
concat!(
include_str!("../shader/vertex.wgsl"),
"\n",
include_str!("../shader/image.wgsl"),
),
)),
});
let pipeline =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("iced_wgpu::image pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[wgpu::VertexBufferLayout {
array_stride: mem::size_of::<Instance>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &wgpu::vertex_attr_array!(
// Center
0 => Float32x2,
// Clip bounds
1 => Float32x4,
// Border radius
2 => Float32x4,
// Tile
3 => Float32x4,
// Rotation
4 => Float32,
// Opacity
5 => Float32,
// Atlas position
6 => Float32x2,
// Atlas scale
7 => Float32x2,
// Layer
8 => Sint32,
// Snap
9 => Uint32,
),
}],
compilation_options:
wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::SrcAlpha,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
alpha: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
operation: wgpu::BlendOperation::Add,
},
}),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options:
wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
front_face: wgpu::FrontFace::Cw,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None,
cache: None,
});
Pipeline {
raw: pipeline,
backend,
nearest_sampler,
linear_sampler,
texture_layout,
constant_layout,
}
}
pub fn create_cache(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
shell: &Shell,
) -> Cache {
Cache::new(
device,
queue,
self.backend,
self.texture_layout.clone(),
shell,
)
}
}
#[derive(Default)]
pub struct State {
layers: Vec<Layer>,
prepare_layer: usize,
nearest_instances: Vec<Instance>,
linear_instances: Vec<Instance>,
}
impl State {
pub fn new() -> Self {
Self::default()
}
pub fn prepare(
&mut self,
pipeline: &Pipeline,
device: &wgpu::Device,
belt: &mut wgpu::util::StagingBelt,
encoder: &mut wgpu::CommandEncoder,
cache: &mut Cache,
images: &Batch,
transformation: Transformation,
scale: f32,
) {
if self.layers.len() <= self.prepare_layer {
self.layers.push(Layer::new(
device,
&pipeline.constant_layout,
&pipeline.nearest_sampler,
&pipeline.linear_sampler,
));
}
let layer = &mut self.layers[self.prepare_layer];
let mut atlas: Option<Arc<wgpu::BindGroup>> = None;
for image in images {
match &image {
#[cfg(feature = "image")]
Image::Raster {
image,
bounds,
clip_bounds,
} => {
if let Some((atlas_entry, bind_group)) = cache
.upload_raster(device, encoder, belt, &image.handle)
{
match atlas.as_mut() {
None => {
atlas = Some(bind_group.clone());
}
Some(atlas) if atlas != bind_group => {
layer.push(
atlas,
&self.nearest_instances,
&self.linear_instances,
);
*atlas = Arc::clone(bind_group);
}
_ => {}
}
add_instances(
*bounds,
*clip_bounds,
image.border_radius,
f32::from(image.rotation),
image.opacity,
image.snap,
atlas_entry,
match image.filter_method {
crate::core::image::FilterMethod::Nearest => {
&mut self.nearest_instances
}
crate::core::image::FilterMethod::Linear => {
&mut self.linear_instances
}
},
);
}
}
#[cfg(not(feature = "image"))]
Image::Raster { .. } => continue,
#[cfg(feature = "svg")]
Image::Vector {
svg,
bounds,
clip_bounds,
} => {
if let Some((atlas_entry, bind_group)) = cache
.upload_vector(
device,
encoder,
belt,
&svg.handle,
svg.color,
bounds.size(),
scale,
)
{
match atlas.as_mut() {
None => {
atlas = Some(bind_group.clone());
}
Some(atlas) if atlas != bind_group => {
layer.push(
atlas,
&self.nearest_instances,
&self.linear_instances,
);
*atlas = bind_group.clone();
}
_ => {}
}
add_instances(
*bounds,
*clip_bounds,
border::radius(0),
f32::from(svg.rotation),
svg.opacity,
true,
atlas_entry,
&mut self.nearest_instances,
);
}
}
#[cfg(not(feature = "svg"))]
Image::Vector { .. } => continue,
}
}
if let Some(atlas) = &atlas {
layer.push(atlas, &self.nearest_instances, &self.linear_instances);
}
layer.prepare(
device,
encoder,
belt,
transformation,
scale,
&self.nearest_instances,
&self.linear_instances,
);
self.prepare_layer += 1;
self.nearest_instances.clear();
self.linear_instances.clear();
}
pub fn render<'a>(
&'a self,
pipeline: &'a Pipeline,
layer: usize,
bounds: Rectangle<u32>,
render_pass: &mut wgpu::RenderPass<'a>,
) {
if let Some(layer) = self.layers.get(layer) {
render_pass.set_pipeline(&pipeline.raw);
render_pass.set_scissor_rect(
bounds.x,
bounds.y,
bounds.width,
bounds.height,
);
layer.render(render_pass);
}
}
pub fn trim(&mut self) {
for layer in &mut self.layers[..self.prepare_layer] {
layer.clear();
}
self.prepare_layer = 0;
}
}
#[derive(Debug)]
struct Layer {
uniforms: wgpu::Buffer,
instances: Buffer<Instance>,
nearest: Vec<Group>,
nearest_layout: wgpu::BindGroup,
nearest_total: usize,
linear: Vec<Group>,
linear_layout: wgpu::BindGroup,
linear_total: usize,
}
#[derive(Debug)]
struct Group {
atlas: Arc<wgpu::BindGroup>,
instance_count: usize,
}
impl Layer {
fn new(
device: &wgpu::Device,
constant_layout: &wgpu::BindGroupLayout,
nearest_sampler: &wgpu::Sampler,
linear_sampler: &wgpu::Sampler,
) -> Self {
let uniforms = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("iced_wgpu::image uniforms buffer"),
size: mem::size_of::<Uniforms>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let instances = Buffer::new(
device,
"iced_wgpu::image instance buffer",
Instance::INITIAL,
wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
);
let nearest_layout =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu::image constants bind group"),
layout: constant_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(
wgpu::BufferBinding {
buffer: &uniforms,
offset: 0,
size: None,
},
),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(
nearest_sampler,
),
},
],
});
let linear_layout =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu::image constants bind group"),
layout: constant_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(
wgpu::BufferBinding {
buffer: &uniforms,
offset: 0,
size: None,
},
),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(
linear_sampler,
),
},
],
});
Self {
uniforms,
instances,
nearest: Vec::new(),
nearest_layout,
nearest_total: 0,
linear: Vec::new(),
linear_layout,
linear_total: 0,
}
}
fn prepare(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
transformation: Transformation,
scale_factor: f32,
nearest: &[Instance],
linear: &[Instance],
) {
let uniforms = Uniforms {
transform: transformation.into(),
scale_factor,
_padding: [0.0; 3],
};
let bytes = bytemuck::bytes_of(&uniforms);
belt.write_buffer(
encoder,
&self.uniforms,
0,
(bytes.len() as u64).try_into().expect("Sized uniforms"),
device,
)
.copy_from_slice(bytes);
let _ = self
.instances
.resize(device, self.nearest_total + self.linear_total);
let mut offset = 0;
if !nearest.is_empty() {
offset += self.instances.write(device, encoder, belt, 0, nearest);
}
if !linear.is_empty() {
let _ = self.instances.write(device, encoder, belt, offset, linear);
}
}
fn push(
&mut self,
atlas: &Arc<wgpu::BindGroup>,
nearest: &[Instance],
linear: &[Instance],
) {
let new_nearest = nearest.len() - self.nearest_total;
if new_nearest > 0 {
self.nearest.push(Group {
atlas: atlas.clone(),
instance_count: new_nearest,
});
self.nearest_total = nearest.len();
}
let new_linear = linear.len() - self.linear_total;
if new_linear > 0 {
self.linear.push(Group {
atlas: atlas.clone(),
instance_count: new_linear,
});
self.linear_total = linear.len();
}
}
fn render<'a>(&'a self, render_pass: &mut wgpu::RenderPass<'a>) {
render_pass.set_vertex_buffer(0, self.instances.slice(..));
let mut offset = 0;
if !self.nearest.is_empty() {
render_pass.set_bind_group(0, &self.nearest_layout, &[]);
for group in &self.nearest {
render_pass.set_bind_group(1, group.atlas.as_ref(), &[]);
render_pass
.draw(0..6, offset..offset + group.instance_count as u32);
offset += group.instance_count as u32;
}
}
if !self.linear.is_empty() {
render_pass.set_bind_group(0, &self.linear_layout, &[]);
for group in &self.linear {
render_pass.set_bind_group(1, group.atlas.as_ref(), &[]);
render_pass
.draw(0..6, offset..offset + group.instance_count as u32);
offset += group.instance_count as u32;
}
}
}
fn clear(&mut self) {
self.nearest.clear();
self.nearest_total = 0;
self.linear.clear();
self.linear_total = 0;
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Zeroable, Pod)]
struct Instance {
_center: [f32; 2],
_clip_bounds: [f32; 4],
_border_radius: [f32; 4],
_tile: [f32; 4],
_rotation: f32,
_opacity: f32,
_position_in_atlas: [f32; 2],
_size_in_atlas: [f32; 2],
_layer: u32,
_snap: u32,
}
impl Instance {
pub const INITIAL: usize = 20;
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Zeroable, Pod)]
struct Uniforms {
transform: [f32; 16],
scale_factor: f32,
// Uniforms must be aligned to their largest member,
// this uses a mat4x4<f32> which aligns to 16, so align to that
_padding: [f32; 3],
}
fn add_instances(
bounds: Rectangle,
clip_bounds: Rectangle,
border_radius: border::Radius,
rotation: f32,
opacity: f32,
snap: bool,
entry: &atlas::Entry,
instances: &mut Vec<Instance>,
) {
let center = [
bounds.x + bounds.width / 2.0,
bounds.y + bounds.height / 2.0,
];
let clip_bounds = [
clip_bounds.x,
clip_bounds.y,
clip_bounds.width,
clip_bounds.height,
];
let border_radius = border_radius.into();
match entry {
atlas::Entry::Contiguous(allocation) => {
add_instance(
center,
clip_bounds,
border_radius,
[bounds.x, bounds.y, bounds.width, bounds.height],
rotation,
opacity,
snap,
allocation,
instances,
);
}
atlas::Entry::Fragmented { fragments, size } => {
let scaling_x = bounds.width / size.width as f32;
let scaling_y = bounds.height / size.height as f32;
for fragment in fragments {
let allocation = &fragment.allocation;
let (fragment_x, fragment_y) = fragment.position;
let Size {
width: fragment_width,
height: fragment_height,
} = allocation.size();
let tile = [
bounds.x + fragment_x as f32 * scaling_x,
bounds.y + fragment_y as f32 * scaling_y,
fragment_width as f32 * scaling_x,
fragment_height as f32 * scaling_y,
];
add_instance(
center,
clip_bounds,
border_radius,
tile,
rotation,
opacity,
snap,
allocation,
instances,
);
}
}
}
}
#[inline]
fn add_instance(
center: [f32; 2],
clip_bounds: [f32; 4],
border_radius: [f32; 4],
tile: [f32; 4],
rotation: f32,
opacity: f32,
snap: bool,
allocation: &atlas::Allocation,
instances: &mut Vec<Instance>,
) {
let (x, y) = allocation.position();
let Size { width, height } = allocation.size();
let layer = allocation.layer();
let atlas_size = allocation.atlas_size();
let instance = Instance {
_center: center,
_clip_bounds: clip_bounds,
_border_radius: border_radius,
_tile: tile,
_rotation: rotation,
_opacity: opacity,
_position_in_atlas: [
x as f32 / atlas_size as f32,
y as f32 / atlas_size as f32,
],
_size_in_atlas: [
width as f32 / atlas_size as f32,
height as f32 / atlas_size as f32,
],
_layer: layer as u32,
_snap: snap as u32,
};
instances.push(instance);
}

View file

@ -0,0 +1,16 @@
pub use crate::graphics::Image;
#[derive(Debug, Default)]
pub struct Batch;
impl Batch {
pub fn push(&mut self, _image: Image) {}
pub fn clear(&mut self) {}
pub fn is_empty(&self) -> bool {
true
}
pub fn append(&mut self, _batch: &mut Self) {}
}

View file

@ -0,0 +1,123 @@
use crate::core::Size;
use crate::core::image;
use crate::graphics;
use crate::image::atlas::{self, Atlas};
use rustc_hash::{FxHashMap, FxHashSet};
use std::sync::{Arc, Weak};
pub type Image = graphics::image::Buffer;
/// Entry in cache corresponding to an image handle
#[derive(Debug)]
pub enum Memory {
/// Image data on host
Host(Image),
/// Storage entry
Device {
entry: atlas::Entry,
bind_group: Option<Arc<wgpu::BindGroup>>,
allocation: Option<Weak<image::Memory>>,
},
Error(image::Error),
}
impl Memory {
pub fn load(handle: &image::Handle) -> Self {
match graphics::image::load(handle) {
Ok(image) => Self::Host(image),
Err(error) => Self::Error(error),
}
}
pub fn dimensions(&self) -> Size<u32> {
match self {
Memory::Host(image) => {
let (width, height) = image.dimensions();
Size::new(width, height)
}
Memory::Device { entry, .. } => entry.size(),
Memory::Error(_) => Size::new(1, 1),
}
}
pub fn host(&self) -> Option<Image> {
match self {
Memory::Host(image) => Some(image.clone()),
Memory::Device { .. } | Memory::Error(_) => None,
}
}
}
#[derive(Debug, Default)]
pub struct Cache {
map: FxHashMap<image::Id, Memory>,
hits: FxHashSet<image::Id>,
should_trim: bool,
}
impl Cache {
pub fn get_mut(&mut self, handle: &image::Handle) -> Option<&mut Memory> {
let _ = self.hits.insert(handle.id());
self.map.get_mut(&handle.id())
}
pub fn insert(&mut self, handle: &image::Handle, memory: Memory) {
let _ = self.map.insert(handle.id(), memory);
let _ = self.hits.insert(handle.id());
self.should_trim = true;
}
pub fn contains(&self, handle: &image::Handle) -> bool {
self.map.contains_key(&handle.id())
}
pub fn trim(
&mut self,
atlas: &mut Atlas,
on_drop: impl Fn(Arc<wgpu::BindGroup>),
) {
// Only trim if new entries have landed in the `Cache`
if !self.should_trim {
return;
}
let hits = &self.hits;
self.map.retain(|id, memory| {
// Retain active allocations
if let Memory::Device { allocation, .. } = memory
&& allocation
.as_ref()
.is_some_and(|allocation| allocation.strong_count() > 0)
{
return true;
}
let retain = hits.contains(id);
if !retain {
log::debug!("Dropping image allocation: {id:?}");
if let Memory::Device {
entry, bind_group, ..
} = memory
{
if let Some(bind_group) = bind_group.take() {
on_drop(bind_group);
} else {
atlas.remove(entry);
}
}
}
retain
});
self.hits.clear();
self.should_trim = false;
}
}

View file

@ -0,0 +1,231 @@
use crate::core::svg;
use crate::core::{Color, Size};
use crate::image::atlas::{self, Atlas};
use resvg::tiny_skia;
use resvg::usvg;
use rustc_hash::{FxHashMap, FxHashSet};
use std::fs;
use std::panic;
use std::sync::Arc;
/// Entry in cache corresponding to an svg handle
pub enum Svg {
/// Parsed svg
Loaded(usvg::Tree),
/// Svg not found or failed to parse
NotFound,
}
impl Svg {
/// Viewport width and height
pub fn viewport_dimensions(&self) -> Size<u32> {
match self {
Svg::Loaded(tree) => {
let size = tree.size();
Size::new(size.width() as u32, size.height() as u32)
}
Svg::NotFound => Size::new(1, 1),
}
}
}
/// Caches svg vector and raster data
#[derive(Debug, Default)]
pub struct Cache {
svgs: FxHashMap<u64, Svg>,
rasterized: FxHashMap<(u64, u32, u32, ColorFilter), atlas::Entry>,
svg_hits: FxHashSet<u64>,
rasterized_hits: FxHashSet<(u64, u32, u32, ColorFilter)>,
should_trim: bool,
fontdb: Option<Arc<usvg::fontdb::Database>>,
}
type ColorFilter = Option<[u8; 4]>;
impl Cache {
/// Load svg
pub fn load(&mut self, handle: &svg::Handle) -> &Svg {
if self.svgs.contains_key(&handle.id()) {
return self.svgs.get(&handle.id()).unwrap();
}
// TODO: Reuse `cosmic-text` font database
if self.fontdb.is_none() {
let mut fontdb = usvg::fontdb::Database::new();
fontdb.load_system_fonts();
self.fontdb = Some(Arc::new(fontdb));
}
let options = usvg::Options {
fontdb: self
.fontdb
.as_ref()
.expect("fontdb must be initialized")
.clone(),
..usvg::Options::default()
};
let svg = match handle.data() {
svg::Data::Path(path) => fs::read_to_string(path)
.ok()
.and_then(|contents| {
usvg::Tree::from_str(&contents, &options).ok()
})
.map(Svg::Loaded)
.unwrap_or(Svg::NotFound),
svg::Data::Bytes(bytes) => {
match usvg::Tree::from_data(bytes, &options) {
Ok(tree) => Svg::Loaded(tree),
Err(_) => Svg::NotFound,
}
}
};
self.should_trim = true;
let _ = self.svgs.insert(handle.id(), svg);
self.svgs.get(&handle.id()).unwrap()
}
/// Load svg and upload raster data
pub fn upload(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
handle: &svg::Handle,
color: Option<Color>,
size: Size,
scale: f32,
atlas: &mut Atlas,
) -> Option<&atlas::Entry> {
let id = handle.id();
let (width, height) = (
(scale * size.width).ceil() as u32,
(scale * size.height).ceil() as u32,
);
let color = color.map(Color::into_rgba8);
let key = (id, width, height, color);
// TODO: Optimize!
// We currently rerasterize the SVG when its size changes. This is slow
// as heck. A GPU rasterizer like `pathfinder` may perform better.
// It would be cool to be able to smooth resize the `svg` example.
if self.rasterized.contains_key(&key) {
let _ = self.svg_hits.insert(id);
let _ = self.rasterized_hits.insert(key);
return self.rasterized.get(&key);
}
match self.load(handle) {
Svg::Loaded(tree) => {
if width == 0 || height == 0 {
return None;
}
// TODO: Optimize!
// We currently rerasterize the SVG when its size changes. This is slow
// as heck. A GPU rasterizer like `pathfinder` may perform better.
// It would be cool to be able to smooth resize the `svg` example.
let mut img = tiny_skia::Pixmap::new(width, height)?;
let tree_size = tree.size().to_int_size();
let target_size = if width > height {
tree_size.scale_to_width(width)
} else {
tree_size.scale_to_height(height)
};
let transform = if let Some(target_size) = target_size {
let tree_size = tree_size.to_size();
let target_size = target_size.to_size();
tiny_skia::Transform::from_scale(
target_size.width() / tree_size.width(),
target_size.height() / tree_size.height(),
)
} else {
tiny_skia::Transform::default()
};
// SVG rendering can panic on malformed or complex vectors.
// We catch panics to prevent crashes and continue gracefully.
let render =
panic::catch_unwind(panic::AssertUnwindSafe(|| {
resvg::render(tree, transform, &mut img.as_mut());
}));
if let Err(error) = render {
log::warn!(
"SVG rendering for {handle:?} panicked: {error:?}"
);
}
let mut rgba = img.take();
if let Some(color) = color {
rgba.chunks_exact_mut(4).for_each(|rgba| {
if rgba[3] > 0 {
rgba[0] = color[0];
rgba[1] = color[1];
rgba[2] = color[2];
}
});
}
let allocation = atlas
.upload(device, encoder, belt, width, height, &rgba)?;
log::debug!("allocating {id} {width}x{height}");
let _ = self.svg_hits.insert(id);
let _ = self.rasterized_hits.insert(key);
let _ = self.rasterized.insert(key, allocation);
self.should_trim = true;
self.rasterized.get(&key)
}
Svg::NotFound => None,
}
}
/// Load svg and upload raster data
pub fn trim(&mut self, atlas: &mut Atlas) {
if !self.should_trim {
return;
}
let svg_hits = &self.svg_hits;
let rasterized_hits = &self.rasterized_hits;
self.svgs.retain(|k, _| svg_hits.contains(k));
self.rasterized.retain(|k, entry| {
let retain = rasterized_hits.contains(k);
if !retain {
atlas.remove(entry);
}
retain
});
self.svg_hits.clear();
self.rasterized_hits.clear();
self.should_trim = false;
}
}
impl std::fmt::Debug for Svg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Svg::Loaded(_) => write!(f, "Svg::Loaded"),
Svg::NotFound => write!(f, "Svg::NotFound"),
}
}
}

View file

@ -0,0 +1,405 @@
use crate::core::{
self, Background, Color, Point, Rectangle, Svg, Transformation, renderer,
};
use crate::graphics;
use crate::graphics::Mesh;
use crate::graphics::color;
use crate::graphics::layer;
use crate::graphics::mesh;
use crate::graphics::text::{Editor, Paragraph};
use crate::image::{self, Image};
use crate::primitive::{self, Primitive};
use crate::quad::{self, Quad};
use crate::text::{self, Text};
use crate::triangle;
pub type Stack = layer::Stack<Layer>;
#[derive(Debug)]
pub struct Layer {
pub bounds: Rectangle,
pub quads: quad::Batch,
pub triangles: triangle::Batch,
pub primitives: primitive::Batch,
pub images: image::Batch,
pub text: text::Batch,
pending_meshes: Vec<Mesh>,
pending_text: Vec<Text>,
}
impl Layer {
pub fn is_empty(&self) -> bool {
self.quads.is_empty()
&& self.triangles.is_empty()
&& self.primitives.is_empty()
&& self.images.is_empty()
&& self.text.is_empty()
&& self.pending_meshes.is_empty()
&& self.pending_text.is_empty()
}
pub fn draw_quad(
&mut self,
quad: renderer::Quad,
background: Background,
transformation: Transformation,
) {
let bounds = quad.bounds * transformation;
let quad = Quad {
position: [bounds.x, bounds.y],
size: [bounds.width, bounds.height],
border_color: color::pack(quad.border.color),
border_radius: (quad.border.radius * transformation.scale_factor())
.into(),
border_width: quad.border.width * transformation.scale_factor(),
shadow_color: color::pack(quad.shadow.color),
shadow_offset: (quad.shadow.offset * transformation.scale_factor())
.into(),
shadow_blur_radius: quad.shadow.blur_radius
* transformation.scale_factor(),
snap: quad.snap as u32,
};
self.quads.add(quad, &background);
}
pub fn draw_paragraph(
&mut self,
paragraph: &Paragraph,
position: Point,
color: Color,
clip_bounds: Rectangle,
transformation: Transformation,
) {
let paragraph = Text::Paragraph {
paragraph: paragraph.downgrade(),
position,
color,
clip_bounds,
transformation,
};
self.pending_text.push(paragraph);
}
pub fn draw_editor(
&mut self,
editor: &Editor,
position: Point,
color: Color,
clip_bounds: Rectangle,
transformation: Transformation,
) {
let editor = Text::Editor {
editor: editor.downgrade(),
position,
color,
clip_bounds,
transformation,
};
self.pending_text.push(editor);
}
pub fn draw_text(
&mut self,
text: crate::core::Text,
position: Point,
color: Color,
clip_bounds: Rectangle,
transformation: Transformation,
) {
let text = Text::Cached {
content: text.content,
bounds: Rectangle::new(position, text.bounds) * transformation,
color,
size: text.size * transformation.scale_factor(),
line_height: text.line_height.to_absolute(text.size)
* transformation.scale_factor(),
font: text.font,
align_x: text.align_x,
align_y: text.align_y,
shaping: text.shaping,
clip_bounds: clip_bounds * transformation,
};
self.pending_text.push(text);
}
pub fn draw_text_raw(
&mut self,
raw: graphics::text::Raw,
transformation: Transformation,
) {
let raw = Text::Raw {
raw,
transformation,
};
self.pending_text.push(raw);
}
pub fn draw_image(&mut self, image: Image, transformation: Transformation) {
match image {
Image::Raster {
image,
bounds,
clip_bounds,
} => {
self.draw_raster(image, bounds, clip_bounds, transformation);
}
Image::Vector {
svg,
bounds,
clip_bounds,
} => {
self.draw_svg(svg, bounds, clip_bounds, transformation);
}
}
}
pub fn draw_raster(
&mut self,
image: core::Image,
bounds: Rectangle,
clip_bounds: Rectangle,
transformation: Transformation,
) {
let image = Image::Raster {
image: core::Image {
border_radius: image.border_radius
* transformation.scale_factor(),
..image
},
bounds: bounds * transformation,
clip_bounds: clip_bounds * transformation,
};
self.images.push(image);
}
pub fn draw_svg(
&mut self,
svg: Svg,
bounds: Rectangle,
clip_bounds: Rectangle,
transformation: Transformation,
) {
let svg = Image::Vector {
svg,
bounds: bounds * transformation,
clip_bounds: clip_bounds * transformation,
};
self.images.push(svg);
}
pub fn draw_mesh(
&mut self,
mut mesh: Mesh,
transformation: Transformation,
) {
match &mut mesh {
Mesh::Solid {
transformation: local_transformation,
..
}
| Mesh::Gradient {
transformation: local_transformation,
..
} => {
*local_transformation = *local_transformation * transformation;
}
}
self.pending_meshes.push(mesh);
}
pub fn draw_mesh_group(
&mut self,
meshes: Vec<Mesh>,
transformation: Transformation,
) {
self.flush_meshes();
self.triangles.push(triangle::Item::Group {
meshes,
transformation,
});
}
pub fn draw_mesh_cache(
&mut self,
cache: mesh::Cache,
transformation: Transformation,
) {
self.flush_meshes();
self.triangles.push(triangle::Item::Cached {
cache,
transformation,
});
}
pub fn draw_text_group(
&mut self,
text: Vec<Text>,
transformation: Transformation,
) {
self.flush_text();
self.text.push(text::Item::Group {
text,
transformation,
});
}
pub fn draw_text_cache(
&mut self,
cache: text::Cache,
transformation: Transformation,
) {
self.flush_text();
self.text.push(text::Item::Cached {
cache,
transformation,
});
}
pub fn draw_primitive(
&mut self,
bounds: Rectangle,
primitive: impl Primitive,
transformation: Transformation,
) {
let bounds = bounds * transformation;
self.primitives
.push(primitive::Instance::new(bounds, primitive));
}
fn flush_meshes(&mut self) {
if !self.pending_meshes.is_empty() {
self.triangles.push(triangle::Item::Group {
transformation: Transformation::IDENTITY,
meshes: self.pending_meshes.drain(..).collect(),
});
}
}
fn flush_text(&mut self) {
if !self.pending_text.is_empty() {
self.text.push(text::Item::Group {
transformation: Transformation::IDENTITY,
text: self.pending_text.drain(..).collect(),
});
}
}
}
impl graphics::Layer for Layer {
fn with_bounds(bounds: Rectangle) -> Self {
Self {
bounds,
..Self::default()
}
}
fn bounds(&self) -> Rectangle {
self.bounds
}
fn flush(&mut self) {
self.flush_meshes();
self.flush_text();
}
fn resize(&mut self, bounds: Rectangle) {
self.bounds = bounds;
}
fn reset(&mut self) {
self.bounds = Rectangle::INFINITE;
self.quads.clear();
self.triangles.clear();
self.primitives.clear();
self.text.clear();
self.images.clear();
self.pending_meshes.clear();
self.pending_text.clear();
}
fn start(&self) -> usize {
if !self.quads.is_empty() {
return 1;
}
if !self.triangles.is_empty() {
return 2;
}
if !self.primitives.is_empty() {
return 3;
}
if !self.images.is_empty() {
return 4;
}
if !self.text.is_empty() {
return 5;
}
usize::MAX
}
fn end(&self) -> usize {
if !self.text.is_empty() {
return 5;
}
if !self.images.is_empty() {
return 4;
}
if !self.primitives.is_empty() {
return 3;
}
if !self.triangles.is_empty() {
return 2;
}
if !self.quads.is_empty() {
return 1;
}
0
}
fn merge(&mut self, layer: &mut Self) {
self.quads.append(&mut layer.quads);
self.triangles.append(&mut layer.triangles);
self.primitives.append(&mut layer.primitives);
self.images.append(&mut layer.images);
self.text.append(&mut layer.text);
}
}
impl Default for Layer {
fn default() -> Self {
Self {
bounds: Rectangle::INFINITE,
quads: quad::Batch::default(),
triangles: triangle::Batch::default(),
primitives: primitive::Batch::default(),
text: text::Batch::default(),
images: image::Batch::default(),
pending_meshes: Vec::new(),
pending_text: Vec::new(),
}
}
}

984
crates/iced_wgpu/src/lib.rs Normal file
View file

@ -0,0 +1,984 @@
//! A [`wgpu`] renderer for [Iced].
//!
//! ![The native path of the Iced ecosystem](https://github.com/iced-rs/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/native.png?raw=true)
//!
//! [`wgpu`] supports most modern graphics backends: Vulkan, Metal, DX11, and
//! DX12 (OpenGL and WebGL are still WIP). Additionally, it will support the
//! incoming [WebGPU API].
//!
//! Currently, `iced_wgpu` supports the following primitives:
//! - Text, which is rendered using [`glyphon`].
//! - Quads or rectangles, with rounded borders and a solid background color.
//! - Clip areas, useful to implement scrollables or hide overflowing content.
//! - Images and SVG, loaded from memory or the file system.
//! - Meshes of triangles, useful to draw geometry freely.
//!
//! [Iced]: https://github.com/iced-rs/iced
//! [`wgpu`]: https://github.com/gfx-rs/wgpu-rs
//! [WebGPU API]: https://gpuweb.github.io/gpuweb/
//! [`glyphon`]: https://github.com/grovesNL/glyphon
#![doc(
html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![allow(missing_docs)]
pub mod layer;
pub mod primitive;
pub mod settings;
pub mod window;
#[cfg(feature = "geometry")]
pub mod geometry;
mod buffer;
mod color;
mod engine;
mod quad;
mod text;
mod triangle;
#[cfg(any(feature = "image", feature = "svg"))]
#[path = "image/mod.rs"]
mod image;
#[cfg(not(any(feature = "image", feature = "svg")))]
#[path = "image/null.rs"]
mod image;
use buffer::Buffer;
use iced_debug as debug;
pub use iced_graphics as graphics;
pub use iced_graphics::core;
pub use wgpu;
pub use engine::Engine;
pub use layer::Layer;
pub use primitive::Primitive;
pub use settings::Settings;
#[cfg(feature = "geometry")]
pub use geometry::Geometry;
use crate::core::renderer;
use crate::core::{
Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation,
};
use crate::graphics::mesh;
use crate::graphics::text::{Editor, Paragraph};
use crate::graphics::{Shell, Viewport};
/// A [`wgpu`] graphics renderer for [`iced`].
///
/// [`wgpu`]: https://github.com/gfx-rs/wgpu-rs
/// [`iced`]: https://github.com/iced-rs/iced
pub struct Renderer {
engine: Engine,
default_font: Font,
default_text_size: Pixels,
layers: layer::Stack,
quad: quad::State,
triangle: triangle::State,
text: text::State,
text_viewport: text::Viewport,
#[cfg(any(feature = "svg", feature = "image"))]
image: image::State,
// TODO: Centralize all the image feature handling
#[cfg(any(feature = "svg", feature = "image"))]
image_cache: std::cell::RefCell<image::Cache>,
staging_belt: wgpu::util::StagingBelt,
}
impl Renderer {
pub fn new(
engine: Engine,
default_font: Font,
default_text_size: Pixels,
) -> Self {
Self {
default_font,
default_text_size,
layers: layer::Stack::new(),
quad: quad::State::new(),
triangle: triangle::State::new(
&engine.device,
&engine.triangle_pipeline,
),
text: text::State::new(),
text_viewport: engine.text_pipeline.create_viewport(&engine.device),
#[cfg(any(feature = "svg", feature = "image"))]
image: image::State::new(),
#[cfg(any(feature = "svg", feature = "image"))]
image_cache: std::cell::RefCell::new(engine.create_image_cache()),
// TODO: Resize belt smartly (?)
// It would be great if the `StagingBelt` API exposed methods
// for introspection to detect when a resize may be worth it.
staging_belt: wgpu::util::StagingBelt::new(
buffer::MAX_WRITE_SIZE as u64,
),
engine,
}
}
fn draw(
&mut self,
clear_color: Option<Color>,
target: &wgpu::TextureView,
viewport: &Viewport,
) -> wgpu::CommandEncoder {
let mut encoder = self.engine.device.create_command_encoder(
&wgpu::CommandEncoderDescriptor {
label: Some("iced_wgpu encoder"),
},
);
self.prepare(&mut encoder, viewport);
self.render(&mut encoder, target, clear_color, viewport);
self.quad.trim();
self.triangle.trim();
self.text.trim();
// TODO: Provide window id (?)
self.engine.trim();
#[cfg(any(feature = "svg", feature = "image"))]
{
self.image.trim();
self.image_cache.borrow_mut().trim();
}
encoder
}
pub fn present(
&mut self,
clear_color: Option<Color>,
_format: wgpu::TextureFormat,
frame: &wgpu::TextureView,
viewport: &Viewport,
) -> wgpu::SubmissionIndex {
let encoder = self.draw(clear_color, frame, viewport);
self.staging_belt.finish();
let submission = self.engine.queue.submit([encoder.finish()]);
self.staging_belt.recall();
submission
}
/// Renders the current surface to an offscreen buffer.
///
/// Returns RGBA bytes of the texture data.
pub fn screenshot(
&mut self,
viewport: &Viewport,
background_color: Color,
) -> Vec<u8> {
#[derive(Clone, Copy, Debug)]
struct BufferDimensions {
width: u32,
height: u32,
unpadded_bytes_per_row: usize,
padded_bytes_per_row: usize,
}
impl BufferDimensions {
fn new(size: Size<u32>) -> Self {
let unpadded_bytes_per_row = size.width as usize * 4; //slice of buffer per row; always RGBA
let alignment = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; //256
let padded_bytes_per_row_padding = (alignment
- unpadded_bytes_per_row % alignment)
% alignment;
let padded_bytes_per_row =
unpadded_bytes_per_row + padded_bytes_per_row_padding;
Self {
width: size.width,
height: size.height,
unpadded_bytes_per_row,
padded_bytes_per_row,
}
}
}
let dimensions = BufferDimensions::new(viewport.physical_size());
let texture_extent = wgpu::Extent3d {
width: dimensions.width,
height: dimensions.height,
depth_or_array_layers: 1,
};
let texture =
self.engine.device.create_texture(&wgpu::TextureDescriptor {
label: Some("iced_wgpu.offscreen.source_texture"),
size: texture_extent,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: self.engine.format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::COPY_SRC
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self.draw(Some(background_color), &view, viewport);
let texture = crate::color::convert(
&self.engine.device,
&mut encoder,
texture,
if graphics::color::GAMMA_CORRECTION {
wgpu::TextureFormat::Rgba8UnormSrgb
} else {
wgpu::TextureFormat::Rgba8Unorm
},
);
let output_buffer =
self.engine.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("iced_wgpu.offscreen.output_texture_buffer"),
size: (dimensions.padded_bytes_per_row
* dimensions.height as usize) as u64,
usage: wgpu::BufferUsages::MAP_READ
| wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
encoder.copy_texture_to_buffer(
texture.as_image_copy(),
wgpu::TexelCopyBufferInfo {
buffer: &output_buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(dimensions.padded_bytes_per_row as u32),
rows_per_image: None,
},
},
texture_extent,
);
self.staging_belt.finish();
let index = self.engine.queue.submit([encoder.finish()]);
self.staging_belt.recall();
let slice = output_buffer.slice(..);
slice.map_async(wgpu::MapMode::Read, |_| {});
let _ = self.engine.device.poll(wgpu::PollType::Wait {
submission_index: Some(index),
timeout: None,
});
let mapped_buffer = slice.get_mapped_range();
mapped_buffer.chunks(dimensions.padded_bytes_per_row).fold(
vec![],
|mut acc, row| {
acc.extend(&row[..dimensions.unpadded_bytes_per_row]);
acc
},
)
}
fn prepare(
&mut self,
encoder: &mut wgpu::CommandEncoder,
viewport: &Viewport,
) {
let scale_factor = viewport.scale_factor();
self.text_viewport
.update(&self.engine.queue, viewport.physical_size());
let physical_bounds = Rectangle::<f32>::from(Rectangle::with_size(
viewport.physical_size(),
));
self.layers.merge();
for layer in self.layers.iter() {
let clip_bounds = layer.bounds * scale_factor;
if physical_bounds
.intersection(&clip_bounds)
.and_then(Rectangle::snap)
.is_none()
{
continue;
}
if !layer.quads.is_empty() {
let prepare_span = debug::prepare(debug::Primitive::Quad);
self.quad.prepare(
&self.engine.quad_pipeline,
&self.engine.device,
&mut self.staging_belt,
encoder,
&layer.quads,
viewport.projection(),
scale_factor,
);
prepare_span.finish();
}
if !layer.triangles.is_empty() {
let prepare_span = debug::prepare(debug::Primitive::Triangle);
self.triangle.prepare(
&self.engine.triangle_pipeline,
&self.engine.device,
&mut self.staging_belt,
encoder,
&layer.triangles,
Transformation::scale(scale_factor),
viewport.physical_size(),
);
prepare_span.finish();
}
if !layer.primitives.is_empty() {
let prepare_span = debug::prepare(debug::Primitive::Shader);
let mut primitive_storage = self
.engine
.primitive_storage
.write()
.expect("Write primitive storage");
for instance in &layer.primitives {
instance.primitive.prepare(
&mut primitive_storage,
&self.engine.device,
&self.engine.queue,
self.engine.format,
&instance.bounds,
viewport,
);
}
prepare_span.finish();
}
#[cfg(any(feature = "svg", feature = "image"))]
if !layer.images.is_empty() {
let prepare_span = debug::prepare(debug::Primitive::Image);
self.image.prepare(
&self.engine.image_pipeline,
&self.engine.device,
&mut self.staging_belt,
encoder,
&mut self.image_cache.borrow_mut(),
&layer.images,
viewport.projection(),
scale_factor,
);
prepare_span.finish();
}
if !layer.text.is_empty() {
let prepare_span = debug::prepare(debug::Primitive::Text);
self.text.prepare(
&self.engine.text_pipeline,
&self.engine.device,
&self.engine.queue,
&self.text_viewport,
encoder,
&layer.text,
layer.bounds,
Transformation::scale(scale_factor),
);
prepare_span.finish();
}
}
}
fn render(
&mut self,
encoder: &mut wgpu::CommandEncoder,
frame: &wgpu::TextureView,
clear_color: Option<Color>,
viewport: &Viewport,
) {
use std::mem::ManuallyDrop;
let mut render_pass = ManuallyDrop::new(encoder.begin_render_pass(
&wgpu::RenderPassDescriptor {
label: Some("iced_wgpu render pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: frame,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: match clear_color {
Some(background_color) => wgpu::LoadOp::Clear({
let [r, g, b, a] =
graphics::color::pack(background_color)
.components();
wgpu::Color {
r: f64::from(r),
g: f64::from(g),
b: f64::from(b),
a: f64::from(a),
}
}),
None => wgpu::LoadOp::Load,
},
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
},
));
let mut quad_layer = 0;
let mut mesh_layer = 0;
let mut text_layer = 0;
#[cfg(any(feature = "svg", feature = "image"))]
let mut image_layer = 0;
let scale_factor = viewport.scale_factor();
let physical_bounds = Rectangle::<f32>::from(Rectangle::with_size(
viewport.physical_size(),
));
let scale = Transformation::scale(scale_factor);
for layer in self.layers.iter() {
let Some(physical_bounds) =
physical_bounds.intersection(&(layer.bounds * scale_factor))
else {
continue;
};
let Some(scissor_rect) = physical_bounds.snap() else {
continue;
};
if !layer.quads.is_empty() {
let render_span = debug::render(debug::Primitive::Quad);
self.quad.render(
&self.engine.quad_pipeline,
quad_layer,
scissor_rect,
&layer.quads,
&mut render_pass,
);
render_span.finish();
quad_layer += 1;
}
if !layer.triangles.is_empty() {
let _ = ManuallyDrop::into_inner(render_pass);
let render_span = debug::render(debug::Primitive::Triangle);
mesh_layer += self.triangle.render(
&self.engine.triangle_pipeline,
encoder,
frame,
mesh_layer,
&layer.triangles,
physical_bounds,
scale,
);
render_span.finish();
render_pass = ManuallyDrop::new(encoder.begin_render_pass(
&wgpu::RenderPassDescriptor {
label: Some("iced_wgpu render pass"),
color_attachments: &[Some(
wgpu::RenderPassColorAttachment {
view: frame,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
},
)],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
},
));
}
if !layer.primitives.is_empty() {
let render_span = debug::render(debug::Primitive::Shader);
let primitive_storage = self
.engine
.primitive_storage
.read()
.expect("Read primitive storage");
let mut need_render = Vec::new();
for instance in &layer.primitives {
let bounds = instance.bounds * scale;
if let Some(clip_bounds) = (instance.bounds * scale)
.intersection(&physical_bounds)
.and_then(Rectangle::snap)
{
render_pass.set_viewport(
bounds.x,
bounds.y,
bounds.width,
bounds.height,
0.0,
1.0,
);
render_pass.set_scissor_rect(
clip_bounds.x,
clip_bounds.y,
clip_bounds.width,
clip_bounds.height,
);
let drawn = instance
.primitive
.draw(&primitive_storage, &mut render_pass);
if !drawn {
need_render.push((instance, clip_bounds));
}
}
}
render_pass.set_viewport(
0.0,
0.0,
viewport.physical_width() as f32,
viewport.physical_height() as f32,
0.0,
1.0,
);
render_pass.set_scissor_rect(
0,
0,
viewport.physical_width(),
viewport.physical_height(),
);
if !need_render.is_empty() {
let _ = ManuallyDrop::into_inner(render_pass);
for (instance, clip_bounds) in need_render {
instance.primitive.render(
&primitive_storage,
encoder,
frame,
&clip_bounds,
);
}
render_pass = ManuallyDrop::new(encoder.begin_render_pass(
&wgpu::RenderPassDescriptor {
label: Some("iced_wgpu render pass"),
color_attachments: &[Some(
wgpu::RenderPassColorAttachment {
view: frame,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
},
)],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
},
));
}
render_span.finish();
}
#[cfg(any(feature = "svg", feature = "image"))]
if !layer.images.is_empty() {
let render_span = debug::render(debug::Primitive::Image);
self.image.render(
&self.engine.image_pipeline,
image_layer,
scissor_rect,
&mut render_pass,
);
render_span.finish();
image_layer += 1;
}
if !layer.text.is_empty() {
let render_span = debug::render(debug::Primitive::Text);
text_layer += self.text.render(
&self.engine.text_pipeline,
&self.text_viewport,
text_layer,
&layer.text,
scissor_rect,
&mut render_pass,
);
render_span.finish();
}
}
let _ = ManuallyDrop::into_inner(render_pass);
debug::layers_rendered(|| {
self.layers
.iter()
.filter(|layer| {
!layer.is_empty()
&& physical_bounds
.intersection(&(layer.bounds * scale_factor))
.is_some_and(|viewport| viewport.snap().is_some())
})
.count()
});
}
}
impl core::Renderer for Renderer {
fn start_layer(&mut self, bounds: Rectangle) {
self.layers.push_clip(bounds);
}
fn end_layer(&mut self) {
self.layers.pop_clip();
}
fn start_transformation(&mut self, transformation: Transformation) {
self.layers.push_transformation(transformation);
}
fn end_transformation(&mut self) {
self.layers.pop_transformation();
}
fn fill_quad(
&mut self,
quad: core::renderer::Quad,
background: impl Into<Background>,
) {
let (layer, transformation) = self.layers.current_mut();
layer.draw_quad(quad, background.into(), transformation);
}
fn reset(&mut self, new_bounds: Rectangle) {
self.layers.reset(new_bounds);
}
fn allocate_image(
&mut self,
_handle: &core::image::Handle,
_callback: impl FnOnce(Result<core::image::Allocation, core::image::Error>)
+ Send
+ 'static,
) {
#[cfg(feature = "image")]
self.image_cache
.get_mut()
.allocate_image(_handle, _callback);
}
}
impl core::text::Renderer for Renderer {
type Font = Font;
type Paragraph = Paragraph;
type Editor = Editor;
const ICON_FONT: Font = Font::with_name("Iced-Icons");
const CHECKMARK_ICON: char = '\u{f00c}';
const ARROW_DOWN_ICON: char = '\u{e800}';
const ICED_LOGO: char = '\u{e801}';
const SCROLL_UP_ICON: char = '\u{e802}';
const SCROLL_DOWN_ICON: char = '\u{e803}';
const SCROLL_LEFT_ICON: char = '\u{e804}';
const SCROLL_RIGHT_ICON: char = '\u{e805}';
fn default_font(&self) -> Self::Font {
self.default_font
}
fn default_size(&self) -> Pixels {
self.default_text_size
}
fn fill_paragraph(
&mut self,
text: &Self::Paragraph,
position: Point,
color: Color,
clip_bounds: Rectangle,
) {
let (layer, transformation) = self.layers.current_mut();
layer.draw_paragraph(
text,
position,
color,
clip_bounds,
transformation,
);
}
fn fill_editor(
&mut self,
editor: &Self::Editor,
position: Point,
color: Color,
clip_bounds: Rectangle,
) {
let (layer, transformation) = self.layers.current_mut();
layer.draw_editor(editor, position, color, clip_bounds, transformation);
}
fn fill_text(
&mut self,
text: core::Text,
position: Point,
color: Color,
clip_bounds: Rectangle,
) {
let (layer, transformation) = self.layers.current_mut();
layer.draw_text(text, position, color, clip_bounds, transformation);
}
}
impl graphics::text::Renderer for Renderer {
fn fill_raw(&mut self, raw: graphics::text::Raw) {
let (layer, transformation) = self.layers.current_mut();
layer.draw_text_raw(raw, transformation);
}
}
#[cfg(feature = "image")]
impl core::image::Renderer for Renderer {
type Handle = core::image::Handle;
fn load_image(
&self,
handle: &Self::Handle,
) -> Result<core::image::Allocation, core::image::Error> {
self.image_cache.borrow_mut().load_image(
&self.engine.device,
&self.engine.queue,
handle,
)
}
fn measure_image(&self, handle: &Self::Handle) -> Option<core::Size<u32>> {
self.image_cache.borrow_mut().measure_image(handle)
}
fn draw_image(
&mut self,
image: core::Image,
bounds: Rectangle,
clip_bounds: Rectangle,
) {
let (layer, transformation) = self.layers.current_mut();
layer.draw_raster(image, bounds, clip_bounds, transformation);
}
}
#[cfg(feature = "svg")]
impl core::svg::Renderer for Renderer {
fn measure_svg(&self, handle: &core::svg::Handle) -> core::Size<u32> {
self.image_cache.borrow_mut().measure_svg(handle)
}
fn draw_svg(
&mut self,
svg: core::Svg,
bounds: Rectangle,
clip_bounds: Rectangle,
) {
let (layer, transformation) = self.layers.current_mut();
layer.draw_svg(svg, bounds, clip_bounds, transformation);
}
}
impl graphics::mesh::Renderer for Renderer {
fn draw_mesh(&mut self, mesh: graphics::Mesh) {
debug_assert!(
!mesh.indices().is_empty(),
"Mesh must not have empty indices"
);
debug_assert!(
mesh.indices().len().is_multiple_of(3),
"Mesh indices length must be a multiple of 3"
);
let (layer, transformation) = self.layers.current_mut();
layer.draw_mesh(mesh, transformation);
}
fn draw_mesh_cache(&mut self, cache: mesh::Cache) {
let (layer, transformation) = self.layers.current_mut();
layer.draw_mesh_cache(cache, transformation);
}
}
#[cfg(feature = "geometry")]
impl graphics::geometry::Renderer for Renderer {
type Geometry = Geometry;
type Frame = geometry::Frame;
fn new_frame(&self, bounds: Rectangle) -> Self::Frame {
geometry::Frame::new(bounds)
}
fn draw_geometry(&mut self, geometry: Self::Geometry) {
let (layer, transformation) = self.layers.current_mut();
match geometry {
Geometry::Live {
meshes,
images,
text,
} => {
layer.draw_mesh_group(meshes, transformation);
for image in images {
layer.draw_image(image, transformation);
}
layer.draw_text_group(text, transformation);
}
Geometry::Cached(cache) => {
if let Some(meshes) = cache.meshes {
layer.draw_mesh_cache(meshes, transformation);
}
if let Some(images) = cache.images {
for image in images.iter().cloned() {
layer.draw_image(image, transformation);
}
}
if let Some(text) = cache.text {
layer.draw_text_cache(text, transformation);
}
}
}
}
}
impl primitive::Renderer for Renderer {
fn draw_primitive(&mut self, bounds: Rectangle, primitive: impl Primitive) {
let (layer, transformation) = self.layers.current_mut();
layer.draw_primitive(bounds, primitive, transformation);
}
}
impl graphics::compositor::Default for crate::Renderer {
type Compositor = window::Compositor;
}
impl renderer::Headless for Renderer {
async fn new(
default_font: Font,
default_text_size: Pixels,
backend: Option<&str>,
) -> Option<Self> {
if backend.is_some_and(|backend| backend != "wgpu") {
return None;
}
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::from_env()
.unwrap_or(wgpu::Backends::PRIMARY),
flags: wgpu::InstanceFlags::empty(),
..wgpu::InstanceDescriptor::default()
});
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface: None,
})
.await
.ok()?;
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("iced_wgpu [headless]"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits {
max_bind_groups: 2,
..wgpu::Limits::default()
},
memory_hints: wgpu::MemoryHints::MemoryUsage,
trace: wgpu::Trace::Off,
experimental_features: wgpu::ExperimentalFeatures::disabled(),
})
.await
.ok()?;
let engine = Engine::new(
&adapter,
device,
queue,
if graphics::color::GAMMA_CORRECTION {
wgpu::TextureFormat::Rgba8UnormSrgb
} else {
wgpu::TextureFormat::Rgba8Unorm
},
Some(graphics::Antialiasing::MSAAx4),
Shell::headless(),
);
Some(Self::new(engine, default_font, default_text_size))
}
fn name(&self) -> String {
"wgpu".to_owned()
}
fn screenshot(
&mut self,
size: Size<u32>,
scale_factor: f32,
background_color: Color,
) -> Vec<u8> {
self.screenshot(
&Viewport::with_physical_size(size, scale_factor),
background_color,
)
}
}

View file

@ -0,0 +1,242 @@
//! Draw custom primitives.
use crate::core::{self, Rectangle};
use crate::graphics::Viewport;
use crate::graphics::futures::{MaybeSend, MaybeSync};
use rustc_hash::FxHashMap;
use std::any::{Any, TypeId};
use std::fmt::Debug;
/// A batch of primitives.
pub type Batch = Vec<Instance>;
/// A set of methods which allows a [`Primitive`] to be rendered.
pub trait Primitive: Debug + MaybeSend + MaybeSync + 'static {
/// The shared renderer of this [`Primitive`].
///
/// Normally, this will contain a bunch of [`wgpu`] state; like
/// a rendering pipeline, buffers, and textures.
///
/// All instances of this [`Primitive`] type will share the same
/// [`Renderer`].
type Pipeline: Pipeline + MaybeSend + MaybeSync;
/// Processes the [`Primitive`], allowing for GPU buffer allocation.
fn prepare(
&self,
pipeline: &mut Self::Pipeline,
device: &wgpu::Device,
queue: &wgpu::Queue,
bounds: &Rectangle,
viewport: &Viewport,
);
/// Draws the [`Primitive`] in the given [`wgpu::RenderPass`].
///
/// When possible, this should be implemented over [`render`](Self::render)
/// since reusing the existing render pass should be considerably more
/// efficient than issuing a new one.
///
/// The viewport and scissor rect of the render pass provided is set
/// to the bounds and clip bounds of the [`Primitive`], respectively.
///
/// If you have complex composition needs, then you can leverage
/// [`render`](Self::render) by returning `false` here.
///
/// By default, it does nothing and returns `false`.
fn draw(
&self,
_pipeline: &Self::Pipeline,
_render_pass: &mut wgpu::RenderPass<'_>,
) -> bool {
false
}
/// Renders the [`Primitive`], using the given [`wgpu::CommandEncoder`].
///
/// This will only be called if [`draw`](Self::draw) returns `false`.
///
/// By default, it does nothing.
fn render(
&self,
_pipeline: &Self::Pipeline,
_encoder: &mut wgpu::CommandEncoder,
_target: &wgpu::TextureView,
_clip_bounds: &Rectangle<u32>,
) {
}
}
/// The pipeline of a graphics [`Primitive`].
pub trait Pipeline: Any + MaybeSend + MaybeSync {
/// Creates the [`Pipeline`] of a [`Primitive`].
///
/// This will only be called once, when the first [`Primitive`] with this kind
/// of [`Pipeline`] is encountered.
fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
) -> Self
where
Self: Sized;
/// Trims any cached data in the [`Pipeline`].
///
/// This will normally be called at the end of a frame.
fn trim(&mut self) {}
}
pub(crate) trait Stored:
Debug + MaybeSend + MaybeSync + 'static
{
fn prepare(
&self,
storage: &mut Storage,
device: &wgpu::Device,
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
bounds: &Rectangle,
viewport: &Viewport,
);
fn draw(
&self,
storage: &Storage,
render_pass: &mut wgpu::RenderPass<'_>,
) -> bool;
fn render(
&self,
storage: &Storage,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
clip_bounds: &Rectangle<u32>,
);
}
#[derive(Debug)]
struct BlackBox<P: Primitive> {
primitive: P,
}
impl<P: Primitive> Stored for BlackBox<P> {
fn prepare(
&self,
storage: &mut Storage,
device: &wgpu::Device,
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
bounds: &Rectangle,
viewport: &Viewport,
) {
if !storage.has::<P>() {
storage.store::<P, _>(P::Pipeline::new(device, queue, format));
}
let renderer = storage
.get_mut::<P>()
.expect("renderer should be initialized")
.downcast_mut::<P::Pipeline>()
.expect("renderer should have the proper type");
self.primitive
.prepare(renderer, device, queue, bounds, viewport);
}
fn draw(
&self,
storage: &Storage,
render_pass: &mut wgpu::RenderPass<'_>,
) -> bool {
let renderer = storage
.get::<P>()
.expect("renderer should be initialized")
.downcast_ref::<P::Pipeline>()
.expect("renderer should have the proper type");
self.primitive.draw(renderer, render_pass)
}
fn render(
&self,
storage: &Storage,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
clip_bounds: &Rectangle<u32>,
) {
let renderer = storage
.get::<P>()
.expect("renderer should be initialized")
.downcast_ref::<P::Pipeline>()
.expect("renderer should have the proper type");
self.primitive
.render(renderer, encoder, target, clip_bounds);
}
}
#[derive(Debug)]
/// An instance of a specific [`Primitive`].
pub struct Instance {
/// The bounds of the [`Instance`].
pub(crate) bounds: Rectangle,
/// The [`Primitive`] to render.
pub(crate) primitive: Box<dyn Stored>,
}
impl Instance {
/// Creates a new [`Instance`] with the given [`Primitive`].
pub fn new(bounds: Rectangle, primitive: impl Primitive) -> Self {
Instance {
bounds,
primitive: Box::new(BlackBox { primitive }),
}
}
}
/// A renderer than can draw custom primitives.
pub trait Renderer: core::Renderer {
/// Draws a custom primitive.
fn draw_primitive(&mut self, bounds: Rectangle, primitive: impl Primitive);
}
/// Stores custom, user-provided types.
#[derive(Default)]
pub struct Storage {
pipelines: FxHashMap<TypeId, Box<dyn Pipeline>>,
}
impl Storage {
/// Returns `true` if `Storage` contains a type `T`.
pub fn has<T: 'static>(&self) -> bool {
self.pipelines.contains_key(&TypeId::of::<T>())
}
/// Inserts the data `T` in to [`Storage`].
pub fn store<T: 'static, P: Pipeline>(&mut self, pipeline: P) {
let _ = self.pipelines.insert(TypeId::of::<T>(), Box::new(pipeline));
}
/// Returns a reference to the data with type `T` if it exists in [`Storage`].
pub fn get<T: 'static>(&self) -> Option<&dyn Any> {
self.pipelines
.get(&TypeId::of::<T>())
.map(|pipeline| pipeline.as_ref() as &dyn Any)
}
/// Returns a mutable reference to the data with type `T` if it exists in [`Storage`].
pub fn get_mut<T: 'static>(&mut self) -> Option<&mut dyn Any> {
self.pipelines
.get_mut(&TypeId::of::<T>())
.map(|pipeline| pipeline.as_mut() as &mut dyn Any)
}
/// Trims the cache of all the pipelines in the [`Storage`].
pub fn trim(&mut self) {
for pipeline in self.pipelines.values_mut() {
pipeline.trim();
}
}
}

View file

@ -0,0 +1,362 @@
mod gradient;
mod solid;
use gradient::Gradient;
use solid::Solid;
use crate::core::{Background, Rectangle, Transformation};
use crate::graphics;
use crate::graphics::color;
use bytemuck::{Pod, Zeroable};
use std::mem;
const INITIAL_INSTANCES: usize = 2_000;
/// The properties of a quad.
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
#[repr(C)]
pub struct Quad {
/// The position of the [`Quad`].
pub position: [f32; 2],
/// The size of the [`Quad`].
pub size: [f32; 2],
/// The border color of the [`Quad`], in __linear RGB__.
pub border_color: color::Packed,
/// The border radii of the [`Quad`].
pub border_radius: [f32; 4],
/// The border width of the [`Quad`].
pub border_width: f32,
/// The shadow color of the [`Quad`].
pub shadow_color: color::Packed,
/// The shadow offset of the [`Quad`].
pub shadow_offset: [f32; 2],
/// The shadow blur radius of the [`Quad`].
pub shadow_blur_radius: f32,
/// Whether the [`Quad`] should be snapped to the pixel grid.
pub snap: u32,
}
#[derive(Debug, Clone)]
pub struct Pipeline {
solid: solid::Pipeline,
gradient: gradient::Pipeline,
constant_layout: wgpu::BindGroupLayout,
}
#[derive(Default)]
pub struct State {
layers: Vec<Layer>,
prepare_layer: usize,
}
impl State {
pub fn new() -> Self {
Self::default()
}
pub fn prepare(
&mut self,
pipeline: &Pipeline,
device: &wgpu::Device,
belt: &mut wgpu::util::StagingBelt,
encoder: &mut wgpu::CommandEncoder,
quads: &Batch,
transformation: Transformation,
scale: f32,
) {
if self.layers.len() <= self.prepare_layer {
self.layers
.push(Layer::new(device, &pipeline.constant_layout));
}
let layer = &mut self.layers[self.prepare_layer];
layer.prepare(device, encoder, belt, quads, transformation, scale);
self.prepare_layer += 1;
}
pub fn render<'a>(
&'a self,
pipeline: &'a Pipeline,
layer: usize,
bounds: Rectangle<u32>,
quads: &Batch,
render_pass: &mut wgpu::RenderPass<'a>,
) {
if let Some(layer) = self.layers.get(layer) {
render_pass.set_scissor_rect(
bounds.x,
bounds.y,
bounds.width,
bounds.height,
);
let mut solid_offset = 0;
let mut gradient_offset = 0;
for (kind, count) in &quads.order {
match kind {
Kind::Solid => {
pipeline.solid.render(
render_pass,
&layer.constants,
&layer.solid,
solid_offset..(solid_offset + count),
);
solid_offset += count;
}
Kind::Gradient => {
pipeline.gradient.render(
render_pass,
&layer.constants,
&layer.gradient,
gradient_offset..(gradient_offset + count),
);
gradient_offset += count;
}
}
}
}
}
pub fn trim(&mut self) {
self.prepare_layer = 0;
}
}
impl Pipeline {
pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Pipeline {
let constant_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("iced_wgpu::quad uniforms layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: wgpu::BufferSize::new(
mem::size_of::<Uniforms>() as wgpu::BufferAddress,
),
},
count: None,
}],
});
Self {
solid: solid::Pipeline::new(device, format, &constant_layout),
gradient: gradient::Pipeline::new(device, format, &constant_layout),
constant_layout,
}
}
}
#[derive(Debug)]
pub struct Layer {
constants: wgpu::BindGroup,
constants_buffer: wgpu::Buffer,
solid: solid::Layer,
gradient: gradient::Layer,
}
impl Layer {
pub fn new(
device: &wgpu::Device,
constant_layout: &wgpu::BindGroupLayout,
) -> Self {
let constants_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("iced_wgpu::quad uniforms buffer"),
size: mem::size_of::<Uniforms>() as wgpu::BufferAddress,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let constants = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu::quad uniforms bind group"),
layout: constant_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: constants_buffer.as_entire_binding(),
}],
});
Self {
constants,
constants_buffer,
solid: solid::Layer::new(device),
gradient: gradient::Layer::new(device),
}
}
pub fn prepare(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
quads: &Batch,
transformation: Transformation,
scale: f32,
) {
self.update(device, encoder, belt, transformation, scale);
if !quads.solids.is_empty() {
self.solid.prepare(device, encoder, belt, &quads.solids);
}
if !quads.gradients.is_empty() {
self.gradient
.prepare(device, encoder, belt, &quads.gradients);
}
}
pub fn update(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
transformation: Transformation,
scale: f32,
) {
let uniforms = Uniforms::new(transformation, scale);
let bytes = bytemuck::bytes_of(&uniforms);
belt.write_buffer(
encoder,
&self.constants_buffer,
0,
(bytes.len() as u64).try_into().expect("Sized uniforms"),
device,
)
.copy_from_slice(bytes);
}
}
/// A group of [`Quad`]s rendered together.
#[derive(Default, Debug)]
pub struct Batch {
/// The solid quads of the [`Layer`].
solids: Vec<Solid>,
/// The gradient quads of the [`Layer`].
gradients: Vec<Gradient>,
/// The quad order of the [`Layer`].
order: Order,
}
/// The quad order of a [`Layer`]; stored as a tuple of the quad type & its count.
type Order = Vec<(Kind, usize)>;
impl Batch {
/// Returns true if there are no quads of any type in [`Quads`].
pub fn is_empty(&self) -> bool {
self.solids.is_empty() && self.gradients.is_empty()
}
/// Adds a [`Quad`] with the provided `Background` type to the quad [`Layer`].
pub fn add(&mut self, quad: Quad, background: &Background) {
let kind = match background {
Background::Color(color) => {
self.solids.push(Solid {
color: color::pack(*color),
quad,
});
Kind::Solid
}
Background::Gradient(gradient) => {
self.gradients.push(Gradient {
gradient: graphics::gradient::pack(
gradient,
Rectangle::new(quad.position.into(), quad.size.into()),
),
quad,
});
Kind::Gradient
}
};
match self.order.last_mut() {
Some((last_kind, count)) if kind == *last_kind => {
*count += 1;
}
_ => {
self.order.push((kind, 1));
}
}
}
pub fn clear(&mut self) {
self.solids.clear();
self.gradients.clear();
self.order.clear();
}
pub fn append(&mut self, batch: &mut Batch) {
self.solids.append(&mut batch.solids);
self.gradients.append(&mut batch.gradients);
self.order.append(&mut batch.order);
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
/// The kind of a quad.
enum Kind {
/// A solid quad
Solid,
/// A gradient quad
Gradient,
}
fn color_target_state(
format: wgpu::TextureFormat,
) -> [Option<wgpu::ColorTargetState>; 1] {
[Some(wgpu::ColorTargetState {
format,
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})]
}
#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
struct Uniforms {
transform: [f32; 16],
scale: f32,
// Uniforms must be aligned to their largest member,
// this uses a mat4x4<f32> which aligns to 16, so align to that
_padding: [f32; 3],
}
impl Uniforms {
fn new(transformation: Transformation, scale: f32) -> Uniforms {
Self {
transform: *transformation.as_ref(),
scale,
_padding: [0.0; 3],
}
}
}
impl Default for Uniforms {
fn default() -> Self {
Self {
transform: *Transformation::IDENTITY.as_ref(),
scale: 1.0,
_padding: [0.0; 3],
}
}
}

View file

@ -0,0 +1,187 @@
use crate::Buffer;
use crate::graphics::gradient;
use crate::quad::{self, Quad};
use bytemuck::{Pod, Zeroable};
use std::ops::Range;
/// A quad filled with interpolated colors.
#[derive(Clone, Copy, Debug)]
#[repr(C)]
pub struct Gradient {
/// The background gradient data of the quad.
pub gradient: gradient::Packed,
/// The [`Quad`] data of the [`Gradient`].
pub quad: Quad,
}
#[allow(unsafe_code)]
unsafe impl Pod for Gradient {}
#[allow(unsafe_code)]
unsafe impl Zeroable for Gradient {}
#[derive(Debug)]
pub struct Layer {
instances: Buffer<Gradient>,
instance_count: usize,
}
impl Layer {
pub fn new(device: &wgpu::Device) -> Self {
let instances = Buffer::new(
device,
"iced_wgpu.quad.gradient.buffer",
quad::INITIAL_INSTANCES,
wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
);
Self {
instances,
instance_count: 0,
}
}
pub fn prepare(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
instances: &[Gradient],
) {
let _ = self.instances.resize(device, instances.len());
let _ = self.instances.write(device, encoder, belt, 0, instances);
self.instance_count = instances.len();
}
}
#[derive(Debug, Clone)]
pub struct Pipeline {
#[cfg(not(target_arch = "wasm32"))]
pipeline: wgpu::RenderPipeline,
}
impl Pipeline {
#[allow(unused_variables)]
pub fn new(
device: &wgpu::Device,
format: wgpu::TextureFormat,
constants_layout: &wgpu::BindGroupLayout,
) -> Self {
#[cfg(not(target_arch = "wasm32"))]
{
let layout = device.create_pipeline_layout(
&wgpu::PipelineLayoutDescriptor {
label: Some("iced_wgpu.quad.gradient.pipeline"),
push_constant_ranges: &[],
bind_group_layouts: &[constants_layout],
},
);
let shader =
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("iced_wgpu.quad.gradient.shader"),
source: wgpu::ShaderSource::Wgsl(
std::borrow::Cow::Borrowed(concat!(
include_str!("../shader/quad.wgsl"),
"\n",
include_str!("../shader/vertex.wgsl"),
"\n",
include_str!("../shader/quad/gradient.wgsl"),
"\n",
include_str!("../shader/color.wgsl"),
"\n",
include_str!("../shader/color/linear_rgb.wgsl")
)),
),
});
let pipeline = device.create_render_pipeline(
&wgpu::RenderPipelineDescriptor {
label: Some("iced_wgpu.quad.gradient.pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("gradient_vs_main"),
buffers: &[wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Gradient>()
as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &wgpu::vertex_attr_array!(
// Colors 1-2
0 => Uint32x4,
// Colors 3-4
1 => Uint32x4,
// Colors 5-6
2 => Uint32x4,
// Colors 7-8
3 => Uint32x4,
// Offsets 1-8
4 => Uint32x4,
// Direction
5 => Float32x4,
// Position & Scale
6 => Float32x4,
// Border color
7 => Float32x4,
// Border radius
8 => Float32x4,
// Border width
9 => Float32,
// Snap
10 => Uint32,
),
}],
compilation_options:
wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("gradient_fs_main"),
targets: &quad::color_target_state(format),
compilation_options:
wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
front_face: wgpu::FrontFace::Cw,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None,
cache: None,
},
);
Self { pipeline }
}
#[cfg(target_arch = "wasm32")]
Self {}
}
#[allow(unused_variables)]
pub fn render<'a>(
&'a self,
render_pass: &mut wgpu::RenderPass<'a>,
constants: &'a wgpu::BindGroup,
layer: &'a Layer,
range: Range<usize>,
) {
#[cfg(not(target_arch = "wasm32"))]
{
render_pass.set_pipeline(&self.pipeline);
render_pass.set_bind_group(0, constants, &[]);
render_pass.set_vertex_buffer(0, layer.instances.slice(..));
render_pass.draw(0..6, range.start as u32..range.end as u32);
}
}
}

View file

@ -0,0 +1,162 @@
use crate::Buffer;
use crate::graphics::color;
use crate::quad::{self, Quad};
use bytemuck::{Pod, Zeroable};
use std::ops::Range;
/// A quad filled with a solid color.
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
#[repr(C)]
pub struct Solid {
/// The background color data of the quad.
pub color: color::Packed,
/// The [`Quad`] data of the [`Solid`].
pub quad: Quad,
}
#[derive(Debug)]
pub struct Layer {
instances: Buffer<Solid>,
instance_count: usize,
}
impl Layer {
pub fn new(device: &wgpu::Device) -> Self {
let instances = Buffer::new(
device,
"iced_wgpu.quad.solid.buffer",
quad::INITIAL_INSTANCES,
wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
);
Self {
instances,
instance_count: 0,
}
}
pub fn prepare(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
instances: &[Solid],
) {
let _ = self.instances.resize(device, instances.len());
let _ = self.instances.write(device, encoder, belt, 0, instances);
self.instance_count = instances.len();
}
}
#[derive(Debug, Clone)]
pub struct Pipeline {
pipeline: wgpu::RenderPipeline,
}
impl Pipeline {
pub fn new(
device: &wgpu::Device,
format: wgpu::TextureFormat,
constants_layout: &wgpu::BindGroupLayout,
) -> Self {
let layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("iced_wgpu.quad.solid.pipeline"),
push_constant_ranges: &[],
bind_group_layouts: &[constants_layout],
});
let shader =
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("iced_wgpu.quad.solid.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
concat!(
include_str!("../shader/color.wgsl"),
"\n",
include_str!("../shader/quad.wgsl"),
"\n",
include_str!("../shader/vertex.wgsl"),
"\n",
include_str!("../shader/quad/solid.wgsl"),
),
)),
});
let pipeline =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("iced_wgpu.quad.solid.pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("solid_vs_main"),
buffers: &[wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Solid>() as u64,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &wgpu::vertex_attr_array!(
// Color
0 => Float32x4,
// Position
1 => Float32x2,
// Size
2 => Float32x2,
// Border color
3 => Float32x4,
// Border radius
4 => Float32x4,
// Border width
5 => Float32,
// Shadow color
6 => Float32x4,
// Shadow offset
7 => Float32x2,
// Shadow blur radius
8 => Float32,
// Snap
9 => Uint32,
),
}],
compilation_options:
wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("solid_fs_main"),
targets: &quad::color_target_state(format),
compilation_options:
wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
front_face: wgpu::FrontFace::Cw,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None,
cache: None,
});
Self { pipeline }
}
pub fn render<'a>(
&'a self,
render_pass: &mut wgpu::RenderPass<'a>,
constants: &'a wgpu::BindGroup,
layer: &'a Layer,
range: Range<usize>,
) {
render_pass.set_pipeline(&self.pipeline);
render_pass.set_bind_group(0, constants, &[]);
render_pass.set_vertex_buffer(0, layer.instances.slice(..));
render_pass.draw(0..6, range.start as u32..range.end as u32);
}
}

View file

@ -0,0 +1,84 @@
//! Configure a renderer.
use crate::core::{Font, Pixels};
use crate::graphics::{self, Antialiasing};
/// The settings of a [`Renderer`].
///
/// [`Renderer`]: crate::Renderer
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Settings {
/// The present mode of the [`Renderer`].
///
/// [`Renderer`]: crate::Renderer
pub present_mode: wgpu::PresentMode,
/// The graphics backends to use.
pub backends: wgpu::Backends,
/// The default [`Font`] to use.
pub default_font: Font,
/// The default size of text.
///
/// By default, it will be set to `16.0`.
pub default_text_size: Pixels,
/// The antialiasing strategy that will be used for triangle primitives.
///
/// By default, it is `None`.
pub antialiasing: Option<Antialiasing>,
}
impl Default for Settings {
fn default() -> Settings {
Settings {
present_mode: wgpu::PresentMode::AutoVsync,
backends: wgpu::Backends::all(),
default_font: Font::default(),
default_text_size: Pixels(16.0),
antialiasing: None,
}
}
}
impl From<graphics::Settings> for Settings {
fn from(settings: graphics::Settings) -> Self {
Self {
present_mode: if settings.vsync {
wgpu::PresentMode::AutoVsync
} else {
wgpu::PresentMode::AutoNoVsync
},
default_font: settings.default_font,
default_text_size: settings.default_text_size,
antialiasing: settings.antialiasing,
..Settings::default()
}
}
}
/// Obtains a [`wgpu::PresentMode`] from the current environment
/// configuration, if set.
///
/// The value returned by this function can be changed by setting
/// the `ICED_PRESENT_MODE` env variable. The possible values are:
///
/// - `vsync` → [`wgpu::PresentMode::AutoVsync`]
/// - `no_vsync` → [`wgpu::PresentMode::AutoNoVsync`]
/// - `immediate` → [`wgpu::PresentMode::Immediate`]
/// - `fifo` → [`wgpu::PresentMode::Fifo`]
/// - `fifo_relaxed` → [`wgpu::PresentMode::FifoRelaxed`]
/// - `mailbox` → [`wgpu::PresentMode::Mailbox`]
pub fn present_mode_from_env() -> Option<wgpu::PresentMode> {
let present_mode = std::env::var("ICED_PRESENT_MODE").ok()?;
match present_mode.to_lowercase().as_str() {
"vsync" => Some(wgpu::PresentMode::AutoVsync),
"no_vsync" => Some(wgpu::PresentMode::AutoNoVsync),
"immediate" => Some(wgpu::PresentMode::Immediate),
"fifo" => Some(wgpu::PresentMode::Fifo),
"fifo_relaxed" => Some(wgpu::PresentMode::FifoRelaxed),
"mailbox" => Some(wgpu::PresentMode::Mailbox),
_ => None,
}
}

View file

@ -0,0 +1,37 @@
var<private> uvs: array<vec2<f32>, 6> = array<vec2<f32>, 6>(
vec2<f32>(0.0, 0.0),
vec2<f32>(1.0, 0.0),
vec2<f32>(1.0, 1.0),
vec2<f32>(0.0, 0.0),
vec2<f32>(0.0, 1.0),
vec2<f32>(1.0, 1.0)
);
@group(0) @binding(0) var u_sampler: sampler;
@group(0) @binding(1) var<uniform> u_ratio: vec4<f32>;
@group(1) @binding(0) var u_texture: texture_2d<f32>;
struct VertexInput {
@builtin(vertex_index) vertex_index: u32,
}
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@vertex
fn vs_main(input: VertexInput) -> VertexOutput {
let uv = uvs[input.vertex_index];
var out: VertexOutput;
out.uv = uv * u_ratio.xy;
out.position = vec4<f32>(uv * vec2(2.0, -2.0) + vec2(-1.0, 1.0), 0.0, 1.0);
return out;
}
@fragment
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
return textureSample(u_texture, u_sampler, input.uv);
}

View file

@ -0,0 +1,14 @@
fn premultiply(color: vec4<f32>) -> vec4<f32> {
return vec4(color.xyz * color.a, color.a);
}
fn unpack_color(data: vec2<u32>) -> vec4<f32> {
return premultiply(unpack_u32(data));
}
fn unpack_u32(data: vec2<u32>) -> vec4<f32> {
let rg: vec2<f32> = unpack2x16float(data.x);
let ba: vec2<f32> = unpack2x16float(data.y);
return vec4<f32>(rg.y, rg.x, ba.y, ba.x);
}

View file

@ -0,0 +1,3 @@
fn interpolate_color(from_: vec4<f32>, to_: vec4<f32>, factor: f32) -> vec4<f32> {
return mix(from_, to_, factor);
}

View file

@ -0,0 +1,129 @@
struct Globals {
transform: mat4x4<f32>,
scale_factor: f32,
}
@group(0) @binding(0) var<uniform> globals: Globals;
@group(0) @binding(1) var u_sampler: sampler;
@group(1) @binding(0) var u_texture: texture_2d_array<f32>;
struct VertexInput {
@builtin(vertex_index) vertex_index: u32,
@location(0) center: vec2<f32>,
@location(1) clip_bounds: vec4<f32>,
@location(2) border_radius: vec4<f32>,
@location(3) tile: vec4<f32>,
@location(4) rotation: f32,
@location(5) opacity: f32,
@location(6) atlas_pos: vec2<f32>,
@location(7) atlas_scale: vec2<f32>,
@location(8) layer: i32,
@location(9) snap: u32,
}
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) @interpolate(flat) clip_bounds: vec4<f32>,
@location(1) @interpolate(flat) border_radius: vec4<f32>,
@location(2) @interpolate(flat) atlas: vec4<f32>,
@location(3) @interpolate(flat) layer: i32,
@location(4) @interpolate(flat) opacity: f32,
@location(5) uv: vec2<f32>,
}
@vertex
fn vs_main(input: VertexInput) -> VertexOutput {
var out: VertexOutput;
// Generate a vertex position in the range [0, 1] from the vertex index
let corner = vertex_position(input.vertex_index);
let tile = input.tile;
let center = input.center;
// List the unrotated tile corners
let corners = array<vec2<f32>, 4>(
tile.xy, // Top left
tile.xy + vec2<f32>(tile.z, 0.0), // Top right
tile.xy + vec2<f32>(0.0, tile.w), // Bottom left
tile.xy + tile.zw // Bottom right
);
// Rotate tile corners around center
let cos_r = cos(-input.rotation); // Clockwise
let sin_r = sin(-input.rotation);
var rotated = array<vec2<f32>, 4>();
for (var i = 0u; i < 4u; i++) {
let c = corners[i] - input.center;
rotated[i] = vec2<f32>(c.x * cos_r - c.y * sin_r, c.x * sin_r + c.y * cos_r) + input.center;
}
// Find bounding box of rotated tile
var min_xy = rotated[0];
var max_xy = rotated[0];
for (var i = 1u; i < 4u; i++) {
min_xy = min(min_xy, rotated[i]);
max_xy = max(max_xy, rotated[i]);
}
let rotated_bounds = vec4<f32>(min_xy, max_xy - min_xy);
// Intersect with clip bounds
let clip_min = max(rotated_bounds.xy, input.clip_bounds.xy);
let clip_max = min(rotated_bounds.xy + rotated_bounds.zw, input.clip_bounds.xy + input.clip_bounds.zw);
let clipped_tile = vec4<f32>(clip_min, max(vec2<f32>(0.0), clip_max - clip_min));
// Calculate the vertex position
let v_pos = clipped_tile.xy + corner * clipped_tile.zw;
out.position = vec4(vec2(globals.scale_factor), 1.0, 1.0) * vec4<f32>(v_pos, 0.0, 1.0);
out.clip_bounds = globals.scale_factor * input.clip_bounds;
// Calculate rotated UV
let uv = input.atlas_pos + (v_pos - tile.xy) / tile.zw * input.atlas_scale;
let uv_center = input.atlas_pos + input.atlas_scale / 2.0;
let d = uv - uv_center;
out.uv = vec2<f32>(d.x * cos_r - d.y * sin_r, d.x * sin_r + d.y * cos_r) + uv_center;
// Snap position to the pixel grid
if bool(input.snap) {
out.position = round(out.position);
out.clip_bounds = vec4(
round(out.clip_bounds.xy),
round(out.clip_bounds.xy + out.clip_bounds.zw) - out.clip_bounds.xy,
);
}
out.position = globals.transform * out.position;
out.border_radius = globals.scale_factor * min(input.border_radius, vec4(min(input.clip_bounds.z, input.clip_bounds.w) / 2.0));
out.atlas = vec4(input.atlas_pos, input.atlas_pos + input.atlas_scale);
out.layer = input.layer;
out.opacity = input.opacity;
return out;
}
@fragment
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
let fragment = input.position.xy;
let position = input.clip_bounds.xy;
let scale = input.clip_bounds.zw;
let d = rounded_box_sdf(
2.0 * (fragment - position - scale / 2.0),
scale,
input.border_radius * 2.0,
) / 2.0;
let antialias: f32 = clamp(1.0 - d, 0.0, 1.0);
let inside = all(input.uv >= input.atlas.xy) && all(input.uv <= input.atlas.zw);
return textureSample(u_texture, u_sampler, input.uv, input.layer) * vec4<f32>(1.0, 1.0, 1.0, antialias * input.opacity * f32(inside));
}
fn rounded_box_sdf(p: vec2<f32>, size: vec2<f32>, corners: vec4<f32>) -> f32 {
var box_half = select(corners.yz, corners.xw, p.x > 0.0);
var corner = select(box_half.y, box_half.x, p.y > 0.0);
var q = abs(p) - size + corner;
return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - corner;
}

View file

@ -0,0 +1,13 @@
struct Globals {
transform: mat4x4<f32>,
scale: f32,
}
@group(0) @binding(0) var<uniform> globals: Globals;
fn rounded_box_sdf(p: vec2<f32>, size: vec2<f32>, corners: vec4<f32>) -> f32 {
var box_half = select(corners.yz, corners.xw, p.x > 0.0);
var corner = select(box_half.y, box_half.x, p.y > 0.0);
var q = abs(p) - size + corner;
return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - corner;
}

View file

@ -0,0 +1,184 @@
struct GradientVertexInput {
@builtin(vertex_index) vertex_index: u32,
@location(0) @interpolate(flat) colors_1: vec4<u32>,
@location(1) @interpolate(flat) colors_2: vec4<u32>,
@location(2) @interpolate(flat) colors_3: vec4<u32>,
@location(3) @interpolate(flat) colors_4: vec4<u32>,
@location(4) @interpolate(flat) offsets: vec4<u32>,
@location(5) direction: vec4<f32>,
@location(6) position_and_scale: vec4<f32>,
@location(7) border_color: vec4<f32>,
@location(8) border_radius: vec4<f32>,
@location(9) border_width: f32,
@location(10) snap: u32,
}
struct GradientVertexOutput {
@builtin(position) position: vec4<f32>,
@location(1) @interpolate(flat) colors_1: vec4<u32>,
@location(2) @interpolate(flat) colors_2: vec4<u32>,
@location(3) @interpolate(flat) colors_3: vec4<u32>,
@location(4) @interpolate(flat) colors_4: vec4<u32>,
@location(5) @interpolate(flat) offsets: vec4<u32>,
@location(6) direction: vec4<f32>,
@location(7) position_and_scale: vec4<f32>,
@location(8) border_color: vec4<f32>,
@location(9) border_radius: vec4<f32>,
@location(10) border_width: f32,
}
@vertex
fn gradient_vs_main(input: GradientVertexInput) -> GradientVertexOutput {
var out: GradientVertexOutput;
var pos: vec2<f32> = input.position_and_scale.xy * globals.scale;
var scale: vec2<f32> = input.position_and_scale.zw * globals.scale;
var pos_snap = vec2<f32>(0.0, 0.0);
var scale_snap = vec2<f32>(0.0, 0.0);
if bool(input.snap) {
pos_snap = round(pos + vec2(0.001, 0.001)) - pos;
scale_snap = round(pos + scale + vec2(0.001, 0.001)) - pos - pos_snap - scale;
}
var min_border_radius = min(input.position_and_scale.z, input.position_and_scale.w) * 0.5;
var border_radius: vec4<f32> = vec4<f32>(
min(input.border_radius.x, min_border_radius),
min(input.border_radius.y, min_border_radius),
min(input.border_radius.z, min_border_radius),
min(input.border_radius.w, min_border_radius)
);
var transform: mat4x4<f32> = mat4x4<f32>(
vec4<f32>(scale.x + scale_snap.x + 1.0, 0.0, 0.0, 0.0),
vec4<f32>(0.0, scale.y + scale_snap.y + 1.0, 0.0, 0.0),
vec4<f32>(0.0, 0.0, 1.0, 0.0),
vec4<f32>(pos + pos_snap - vec2<f32>(0.5, 0.5), 0.0, 1.0)
);
out.position = globals.transform * transform * vec4<f32>(vertex_position(input.vertex_index), 0.0, 1.0);
out.colors_1 = input.colors_1;
out.colors_2 = input.colors_2;
out.colors_3 = input.colors_3;
out.colors_4 = input.colors_4;
out.offsets = input.offsets;
out.direction = input.direction * globals.scale;
out.position_and_scale = vec4<f32>(pos + pos_snap, scale + scale_snap);
out.border_color = premultiply(input.border_color);
out.border_radius = border_radius * globals.scale;
out.border_width = input.border_width * globals.scale;
return out;
}
fn random(coords: vec2<f32>) -> f32 {
return fract(sin(dot(coords, vec2(12.9898,78.233))) * 43758.5453);
}
/// Returns the current interpolated color with a max 8-stop gradient
fn gradient(
raw_position: vec2<f32>,
direction: vec4<f32>,
colors: array<vec4<f32>, 8>,
offsets: array<f32, 8>,
last_index: i32
) -> vec4<f32> {
let start = direction.xy;
let end = direction.zw;
let v1 = end - start;
let v2 = raw_position - start;
let unit = normalize(v1);
let coord_offset = dot(unit, v2) / length(v1);
//need to store these as a var to use dynamic indexing in a loop
//this is already added to wgsl spec but not in wgpu yet
var colors_arr = colors;
var offsets_arr = offsets;
var color: vec4<f32>;
let noise_granularity: f32 = 0.3/255.0;
for (var i: i32 = 0; i < last_index; i++) {
let curr_offset = offsets_arr[i];
let next_offset = offsets_arr[i+1];
if (coord_offset <= offsets_arr[0]) {
color = colors_arr[0];
}
if (curr_offset <= coord_offset && coord_offset <= next_offset) {
let from_ = colors_arr[i];
let to_ = colors_arr[i+1];
let factor = smoothstep(curr_offset, next_offset, coord_offset);
color = interpolate_color(from_, to_, factor);
}
if (coord_offset >= offsets_arr[last_index]) {
color = colors_arr[last_index];
}
}
return color + mix(-noise_granularity, noise_granularity, random(raw_position));
}
@fragment
fn gradient_fs_main(input: GradientVertexOutput) -> @location(0) vec4<f32> {
let colors = array<vec4<f32>, 8>(
unpack_color(input.colors_1.xy),
unpack_color(input.colors_1.zw),
unpack_color(input.colors_2.xy),
unpack_color(input.colors_2.zw),
unpack_color(input.colors_3.xy),
unpack_color(input.colors_3.zw),
unpack_color(input.colors_4.xy),
unpack_color(input.colors_4.zw),
);
let offsets_1: vec4<f32> = unpack_u32(input.offsets.xy);
let offsets_2: vec4<f32> = unpack_u32(input.offsets.zw);
var offsets = array<f32, 8>(
offsets_1.x,
offsets_1.y,
offsets_1.z,
offsets_1.w,
offsets_2.x,
offsets_2.y,
offsets_2.z,
offsets_2.w,
);
//TODO could just pass this in to the shader but is probably more performant to just check it here
var last_index = 7;
for (var i: i32 = 0; i <= 7; i++) {
if (offsets[i] > 1.0) {
last_index = i - 1;
break;
}
}
var mixed_color: vec4<f32> = gradient(input.position.xy, input.direction, colors, offsets, last_index);
let pos = input.position_and_scale.xy;
let scale = input.position_and_scale.zw;
var dist: f32 = rounded_box_sdf(
-(input.position.xy - pos - scale / 2.0) * 2.0,
scale,
input.border_radius * 2.0
) / 2.0;
if (input.border_width > 0.0) {
mixed_color = mix(
mixed_color,
input.border_color,
clamp(0.5 + dist + input.border_width, 0.0, 1.0)
);
}
return mixed_color * clamp(0.5-dist, 0.0, 1.0);
}

View file

@ -0,0 +1,102 @@
struct SolidVertexInput {
@builtin(vertex_index) vertex_index: u32,
@location(0) color: vec4<f32>,
@location(1) pos: vec2<f32>,
@location(2) scale: vec2<f32>,
@location(3) border_color: vec4<f32>,
@location(4) border_radius: vec4<f32>,
@location(5) border_width: f32,
@location(6) shadow_color: vec4<f32>,
@location(7) shadow_offset: vec2<f32>,
@location(8) shadow_blur_radius: f32,
@location(9) snap: u32,
}
struct SolidVertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) color: vec4<f32>,
@location(1) border_color: vec4<f32>,
@location(2) pos: vec2<f32>,
@location(3) scale: vec2<f32>,
@location(4) border_radius: vec4<f32>,
@location(5) border_width: f32,
@location(6) shadow_color: vec4<f32>,
@location(7) shadow_offset: vec2<f32>,
@location(8) shadow_blur_radius: f32,
}
@vertex
fn solid_vs_main(input: SolidVertexInput) -> SolidVertexOutput {
var out: SolidVertexOutput;
var pos: vec2<f32> = (input.pos + min(input.shadow_offset, vec2<f32>(0.0, 0.0)) - input.shadow_blur_radius) * globals.scale;
var scale: vec2<f32> = (input.scale + vec2<f32>(abs(input.shadow_offset.x), abs(input.shadow_offset.y)) + input.shadow_blur_radius * 2.0) * globals.scale;
var pos_snap = vec2<f32>(0.0, 0.0);
var scale_snap = vec2<f32>(0.0, 0.0);
if bool(input.snap) {
pos_snap = round(pos + vec2(0.001, 0.001)) - pos;
scale_snap = round(pos + scale + vec2(0.001, 0.001)) - pos - pos_snap - scale;
}
let border_radius = min(input.border_radius, vec4(min(input.scale.x, input.scale.y) / 2.0));
var transform: mat4x4<f32> = mat4x4<f32>(
vec4<f32>(scale.x + scale_snap.x + 1.0, 0.0, 0.0, 0.0),
vec4<f32>(0.0, scale.y + scale_snap.y + 1.0, 0.0, 0.0),
vec4<f32>(0.0, 0.0, 1.0, 0.0),
vec4<f32>(pos + pos_snap - vec2<f32>(0.5, 0.5), 0.0, 1.0)
);
out.position = globals.transform * transform * vec4<f32>(vertex_position(input.vertex_index), 0.0, 1.0);
out.color = premultiply(input.color);
out.border_color = premultiply(input.border_color);
out.pos = input.pos * globals.scale + pos_snap;
out.scale = input.scale * globals.scale + scale_snap;
out.border_radius = border_radius * globals.scale;
out.border_width = input.border_width * globals.scale;
out.shadow_color = premultiply(input.shadow_color);
out.shadow_offset = input.shadow_offset * globals.scale;
out.shadow_blur_radius = input.shadow_blur_radius * globals.scale;
return out;
}
@fragment
fn solid_fs_main(
input: SolidVertexOutput
) -> @location(0) vec4<f32> {
var mixed_color: vec4<f32> = input.color;
var dist = rounded_box_sdf(
-(input.position.xy - input.pos - input.scale * 0.5) * 2.0,
input.scale,
input.border_radius * 2.0
) / 2.0;
if (input.border_width > 0.0) {
mixed_color = mix(
input.color,
input.border_color,
clamp(0.5 + dist + input.border_width, 0.0, 1.0)
);
}
var quad_alpha: f32 = clamp(0.5-dist, 0.0, 1.0);
let quad_color = mixed_color * quad_alpha;
if input.shadow_color.a > 0.0 {
var shadow_dist: f32 = rounded_box_sdf(
-(input.position.xy - input.pos - input.shadow_offset - input.scale/2.0) * 2.0,
input.scale,
input.border_radius * 2.0
) / 2.0;
let shadow_alpha = 1.0 - smoothstep(-input.shadow_blur_radius, input.shadow_blur_radius, max(shadow_dist, 0.0));
return mix(quad_color, input.shadow_color, (1.0 - quad_alpha) * shadow_alpha);
} else {
return quad_color;
}
}

View file

@ -0,0 +1,5 @@
struct Globals {
transform: mat4x4<f32>,
}
@group(0) @binding(0) var<uniform> globals: Globals;

View file

@ -0,0 +1,127 @@
struct GradientVertexInput {
@location(0) v_pos: vec2<f32>,
@location(1) @interpolate(flat) colors_1: vec4<u32>,
@location(2) @interpolate(flat) colors_2: vec4<u32>,
@location(3) @interpolate(flat) colors_3: vec4<u32>,
@location(4) @interpolate(flat) colors_4: vec4<u32>,
@location(5) @interpolate(flat) offsets: vec4<u32>,
@location(6) direction: vec4<f32>,
}
struct GradientVertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) raw_position: vec2<f32>,
@location(1) @interpolate(flat) colors_1: vec4<u32>,
@location(2) @interpolate(flat) colors_2: vec4<u32>,
@location(3) @interpolate(flat) colors_3: vec4<u32>,
@location(4) @interpolate(flat) colors_4: vec4<u32>,
@location(5) @interpolate(flat) offsets: vec4<u32>,
@location(6) direction: vec4<f32>,
}
@vertex
fn gradient_vs_main(input: GradientVertexInput) -> GradientVertexOutput {
var output: GradientVertexOutput;
output.position = globals.transform * vec4<f32>(input.v_pos, 0.0, 1.0);
output.raw_position = input.v_pos;
output.colors_1 = input.colors_1;
output.colors_2 = input.colors_2;
output.colors_3 = input.colors_3;
output.colors_4 = input.colors_4;
output.offsets = input.offsets;
output.direction = input.direction;
return output;
}
/// Returns the current interpolated color with a max 8-stop gradient
fn gradient(
raw_position: vec2<f32>,
direction: vec4<f32>,
colors: array<vec4<f32>, 8>,
offsets: array<f32, 8>,
last_index: i32
) -> vec4<f32> {
let start = direction.xy;
let end = direction.zw;
let v1 = end - start;
let v2 = raw_position - start;
let unit = normalize(v1);
let coord_offset = dot(unit, v2) / length(v1);
//need to store these as a var to use dynamic indexing in a loop
//this is already added to wgsl spec but not in wgpu yet
var colors_arr = colors;
var offsets_arr = offsets;
var color: vec4<f32>;
let noise_granularity: f32 = 0.3/255.0;
for (var i: i32 = 0; i < last_index; i++) {
let curr_offset = offsets_arr[i];
let next_offset = offsets_arr[i+1];
if (coord_offset <= offsets_arr[0]) {
color = colors_arr[0];
}
if (curr_offset <= coord_offset && coord_offset <= next_offset) {
let from_ = colors_arr[i];
let to_ = colors_arr[i+1];
let factor = smoothstep(curr_offset, next_offset, coord_offset);
color = interpolate_color(from_, to_, factor);
}
if (coord_offset >= offsets_arr[last_index]) {
color = colors_arr[last_index];
}
}
return color + mix(-noise_granularity, noise_granularity, random(raw_position));
}
@fragment
fn gradient_fs_main(input: GradientVertexOutput) -> @location(0) vec4<f32> {
let colors = array<vec4<f32>, 8>(
unpack_color(input.colors_1.xy),
unpack_color(input.colors_1.zw),
unpack_color(input.colors_2.xy),
unpack_color(input.colors_2.zw),
unpack_color(input.colors_3.xy),
unpack_color(input.colors_3.zw),
unpack_color(input.colors_4.xy),
unpack_color(input.colors_4.zw),
);
let offsets_1: vec4<f32> = unpack_u32(input.offsets.xy);
let offsets_2: vec4<f32> = unpack_u32(input.offsets.zw);
var offsets = array<f32, 8>(
offsets_1.x,
offsets_1.y,
offsets_1.z,
offsets_1.w,
offsets_2.x,
offsets_2.y,
offsets_2.z,
offsets_2.w,
);
var last_index = 7;
for (var i: i32 = 0; i <= 7; i++) {
if (offsets[i] >= 1.0) {
last_index = i;
break;
}
}
return gradient(input.raw_position, input.direction, colors, offsets, last_index);
}
fn random(coords: vec2<f32>) -> f32 {
return fract(sin(dot(coords, vec2(12.9898,78.233))) * 43758.5453);
}

View file

@ -0,0 +1,24 @@
struct SolidVertexInput {
@location(0) position: vec2<f32>,
@location(1) color: vec4<f32>,
}
struct SolidVertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) color: vec4<f32>,
}
@vertex
fn solid_vs_main(input: SolidVertexInput) -> SolidVertexOutput {
var out: SolidVertexOutput;
out.color = premultiply(input.color);
out.position = globals.transform * vec4<f32>(input.position, 0.0, 1.0);
return out;
}
@fragment
fn solid_fs_main(input: SolidVertexOutput) -> @location(0) vec4<f32> {
return input.color;
}

View file

@ -0,0 +1,7 @@
// Compute the normalized quad coordinates based on the vertex index.
fn vertex_position(vertex_index: u32) -> vec2<f32> {
// #: 0 1 2 3 4 5
// x: 1 1 0 0 0 1
// y: 1 0 0 0 1 1
return vec2<f32>((vec2(1u, 2u) + vertex_index) % vec2(6u) < vec2(3u));
}

View file

@ -0,0 +1,648 @@
use crate::core::alignment;
use crate::core::text::Alignment;
use crate::core::{Rectangle, Size, Transformation};
use crate::graphics::cache;
use crate::graphics::color;
use crate::graphics::text::cache::{self as text_cache, Cache as BufferCache};
use crate::graphics::text::{Editor, Paragraph, font_system, to_color};
use rustc_hash::FxHashMap;
use std::collections::hash_map;
use std::sync::atomic::{self, AtomicU64};
use std::sync::{self, Arc, RwLock};
pub use crate::graphics::Text;
const COLOR_MODE: cryoglyph::ColorMode = if color::GAMMA_CORRECTION {
cryoglyph::ColorMode::Accurate
} else {
cryoglyph::ColorMode::Web
};
pub type Batch = Vec<Item>;
#[derive(Debug)]
pub enum Item {
Group {
transformation: Transformation,
text: Vec<Text>,
},
Cached {
transformation: Transformation,
cache: Cache,
},
}
#[derive(Debug, Clone)]
pub struct Cache {
id: Id,
group: cache::Group,
text: Arc<[Text]>,
version: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Id(u64);
impl Cache {
pub fn new(group: cache::Group, text: Vec<Text>) -> Option<Self> {
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
if text.is_empty() {
return None;
}
Some(Self {
id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)),
group,
text: Arc::from(text),
version: 0,
})
}
pub fn update(&mut self, text: Vec<Text>) {
if self.text.is_empty() && text.is_empty() {
return;
}
self.text = Arc::from(text);
self.version += 1;
}
}
struct Upload {
renderer: cryoglyph::TextRenderer,
buffer_cache: BufferCache,
transformation: Transformation,
version: usize,
group_version: usize,
text: sync::Weak<[Text]>,
_atlas: sync::Weak<()>,
}
#[derive(Default)]
pub struct Storage {
groups: FxHashMap<cache::Group, Group>,
uploads: FxHashMap<Id, Upload>,
}
struct Group {
atlas: cryoglyph::TextAtlas,
version: usize,
should_trim: bool,
handle: Arc<()>, // Keeps track of active uploads
}
impl Storage {
fn get(&self, cache: &Cache) -> Option<(&cryoglyph::TextAtlas, &Upload)> {
if cache.text.is_empty() {
return None;
}
self.groups
.get(&cache.group)
.map(|group| &group.atlas)
.zip(self.uploads.get(&cache.id))
}
fn prepare(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
viewport: &cryoglyph::Viewport,
encoder: &mut wgpu::CommandEncoder,
format: wgpu::TextureFormat,
state: &cryoglyph::Cache,
cache: &Cache,
new_transformation: Transformation,
bounds: Rectangle,
) {
let group_count = self.groups.len();
let group = self.groups.entry(cache.group).or_insert_with(|| {
log::debug!(
"New text atlas: {:?} (total: {})",
cache.group,
group_count + 1
);
Group {
atlas: cryoglyph::TextAtlas::with_color_mode(
device, queue, state, format, COLOR_MODE,
),
version: 0,
should_trim: false,
handle: Arc::new(()),
}
});
match self.uploads.entry(cache.id) {
hash_map::Entry::Occupied(entry) => {
let upload = entry.into_mut();
if upload.version != cache.version
|| upload.group_version != group.version
|| upload.transformation != new_transformation
{
if !cache.text.is_empty() {
let _ = prepare(
device,
queue,
viewport,
encoder,
&mut upload.renderer,
&mut group.atlas,
&mut upload.buffer_cache,
&cache.text,
bounds,
new_transformation,
);
}
// Only trim if glyphs have changed
group.should_trim =
group.should_trim || upload.version != cache.version;
upload.text = Arc::downgrade(&cache.text);
upload.version = cache.version;
upload.group_version = group.version;
upload.transformation = new_transformation;
upload.buffer_cache.trim();
}
}
hash_map::Entry::Vacant(entry) => {
let mut renderer = cryoglyph::TextRenderer::new(
&mut group.atlas,
device,
wgpu::MultisampleState::default(),
None,
);
let mut buffer_cache = BufferCache::new();
if !cache.text.is_empty() {
let _ = prepare(
device,
queue,
viewport,
encoder,
&mut renderer,
&mut group.atlas,
&mut buffer_cache,
&cache.text,
bounds,
new_transformation,
);
}
let _ = entry.insert(Upload {
renderer,
buffer_cache,
transformation: new_transformation,
version: 0,
group_version: group.version,
text: Arc::downgrade(&cache.text),
_atlas: Arc::downgrade(&group.handle),
});
group.should_trim = cache.group.is_singleton();
log::debug!(
"New text upload: {} (total: {})",
cache.id.0,
self.uploads.len()
);
}
}
}
pub fn trim(&mut self) {
self.uploads
.retain(|_id, upload| upload.text.strong_count() > 0);
self.groups.retain(|id, group| {
let active_uploads = Arc::weak_count(&group.handle);
if active_uploads == 0 {
log::debug!("Dropping text atlas: {id:?}");
return false;
}
if group.should_trim {
log::trace!("Trimming text atlas: {id:?}");
group.atlas.trim();
group.should_trim = false;
// We only need to worry about glyph fighting
// when the atlas may be shared by multiple
// uploads.
if !id.is_singleton() {
log::debug!(
"Invalidating text atlas: {id:?} \
(uploads: {active_uploads})"
);
group.version += 1;
}
}
true
});
}
}
pub struct Viewport(cryoglyph::Viewport);
impl Viewport {
pub fn update(&mut self, queue: &wgpu::Queue, resolution: Size<u32>) {
self.0.update(
queue,
cryoglyph::Resolution {
width: resolution.width,
height: resolution.height,
},
);
}
}
#[derive(Clone)]
pub struct Pipeline {
format: wgpu::TextureFormat,
cache: cryoglyph::Cache,
atlas: Arc<RwLock<cryoglyph::TextAtlas>>,
}
impl Pipeline {
pub fn new(
device: &wgpu::Device,
queue: &wgpu::Queue,
format: wgpu::TextureFormat,
) -> Self {
let cache = cryoglyph::Cache::new(device);
let atlas = cryoglyph::TextAtlas::with_color_mode(
device, queue, &cache, format, COLOR_MODE,
);
Pipeline {
format,
cache,
atlas: Arc::new(RwLock::new(atlas)),
}
}
pub fn create_viewport(&self, device: &wgpu::Device) -> Viewport {
Viewport(cryoglyph::Viewport::new(device, &self.cache))
}
pub fn trim(&self) {
self.atlas.write().expect("Write text atlas").trim();
}
}
#[derive(Default)]
pub struct State {
renderers: Vec<cryoglyph::TextRenderer>,
prepare_layer: usize,
cache: BufferCache,
storage: Storage,
}
impl State {
pub fn new() -> Self {
Self::default()
}
pub fn prepare(
&mut self,
pipeline: &Pipeline,
device: &wgpu::Device,
queue: &wgpu::Queue,
viewport: &Viewport,
encoder: &mut wgpu::CommandEncoder,
batch: &Batch,
layer_bounds: Rectangle,
layer_transformation: Transformation,
) {
let mut atlas = pipeline.atlas.write().expect("Write to text atlas");
for item in batch {
match item {
Item::Group {
transformation,
text,
} => {
if self.renderers.len() <= self.prepare_layer {
self.renderers.push(cryoglyph::TextRenderer::new(
&mut atlas,
device,
wgpu::MultisampleState::default(),
None,
));
}
let renderer = &mut self.renderers[self.prepare_layer];
let result = prepare(
device,
queue,
&viewport.0,
encoder,
renderer,
&mut atlas,
&mut self.cache,
text,
layer_bounds * layer_transformation,
layer_transformation * *transformation,
);
match result {
Ok(()) => {
self.prepare_layer += 1;
}
Err(cryoglyph::PrepareError::AtlasFull) => {
// If the atlas cannot grow, then all bets are off.
// Instead of panicking, we will just pray that the result
// will be somewhat readable...
}
}
}
Item::Cached {
transformation,
cache,
} => {
self.storage.prepare(
device,
queue,
&viewport.0,
encoder,
pipeline.format,
&pipeline.cache,
cache,
layer_transformation * *transformation,
layer_bounds * layer_transformation,
);
}
}
}
}
pub fn render<'a>(
&'a self,
pipeline: &'a Pipeline,
viewport: &'a Viewport,
start: usize,
batch: &'a Batch,
bounds: Rectangle<u32>,
render_pass: &mut wgpu::RenderPass<'a>,
) -> usize {
let atlas = pipeline.atlas.read().expect("Read text atlas");
let mut layer_count = 0;
render_pass.set_scissor_rect(
bounds.x,
bounds.y,
bounds.width,
bounds.height,
);
for item in batch {
match item {
Item::Group { .. } => {
let renderer = &self.renderers[start + layer_count];
renderer
.render(&atlas, &viewport.0, render_pass)
.expect("Render text");
layer_count += 1;
}
Item::Cached { cache, .. } => {
if let Some((atlas, upload)) = self.storage.get(cache) {
upload
.renderer
.render(atlas, &viewport.0, render_pass)
.expect("Render cached text");
}
}
}
}
layer_count
}
pub fn trim(&mut self) {
self.cache.trim();
self.storage.trim();
self.prepare_layer = 0;
}
}
fn prepare(
device: &wgpu::Device,
queue: &wgpu::Queue,
viewport: &cryoglyph::Viewport,
encoder: &mut wgpu::CommandEncoder,
renderer: &mut cryoglyph::TextRenderer,
atlas: &mut cryoglyph::TextAtlas,
buffer_cache: &mut BufferCache,
sections: &[Text],
layer_bounds: Rectangle,
layer_transformation: Transformation,
) -> Result<(), cryoglyph::PrepareError> {
let mut font_system = font_system().write().expect("Write font system");
let font_system = font_system.raw();
enum Allocation {
Paragraph(Paragraph),
Editor(Editor),
Cache(text_cache::KeyHash),
Raw(Arc<cryoglyph::Buffer>),
}
let allocations: Vec<_> = sections
.iter()
.map(|section| match section {
Text::Paragraph { paragraph, .. } => {
paragraph.upgrade().map(Allocation::Paragraph)
}
Text::Editor { editor, .. } => {
editor.upgrade().map(Allocation::Editor)
}
Text::Cached {
content,
bounds,
size,
line_height,
font,
shaping,
align_x,
..
} => {
let (key, _) = buffer_cache.allocate(
font_system,
text_cache::Key {
content,
size: f32::from(*size),
line_height: f32::from(*line_height),
font: *font,
align_x: *align_x,
bounds: Size {
width: bounds.width,
height: bounds.height,
},
shaping: *shaping,
},
);
Some(Allocation::Cache(key))
}
Text::Raw { raw, .. } => raw.buffer.upgrade().map(Allocation::Raw),
})
.collect();
let text_areas = sections.iter().zip(allocations.iter()).filter_map(
|(section, allocation)| {
let (buffer, position, color, clip_bounds, transformation) =
match section {
Text::Paragraph {
position,
color,
clip_bounds,
transformation,
..
} => {
let Some(Allocation::Paragraph(paragraph)) = allocation
else {
return None;
};
(
paragraph.buffer(),
*position,
*color,
*clip_bounds,
*transformation,
)
}
Text::Editor {
position,
color,
clip_bounds,
transformation,
..
} => {
let Some(Allocation::Editor(editor)) = allocation
else {
return None;
};
(
editor.buffer(),
*position,
*color,
*clip_bounds,
*transformation,
)
}
Text::Cached {
bounds,
align_x,
align_y,
color,
clip_bounds,
..
} => {
let Some(Allocation::Cache(key)) = allocation else {
return None;
};
let entry =
buffer_cache.get(key).expect("Get cached buffer");
let mut position = bounds.position();
position.x = match align_x {
Alignment::Default
| Alignment::Left
| Alignment::Justified => position.x,
Alignment::Center => {
position.x - entry.min_bounds.width / 2.0
}
Alignment::Right => {
position.x - entry.min_bounds.width
}
};
position.y = match align_y {
alignment::Vertical::Top => position.y,
alignment::Vertical::Center => {
position.y - entry.min_bounds.height / 2.0
}
alignment::Vertical::Bottom => {
position.y - entry.min_bounds.height
}
};
(
&entry.buffer,
position,
*color,
*clip_bounds,
Transformation::IDENTITY,
)
}
Text::Raw {
raw,
transformation,
} => {
let Some(Allocation::Raw(buffer)) = allocation else {
return None;
};
(
buffer.as_ref(),
raw.position,
raw.color,
raw.clip_bounds,
*transformation,
)
}
};
let position = position * transformation * layer_transformation;
let clip_bounds = layer_bounds.intersection(
&(clip_bounds * transformation * layer_transformation),
)?;
Some(cryoglyph::TextArea {
buffer,
left: position.x,
top: position.y,
scale: transformation.scale_factor()
* layer_transformation.scale_factor(),
bounds: cryoglyph::TextBounds {
left: clip_bounds.x.round() as i32,
top: clip_bounds.y.round() as i32,
right: (clip_bounds.x + clip_bounds.width).round() as i32,
bottom: (clip_bounds.y + clip_bounds.height).round() as i32,
},
default_color: to_color(color),
})
},
);
renderer.prepare(
device,
queue,
encoder,
font_system,
atlas,
viewport,
text_areas,
&mut cryoglyph::SwashCache::new(),
)
}

View file

@ -0,0 +1,963 @@
//! Draw meshes of triangles.
mod msaa;
use crate::Buffer;
use crate::core::{Point, Rectangle, Size, Transformation, Vector};
use crate::graphics::Antialiasing;
use crate::graphics::mesh::{self, Mesh};
use rustc_hash::FxHashMap;
use std::collections::hash_map;
use std::sync::Weak;
const INITIAL_INDEX_COUNT: usize = 1_000;
const INITIAL_VERTEX_COUNT: usize = 1_000;
pub type Batch = Vec<Item>;
#[derive(Debug)]
pub enum Item {
Group {
transformation: Transformation,
meshes: Vec<Mesh>,
},
Cached {
transformation: Transformation,
cache: mesh::Cache,
},
}
#[derive(Debug)]
struct Upload {
layer: Layer,
transformation: Transformation,
version: usize,
batch: Weak<[Mesh]>,
}
#[derive(Debug, Default)]
pub struct Storage {
uploads: FxHashMap<mesh::Id, Upload>,
}
impl Storage {
pub fn new() -> Self {
Self::default()
}
fn get(&self, cache: &mesh::Cache) -> Option<&Upload> {
if cache.is_empty() {
return None;
}
self.uploads.get(&cache.id())
}
fn prepare(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
solid: &solid::Pipeline,
gradient: &gradient::Pipeline,
cache: &mesh::Cache,
new_transformation: Transformation,
) {
match self.uploads.entry(cache.id()) {
hash_map::Entry::Occupied(entry) => {
let upload = entry.into_mut();
if !cache.is_empty()
&& (upload.version != cache.version()
|| upload.transformation != new_transformation)
{
upload.layer.prepare(
device,
encoder,
belt,
solid,
gradient,
cache.batch(),
new_transformation,
);
upload.batch = cache.downgrade();
upload.version = cache.version();
upload.transformation = new_transformation;
}
}
hash_map::Entry::Vacant(entry) => {
let mut layer = Layer::new(device, solid, gradient);
layer.prepare(
device,
encoder,
belt,
solid,
gradient,
cache.batch(),
new_transformation,
);
let _ = entry.insert(Upload {
layer,
transformation: new_transformation,
version: 0,
batch: cache.downgrade(),
});
log::debug!(
"New mesh upload: {:?} (total: {})",
cache.id(),
self.uploads.len()
);
}
}
}
pub fn trim(&mut self) {
self.uploads
.retain(|_id, upload| upload.batch.strong_count() > 0);
}
}
#[derive(Debug, Clone)]
pub struct Pipeline {
msaa: Option<msaa::Pipeline>,
solid: solid::Pipeline,
gradient: gradient::Pipeline,
}
pub struct State {
msaa: Option<msaa::State>,
layers: Vec<Layer>,
prepare_layer: usize,
storage: Storage,
}
impl State {
pub fn new(device: &wgpu::Device, pipeline: &Pipeline) -> Self {
Self {
msaa: pipeline
.msaa
.as_ref()
.map(|pipeline| msaa::State::new(device, pipeline)),
layers: Vec::new(),
prepare_layer: 0,
storage: Storage::new(),
}
}
pub fn prepare(
&mut self,
pipeline: &Pipeline,
device: &wgpu::Device,
belt: &mut wgpu::util::StagingBelt,
encoder: &mut wgpu::CommandEncoder,
items: &[Item],
scale: Transformation,
target_size: Size<u32>,
) {
let projection = if let Some((state, pipeline)) =
self.msaa.as_mut().zip(pipeline.msaa.as_ref())
{
state.prepare(device, encoder, belt, pipeline, target_size) * scale
} else {
Transformation::orthographic(target_size.width, target_size.height)
* scale
};
for item in items {
match item {
Item::Group {
transformation,
meshes,
} => {
if self.layers.len() <= self.prepare_layer {
self.layers.push(Layer::new(
device,
&pipeline.solid,
&pipeline.gradient,
));
}
let layer = &mut self.layers[self.prepare_layer];
layer.prepare(
device,
encoder,
belt,
&pipeline.solid,
&pipeline.gradient,
meshes,
projection * *transformation,
);
self.prepare_layer += 1;
}
Item::Cached {
transformation,
cache,
} => {
self.storage.prepare(
device,
encoder,
belt,
&pipeline.solid,
&pipeline.gradient,
cache,
projection * *transformation,
);
}
}
}
}
pub fn render(
&mut self,
pipeline: &Pipeline,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
start: usize,
batch: &Batch,
bounds: Rectangle,
screen_transformation: Transformation,
) -> usize {
let mut layer_count = 0;
let items = batch.iter().filter_map(|item| match item {
Item::Group {
transformation,
meshes,
} => {
let layer = &self.layers[start + layer_count];
layer_count += 1;
Some((
layer,
meshes.as_slice(),
screen_transformation * *transformation,
))
}
Item::Cached {
transformation,
cache,
} => {
let upload = self.storage.get(cache)?;
Some((
&upload.layer,
cache.batch(),
screen_transformation * *transformation,
))
}
});
render(
encoder,
target,
self.msaa.as_ref().zip(pipeline.msaa.as_ref()),
&pipeline.solid,
&pipeline.gradient,
bounds,
items,
);
layer_count
}
pub fn trim(&mut self) {
self.storage.trim();
self.prepare_layer = 0;
}
}
impl Pipeline {
pub fn new(
device: &wgpu::Device,
format: wgpu::TextureFormat,
antialiasing: Option<Antialiasing>,
) -> Pipeline {
Pipeline {
msaa: antialiasing.map(|a| msaa::Pipeline::new(device, format, a)),
solid: solid::Pipeline::new(device, format, antialiasing),
gradient: gradient::Pipeline::new(device, format, antialiasing),
}
}
}
fn render<'a>(
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
mut msaa: Option<(&msaa::State, &msaa::Pipeline)>,
solid: &solid::Pipeline,
gradient: &gradient::Pipeline,
bounds: Rectangle,
group: impl Iterator<Item = (&'a Layer, &'a [Mesh], Transformation)>,
) {
{
let mut render_pass = if let Some((_state, pipeline)) = &mut msaa {
pipeline.render_pass(encoder)
} else {
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("iced_wgpu.triangle.render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
})
};
for (layer, meshes, transformation) in group {
layer.render(
solid,
gradient,
meshes,
bounds,
transformation,
&mut render_pass,
);
}
}
if let Some((state, pipeline)) = msaa {
state.render(pipeline, encoder, target);
}
}
#[derive(Debug)]
pub struct Layer {
index_buffer: Buffer<u32>,
solid: solid::Layer,
gradient: gradient::Layer,
}
impl Layer {
fn new(
device: &wgpu::Device,
solid: &solid::Pipeline,
gradient: &gradient::Pipeline,
) -> Self {
Self {
index_buffer: Buffer::new(
device,
"iced_wgpu.triangle.index_buffer",
INITIAL_INDEX_COUNT,
wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
),
solid: solid::Layer::new(device, &solid.constants_layout),
gradient: gradient::Layer::new(device, &gradient.constants_layout),
}
}
fn prepare(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
solid: &solid::Pipeline,
gradient: &gradient::Pipeline,
meshes: &[Mesh],
transformation: Transformation,
) {
// Count the total amount of vertices & indices we need to handle
let count = mesh::attribute_count_of(meshes);
// Then we ensure the current attribute buffers are big enough, resizing if necessary.
// We are not currently using the return value of these functions as we have no system in
// place to calculate mesh diff, or to know whether or not that would be more performant for
// the majority of use cases. Therefore we will write GPU data every frame (for now).
let _ = self.index_buffer.resize(device, count.indices);
let _ = self.solid.vertices.resize(device, count.solid_vertices);
let _ = self
.gradient
.vertices
.resize(device, count.gradient_vertices);
if self.solid.uniforms.resize(device, count.solids) {
self.solid.constants = solid::Layer::bind_group(
device,
&self.solid.uniforms.raw,
&solid.constants_layout,
);
}
if self.gradient.uniforms.resize(device, count.gradients) {
self.gradient.constants = gradient::Layer::bind_group(
device,
&self.gradient.uniforms.raw,
&gradient.constants_layout,
);
}
let mut solid_vertex_offset = 0;
let mut solid_uniform_offset = 0;
let mut gradient_vertex_offset = 0;
let mut gradient_uniform_offset = 0;
let mut index_offset = 0;
for mesh in meshes {
let clip_bounds = mesh.clip_bounds() * transformation;
let snap_distance = clip_bounds
.snap()
.map(|snapped_bounds| {
Point::new(snapped_bounds.x as f32, snapped_bounds.y as f32)
- clip_bounds.position()
})
.unwrap_or(Vector::ZERO);
let uniforms = Uniforms::new(
transformation
* mesh.transformation()
* Transformation::translate(
snap_distance.x,
snap_distance.y,
),
);
let indices = mesh.indices();
index_offset += self.index_buffer.write(
device,
encoder,
belt,
index_offset,
indices,
);
match mesh {
Mesh::Solid { buffers, .. } => {
solid_vertex_offset += self.solid.vertices.write(
device,
encoder,
belt,
solid_vertex_offset,
&buffers.vertices,
);
solid_uniform_offset += self.solid.uniforms.write(
device,
encoder,
belt,
solid_uniform_offset,
&[uniforms],
);
}
Mesh::Gradient { buffers, .. } => {
gradient_vertex_offset += self.gradient.vertices.write(
device,
encoder,
belt,
gradient_vertex_offset,
&buffers.vertices,
);
gradient_uniform_offset += self.gradient.uniforms.write(
device,
encoder,
belt,
gradient_uniform_offset,
&[uniforms],
);
}
}
}
}
fn render<'a>(
&'a self,
solid: &'a solid::Pipeline,
gradient: &'a gradient::Pipeline,
meshes: &[Mesh],
bounds: Rectangle,
transformation: Transformation,
render_pass: &mut wgpu::RenderPass<'a>,
) {
let mut num_solids = 0;
let mut num_gradients = 0;
let mut solid_offset = 0;
let mut gradient_offset = 0;
let mut index_offset = 0;
let mut last_is_solid = None;
for mesh in meshes {
let Some(clip_bounds) = bounds
.intersection(&(mesh.clip_bounds() * transformation))
.and_then(Rectangle::snap)
else {
match mesh {
Mesh::Solid { buffers, .. } => {
solid_offset += buffers.vertices.len();
num_solids += 1;
}
Mesh::Gradient { buffers, .. } => {
gradient_offset += buffers.vertices.len();
num_gradients += 1;
}
}
continue;
};
render_pass.set_scissor_rect(
clip_bounds.x,
clip_bounds.y,
clip_bounds.width,
clip_bounds.height,
);
match mesh {
Mesh::Solid { buffers, .. } => {
if !last_is_solid.unwrap_or(false) {
render_pass.set_pipeline(&solid.pipeline);
last_is_solid = Some(true);
}
render_pass.set_bind_group(
0,
&self.solid.constants,
&[(num_solids * std::mem::size_of::<Uniforms>())
as u32],
);
render_pass.set_vertex_buffer(
0,
self.solid.vertices.range(
solid_offset,
solid_offset + buffers.vertices.len(),
),
);
num_solids += 1;
solid_offset += buffers.vertices.len();
}
Mesh::Gradient { buffers, .. } => {
if last_is_solid.unwrap_or(true) {
render_pass.set_pipeline(&gradient.pipeline);
last_is_solid = Some(false);
}
render_pass.set_bind_group(
0,
&self.gradient.constants,
&[(num_gradients * std::mem::size_of::<Uniforms>())
as u32],
);
render_pass.set_vertex_buffer(
0,
self.gradient.vertices.range(
gradient_offset,
gradient_offset + buffers.vertices.len(),
),
);
num_gradients += 1;
gradient_offset += buffers.vertices.len();
}
};
render_pass.set_index_buffer(
self.index_buffer
.range(index_offset, index_offset + mesh.indices().len()),
wgpu::IndexFormat::Uint32,
);
render_pass.draw_indexed(0..mesh.indices().len() as u32, 0, 0..1);
index_offset += mesh.indices().len();
}
}
}
fn fragment_target(
texture_format: wgpu::TextureFormat,
) -> wgpu::ColorTargetState {
wgpu::ColorTargetState {
format: texture_format,
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
}
}
fn primitive_state() -> wgpu::PrimitiveState {
wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
front_face: wgpu::FrontFace::Cw,
..Default::default()
}
}
fn multisample_state(
antialiasing: Option<Antialiasing>,
) -> wgpu::MultisampleState {
wgpu::MultisampleState {
count: antialiasing.map(Antialiasing::sample_count).unwrap_or(1),
mask: !0,
alpha_to_coverage_enabled: false,
}
}
#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub struct Uniforms {
transform: [f32; 16],
/// Uniform values must be 256-aligned;
/// see: [`wgpu::Limits`] `min_uniform_buffer_offset_alignment`.
_padding: [f32; 48],
}
impl Uniforms {
pub fn new(transform: Transformation) -> Self {
Self {
transform: transform.into(),
_padding: [0.0; 48],
}
}
pub fn entry() -> wgpu::BindGroupLayoutEntry {
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: true,
min_binding_size: wgpu::BufferSize::new(
std::mem::size_of::<Self>() as u64,
),
},
count: None,
}
}
pub fn min_size() -> Option<wgpu::BufferSize> {
wgpu::BufferSize::new(std::mem::size_of::<Self>() as u64)
}
}
mod solid {
use crate::Buffer;
use crate::graphics::Antialiasing;
use crate::graphics::mesh;
use crate::triangle;
#[derive(Debug, Clone)]
pub struct Pipeline {
pub pipeline: wgpu::RenderPipeline,
pub constants_layout: wgpu::BindGroupLayout,
}
#[derive(Debug)]
pub struct Layer {
pub vertices: Buffer<mesh::SolidVertex2D>,
pub uniforms: Buffer<triangle::Uniforms>,
pub constants: wgpu::BindGroup,
}
impl Layer {
pub fn new(
device: &wgpu::Device,
constants_layout: &wgpu::BindGroupLayout,
) -> Self {
let vertices = Buffer::new(
device,
"iced_wgpu.triangle.solid.vertex_buffer",
triangle::INITIAL_VERTEX_COUNT,
wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
);
let uniforms = Buffer::new(
device,
"iced_wgpu.triangle.solid.uniforms",
1,
wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
);
let constants =
Self::bind_group(device, &uniforms.raw, constants_layout);
Self {
vertices,
uniforms,
constants,
}
}
pub fn bind_group(
device: &wgpu::Device,
buffer: &wgpu::Buffer,
layout: &wgpu::BindGroupLayout,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu.triangle.solid.bind_group"),
layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(
wgpu::BufferBinding {
buffer,
offset: 0,
size: triangle::Uniforms::min_size(),
},
),
}],
})
}
}
impl Pipeline {
pub fn new(
device: &wgpu::Device,
format: wgpu::TextureFormat,
antialiasing: Option<Antialiasing>,
) -> Self {
let constants_layout = device.create_bind_group_layout(
&wgpu::BindGroupLayoutDescriptor {
label: Some("iced_wgpu.triangle.solid.bind_group_layout"),
entries: &[triangle::Uniforms::entry()],
},
);
let layout = device.create_pipeline_layout(
&wgpu::PipelineLayoutDescriptor {
label: Some("iced_wgpu.triangle.solid.pipeline_layout"),
bind_group_layouts: &[&constants_layout],
push_constant_ranges: &[],
},
);
let shader =
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("iced_wgpu.triangle.solid.shader"),
source: wgpu::ShaderSource::Wgsl(
std::borrow::Cow::Borrowed(concat!(
include_str!("shader/triangle.wgsl"),
"\n",
include_str!("shader/triangle/solid.wgsl"),
"\n",
include_str!("shader/color.wgsl"),
)),
),
});
let pipeline =
device.create_render_pipeline(
&wgpu::RenderPipelineDescriptor {
label: Some("iced_wgpu::triangle::solid pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("solid_vs_main"),
buffers: &[wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<
mesh::SolidVertex2D,
>(
)
as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &wgpu::vertex_attr_array!(
// Position
0 => Float32x2,
// Color
1 => Float32x4,
),
}],
compilation_options:
wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("solid_fs_main"),
targets: &[Some(triangle::fragment_target(format))],
compilation_options:
wgpu::PipelineCompilationOptions::default(),
}),
primitive: triangle::primitive_state(),
depth_stencil: None,
multisample: triangle::multisample_state(antialiasing),
multiview: None,
cache: None,
},
);
Self {
pipeline,
constants_layout,
}
}
}
}
mod gradient {
use crate::Buffer;
use crate::graphics::Antialiasing;
use crate::graphics::mesh;
use crate::triangle;
#[derive(Debug, Clone)]
pub struct Pipeline {
pub pipeline: wgpu::RenderPipeline,
pub constants_layout: wgpu::BindGroupLayout,
}
#[derive(Debug)]
pub struct Layer {
pub vertices: Buffer<mesh::GradientVertex2D>,
pub uniforms: Buffer<triangle::Uniforms>,
pub constants: wgpu::BindGroup,
}
impl Layer {
pub fn new(
device: &wgpu::Device,
constants_layout: &wgpu::BindGroupLayout,
) -> Self {
let vertices = Buffer::new(
device,
"iced_wgpu.triangle.gradient.vertex_buffer",
triangle::INITIAL_VERTEX_COUNT,
wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
);
let uniforms = Buffer::new(
device,
"iced_wgpu.triangle.gradient.uniforms",
1,
wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
);
let constants =
Self::bind_group(device, &uniforms.raw, constants_layout);
Self {
vertices,
uniforms,
constants,
}
}
pub fn bind_group(
device: &wgpu::Device,
uniform_buffer: &wgpu::Buffer,
layout: &wgpu::BindGroupLayout,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu.triangle.gradient.bind_group"),
layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Buffer(
wgpu::BufferBinding {
buffer: uniform_buffer,
offset: 0,
size: triangle::Uniforms::min_size(),
},
),
}],
})
}
}
impl Pipeline {
pub fn new(
device: &wgpu::Device,
format: wgpu::TextureFormat,
antialiasing: Option<Antialiasing>,
) -> Self {
let constants_layout = device.create_bind_group_layout(
&wgpu::BindGroupLayoutDescriptor {
label: Some(
"iced_wgpu.triangle.gradient.bind_group_layout",
),
entries: &[triangle::Uniforms::entry()],
},
);
let layout = device.create_pipeline_layout(
&wgpu::PipelineLayoutDescriptor {
label: Some("iced_wgpu.triangle.gradient.pipeline_layout"),
bind_group_layouts: &[&constants_layout],
push_constant_ranges: &[],
},
);
let shader =
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("iced_wgpu.triangle.gradient.shader"),
source: wgpu::ShaderSource::Wgsl(
std::borrow::Cow::Borrowed(concat!(
include_str!("shader/triangle.wgsl"),
"\n",
include_str!("shader/triangle/gradient.wgsl"),
"\n",
include_str!("shader/color.wgsl"),
"\n",
include_str!("shader/color/linear_rgb.wgsl")
)),
),
});
let pipeline = device.create_render_pipeline(
&wgpu::RenderPipelineDescriptor {
label: Some("iced_wgpu.triangle.gradient.pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("gradient_vs_main"),
buffers: &[wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<
mesh::GradientVertex2D,
>()
as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &wgpu::vertex_attr_array!(
// Position
0 => Float32x2,
// Colors 1-2
1 => Uint32x4,
// Colors 3-4
2 => Uint32x4,
// Colors 5-6
3 => Uint32x4,
// Colors 7-8
4 => Uint32x4,
// Offsets
5 => Uint32x4,
// Direction
6 => Float32x4
),
}],
compilation_options:
wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("gradient_fs_main"),
targets: &[Some(triangle::fragment_target(format))],
compilation_options:
wgpu::PipelineCompilationOptions::default(),
}),
primitive: triangle::primitive_state(),
depth_stencil: None,
multisample: triangle::multisample_state(antialiasing),
multiview: None,
cache: None,
},
);
Self {
pipeline,
constants_layout,
}
}
}
}

View file

@ -0,0 +1,371 @@
use crate::core::{Size, Transformation};
use crate::graphics;
use std::num::NonZeroU64;
use std::sync::{Arc, RwLock};
#[derive(Debug, Clone)]
pub struct Pipeline {
format: wgpu::TextureFormat,
sampler: wgpu::Sampler,
raw: wgpu::RenderPipeline,
constant_layout: wgpu::BindGroupLayout,
texture_layout: wgpu::BindGroupLayout,
sample_count: u32,
targets: Arc<RwLock<Option<Targets>>>,
}
impl Pipeline {
pub fn new(
device: &wgpu::Device,
format: wgpu::TextureFormat,
antialiasing: graphics::Antialiasing,
) -> Pipeline {
let sampler =
device.create_sampler(&wgpu::SamplerDescriptor::default());
let constant_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("iced_wgpu::triangle:msaa uniforms layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(
wgpu::SamplerBindingType::NonFiltering,
),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let texture_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("iced_wgpu::triangle::msaa texture layout"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float {
filterable: false,
},
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
}],
});
let layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("iced_wgpu::triangle::msaa pipeline layout"),
push_constant_ranges: &[],
bind_group_layouts: &[&constant_layout, &texture_layout],
});
let shader =
device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("iced_wgpu triangle blit_shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
include_str!("../shader/blit.wgsl"),
)),
});
let pipeline =
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("iced_wgpu::triangle::msaa pipeline"),
layout: Some(&layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options:
wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format,
blend: Some(
wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING,
),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options:
wgpu::PipelineCompilationOptions::default(),
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
front_face: wgpu::FrontFace::Cw,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None,
cache: None,
});
Self {
format,
sampler,
raw: pipeline,
constant_layout,
texture_layout,
sample_count: antialiasing.sample_count(),
targets: Arc::new(RwLock::new(None)),
}
}
fn targets(
&self,
device: &wgpu::Device,
region_size: Size<u32>,
) -> Targets {
let mut targets = self.targets.write().expect("Write MSAA targets");
match targets.as_mut() {
Some(targets)
if region_size.width <= targets.size.width
&& region_size.height <= targets.size.height => {}
_ => {
*targets = Some(Targets::new(
device,
self.format,
&self.texture_layout,
self.sample_count,
region_size,
));
}
}
targets.as_ref().unwrap().clone()
}
pub fn render_pass<'a>(
&self,
encoder: &'a mut wgpu::CommandEncoder,
) -> wgpu::RenderPass<'a> {
let targets = self.targets.read().expect("Read MSAA targets");
let targets = targets.as_ref().unwrap();
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("iced_wgpu.triangle.render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &targets.attachment,
depth_slice: None,
resolve_target: Some(&targets.resolve),
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
})
}
}
#[derive(Debug, Clone)]
struct Targets {
attachment: wgpu::TextureView,
resolve: wgpu::TextureView,
bind_group: wgpu::BindGroup,
size: Size<u32>,
}
impl Targets {
pub fn new(
device: &wgpu::Device,
format: wgpu::TextureFormat,
texture_layout: &wgpu::BindGroupLayout,
sample_count: u32,
size: Size<u32>,
) -> Targets {
let extent = wgpu::Extent3d {
width: size.width,
height: size.height,
depth_or_array_layers: 1,
};
let attachment = device.create_texture(&wgpu::TextureDescriptor {
label: Some("iced_wgpu::triangle::msaa attachment"),
size: extent,
mip_level_count: 1,
sample_count,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});
let resolve = device.create_texture(&wgpu::TextureDescriptor {
label: Some("iced_wgpu::triangle::msaa resolve target"),
size: extent,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let attachment =
attachment.create_view(&wgpu::TextureViewDescriptor::default());
let resolve =
resolve.create_view(&wgpu::TextureViewDescriptor::default());
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu::triangle::msaa texture bind group"),
layout: texture_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&resolve),
}],
});
Targets {
attachment,
resolve,
bind_group,
size,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
struct Ratio {
u: f32,
v: f32,
// Padding field for 16-byte alignment.
// See https://docs.rs/wgpu/latest/wgpu/struct.DownlevelFlags.html#associatedconstant.BUFFER_BINDINGS_NOT_16_BYTE_ALIGNED
_padding: [f32; 2],
}
pub struct State {
ratio: wgpu::Buffer,
constants: wgpu::BindGroup,
last_ratio: Option<Ratio>,
}
impl State {
pub fn new(device: &wgpu::Device, pipeline: &Pipeline) -> Self {
let ratio = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("iced_wgpu::triangle::msaa ratio"),
size: std::mem::size_of::<Ratio>() as u64,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM,
mapped_at_creation: false,
});
let constants = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iced_wgpu::triangle::msaa uniforms bind group"),
layout: &pipeline.constant_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::Sampler(&pipeline.sampler),
},
wgpu::BindGroupEntry {
binding: 1,
resource: ratio.as_entire_binding(),
},
],
});
Self {
ratio,
constants,
last_ratio: None,
}
}
pub fn prepare(
&mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
belt: &mut wgpu::util::StagingBelt,
pipeline: &Pipeline,
region_size: Size<u32>,
) -> Transformation {
let targets = pipeline.targets(device, region_size);
let ratio = Ratio {
u: region_size.width as f32 / targets.size.width as f32,
v: region_size.height as f32 / targets.size.height as f32,
_padding: [0.0; 2],
};
if Some(ratio) != self.last_ratio {
belt.write_buffer(
encoder,
&self.ratio,
0,
NonZeroU64::new(std::mem::size_of::<Ratio>() as u64)
.expect("non-empty ratio"),
device,
)
.copy_from_slice(bytemuck::bytes_of(&ratio));
self.last_ratio = Some(ratio);
}
Transformation::orthographic(targets.size.width, targets.size.height)
}
pub fn render(
&self,
pipeline: &Pipeline,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
) {
let mut render_pass =
encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("iced_wgpu::triangle::msaa render pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: target,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
});
render_pass.set_pipeline(&pipeline.raw);
render_pass.set_bind_group(0, &self.constants, &[]);
render_pass.set_bind_group(
1,
&pipeline
.targets
.read()
.expect("Read MSAA targets")
.as_ref()
.unwrap()
.bind_group,
&[],
);
render_pass.draw(0..6, 0..1);
}
}

View file

@ -0,0 +1,5 @@
//! Display rendering results on windows.
pub mod compositor;
pub use compositor::Compositor;
pub use wgpu::Surface;

View file

@ -0,0 +1,358 @@
//! Connect a window with a renderer.
use crate::core::Color;
use crate::graphics::color;
use crate::graphics::compositor;
use crate::graphics::error;
use crate::graphics::{self, Shell, Viewport};
use crate::settings::{self, Settings};
use crate::{Engine, Renderer};
/// A window graphics backend for iced powered by `wgpu`.
pub struct Compositor {
instance: wgpu::Instance,
adapter: wgpu::Adapter,
format: wgpu::TextureFormat,
alpha_mode: wgpu::CompositeAlphaMode,
engine: Engine,
settings: Settings,
}
/// A compositor error.
#[derive(Debug, Clone, thiserror::Error)]
pub enum Error {
/// The surface creation failed.
#[error("the surface creation failed: {0}")]
SurfaceCreationFailed(#[from] wgpu::CreateSurfaceError),
/// The surface is not compatible.
#[error("the surface is not compatible")]
IncompatibleSurface,
/// No adapter was found for the options requested.
#[error("no adapter was found for the options requested: {0:?}")]
NoAdapterFound(String),
/// No device request succeeded.
#[error("no device request succeeded: {0:?}")]
RequestDeviceFailed(Vec<(wgpu::Limits, wgpu::RequestDeviceError)>),
}
impl From<Error> for graphics::Error {
fn from(error: Error) -> Self {
Self::GraphicsAdapterNotFound {
backend: "wgpu",
reason: error::Reason::RequestFailed(error.to_string()),
}
}
}
impl Compositor {
/// Requests a new [`Compositor`] with the given [`Settings`].
///
/// Returns `None` if no compatible graphics adapter could be found.
pub async fn request<W: compositor::Window>(
settings: Settings,
compatible_window: Option<W>,
shell: Shell,
) -> Result<Self, Error> {
let instance = wgpu::util::new_instance_with_webgpu_detection(&wgpu::InstanceDescriptor {
backends: settings.backends,
flags: if cfg!(feature = "strict-assertions") {
wgpu::InstanceFlags::debugging()
} else {
wgpu::InstanceFlags::empty()
},
..Default::default()
})
.await;
log::info!("{settings:#?}");
#[cfg(not(target_arch = "wasm32"))]
if log::max_level() >= log::LevelFilter::Info {
let available_adapters: Vec<_> = instance
.enumerate_adapters(settings.backends)
.iter()
.map(wgpu::Adapter::get_info)
.collect();
log::info!("Available adapters: {available_adapters:#?}");
}
#[allow(unsafe_code)]
let compatible_surface =
compatible_window.and_then(|window| instance.create_surface(window).ok());
let adapter_options = wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::from_env()
.unwrap_or(wgpu::PowerPreference::HighPerformance),
compatible_surface: compatible_surface.as_ref(),
force_fallback_adapter: false,
};
let adapter = instance
.request_adapter(&adapter_options)
.await
.map_err(|_error| Error::NoAdapterFound(format!("{adapter_options:?}")))?;
log::info!("Selected: {:#?}", adapter.get_info());
let (format, alpha_mode) = compatible_surface
.as_ref()
.and_then(|surface| {
let capabilities = surface.get_capabilities(&adapter);
let formats = capabilities.formats.iter().copied();
log::info!("Available formats: {formats:#?}");
let mut formats =
formats.filter(|format| format.required_features() == wgpu::Features::empty());
let format = if color::GAMMA_CORRECTION {
formats.find(wgpu::TextureFormat::is_srgb)
} else {
formats.find(|format| !wgpu::TextureFormat::is_srgb(format))
};
let format = format.or_else(|| {
log::warn!("No format found!");
capabilities.formats.first().copied()
});
let alpha_modes = capabilities.alpha_modes;
log::info!("Available alpha modes: {alpha_modes:#?}");
let preferred_alpha =
if alpha_modes.contains(&wgpu::CompositeAlphaMode::PostMultiplied) {
wgpu::CompositeAlphaMode::PostMultiplied
} else if alpha_modes.contains(&wgpu::CompositeAlphaMode::PreMultiplied) {
wgpu::CompositeAlphaMode::PreMultiplied
} else {
wgpu::CompositeAlphaMode::Auto
};
format.zip(Some(preferred_alpha))
})
.ok_or(Error::IncompatibleSurface)?;
log::info!("Selected format: {format:?} with alpha mode: {alpha_mode:?}");
#[cfg(target_arch = "wasm32")]
let limits = if adapter.get_info().backend == wgpu::Backend::BrowserWebGpu {
vec![
wgpu::Limits::default().using_resolution(adapter.limits()),
wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits()),
]
} else {
vec![wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits())]
};
#[cfg(not(target_arch = "wasm32"))]
let limits = vec![wgpu::Limits::default(), wgpu::Limits::downlevel_defaults()];
let limits = limits.into_iter().map(|limits| wgpu::Limits {
max_bind_groups: 2,
max_non_sampler_bindings: 2048,
..limits
});
let mut errors = Vec::new();
for required_limits in limits {
let result = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("iced_wgpu::window::compositor device descriptor"),
required_features: wgpu::Features::empty(),
required_limits: required_limits.clone(),
memory_hints: wgpu::MemoryHints::MemoryUsage,
trace: wgpu::Trace::Off,
experimental_features: wgpu::ExperimentalFeatures::disabled(),
})
.await;
match result {
Ok((device, queue)) => {
let engine = Engine::new(
&adapter,
device,
queue,
format,
settings.antialiasing,
shell,
);
return Ok(Compositor {
instance,
adapter,
format,
alpha_mode,
engine,
settings,
});
}
Err(error) => {
errors.push((required_limits, error));
}
}
}
Err(Error::RequestDeviceFailed(errors))
}
}
/// Creates a [`Compositor`] with the given [`Settings`] and window.
pub async fn new<W: compositor::Window>(
settings: Settings,
compatible_window: W,
shell: Shell,
) -> Result<Compositor, Error> {
Compositor::request(settings, Some(compatible_window), shell).await
}
/// Presents the given primitives with the given [`Compositor`].
pub fn present(
renderer: &mut Renderer,
surface: &mut wgpu::Surface<'static>,
viewport: &Viewport,
background_color: Color,
on_pre_present: impl FnOnce(),
) -> Result<(), compositor::SurfaceError> {
match surface.get_current_texture() {
Ok(frame) => {
let view = &frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let _submission = renderer.present(
Some(background_color),
frame.texture.format(),
view,
viewport,
);
// Present the frame
on_pre_present();
frame.present();
Ok(())
}
Err(error) => match error {
wgpu::SurfaceError::Timeout => Err(compositor::SurfaceError::Timeout),
wgpu::SurfaceError::Outdated => Err(compositor::SurfaceError::Outdated),
wgpu::SurfaceError::Lost => Err(compositor::SurfaceError::Lost),
wgpu::SurfaceError::OutOfMemory => Err(compositor::SurfaceError::OutOfMemory),
wgpu::SurfaceError::Other => Err(compositor::SurfaceError::Other),
},
}
}
impl graphics::Compositor for Compositor {
type Renderer = Renderer;
type Surface = wgpu::Surface<'static>;
async fn with_backend(
settings: graphics::Settings,
_display: impl compositor::Display,
compatible_window: impl compositor::Window,
shell: Shell,
backend: Option<&str>,
) -> Result<Self, graphics::Error> {
match backend {
None | Some("wgpu") => {
let mut settings = Settings::from(settings);
if let Some(backends) = wgpu::Backends::from_env() {
settings.backends = backends;
}
if let Some(present_mode) = settings::present_mode_from_env() {
settings.present_mode = present_mode;
}
Ok(new(settings, compatible_window, shell).await?)
}
Some(backend) => Err(graphics::Error::GraphicsAdapterNotFound {
backend: "wgpu",
reason: error::Reason::DidNotMatch {
preferred_backend: backend.to_owned(),
},
}),
}
}
fn create_renderer(&self) -> Self::Renderer {
Renderer::new(
self.engine.clone(),
self.settings.default_font,
self.settings.default_text_size,
)
}
fn create_surface<W: compositor::Window>(
&mut self,
window: W,
width: u32,
height: u32,
) -> Self::Surface {
let mut surface = self
.instance
.create_surface(window)
.expect("Create surface");
if width > 0 && height > 0 {
self.configure_surface(&mut surface, width, height);
}
surface
}
fn configure_surface(&mut self, surface: &mut Self::Surface, width: u32, height: u32) {
surface.configure(
&self.engine.device,
&wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: self.format,
present_mode: self.settings.present_mode,
width,
height,
alpha_mode: self.alpha_mode,
view_formats: vec![],
desired_maximum_frame_latency: 1,
},
);
}
fn information(&self) -> compositor::Information {
let information = self.adapter.get_info();
compositor::Information {
adapter: information.name,
backend: format!("{:?}", information.backend),
}
}
fn present(
&mut self,
renderer: &mut Self::Renderer,
surface: &mut Self::Surface,
viewport: &Viewport,
background_color: Color,
on_pre_present: impl FnOnce(),
) -> Result<(), compositor::SurfaceError> {
present(
renderer,
surface,
viewport,
background_color,
on_pre_present,
)
}
fn screenshot(
&mut self,
renderer: &mut Self::Renderer,
viewport: &Viewport,
background_color: Color,
) -> Vec<u8> {
renderer.screenshot(viewport, background_color)
}
}

View file

@ -0,0 +1,15 @@
[package]
name = "ocs_web_worker"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
acadrust = { version = "0.4", features = ["serde"] }
bincode = "1.3"
getrandom = { version = "0.3", features = ["wasm_js"] }
js-sys = "0.3"
wasm-bindgen = "0.2"

View file

@ -0,0 +1,32 @@
use std::io::Cursor;
use acadrust::io::dwg::DwgReader;
use acadrust::DxfReader;
use js_sys::Uint8Array;
use wasm_bindgen::prelude::*;
/// Parse DWG/DXF on a dedicated browser worker and return a compact serialized
/// document. The main wasm instance only deserializes and installs it, so the
/// expensive bit/handle/object decode never occupies the browser UI thread.
#[wasm_bindgen]
pub fn parse_document(name: String, bytes: Uint8Array) -> Result<Uint8Array, JsValue> {
let bytes = bytes.to_vec();
let ext = name.rsplit('.').next().unwrap_or_default().to_lowercase();
let document = match ext.as_str() {
"dwg" => DwgReader::from_stream(Cursor::new(bytes))
.read()
.map_err(|error| JsValue::from_str(&error.to_string()))?,
"dxf" => DxfReader::from_reader(Cursor::new(bytes))
.map_err(|error| JsValue::from_str(&error.to_string()))?
.read()
.map_err(|error| JsValue::from_str(&error.to_string()))?,
_ => {
return Err(JsValue::from_str(&format!(
"Unsupported file format: .{ext}"
)))
}
};
let encoded =
bincode::serialize(&document).map_err(|error| JsValue::from_str(&error.to_string()))?;
Ok(Uint8Array::from(encoded.as_slice()))
}

View file

@ -45,6 +45,8 @@
<!-- Per-script Noto subsets, fetched lazily at runtime (one alphabet per
file) so CAD text renders non-Latin scripts on the web. (#141) -->
<link data-trunk rel="copy-dir" href="web/fonts" />
<link data-trunk rel="copy-file" href="web/ocs-parse-worker.js" />
<link data-trunk rel="copy-dir" href="web/worker_pkg" />
</head>
<body>
<div id="loading">

View file

@ -0,0 +1,10 @@
#!/bin/sh
set -eu
cargo build --release --target wasm32-unknown-unknown --package ocs_web_worker
mkdir -p web/worker_pkg
wasm-bindgen \
--target web \
--out-dir web/worker_pkg \
--out-name ocs_web_worker \
target/wasm32-unknown-unknown/release/ocs_web_worker.wasm

View file

@ -1052,9 +1052,10 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.push_undo_snapshot(i, "GROUP");
let undo = self.begin_group_undo(i, "GROUP");
self.tabs[i].scene.create_group(name.clone(), handles);
self.tabs[i].dirty = true;
self.commit_group_undo(i, undo);
self.command_line
.push_info(&format!("Group \"{}\" created.", name));
}
@ -1062,9 +1063,10 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.push_undo_snapshot(i, "UNGROUP");
let undo = self.begin_group_undo(i, "UNGROUP");
let count = self.tabs[i].scene.delete_groups_containing(&handles);
self.tabs[i].dirty = true;
self.commit_group_undo(i, undo);
if count > 0 {
self.command_line
.push_info(&format!("{} group(s) dissolved.", count));

View file

@ -817,10 +817,15 @@ impl OpenCADStudio {
self.command_line
.push_error(&format!("DIMSTYLE: '{}' already exists.", name));
} else {
let undo = self.begin_dim_style_undo(
i,
"DIMSTYLE NEW",
std::slice::from_ref(&name),
);
let style = DimStyle::new(&name);
let _ = self.tabs[i].scene.document.dim_styles.add(style);
self.push_undo_snapshot(i, "DIMSTYLE NEW");
self.tabs[i].dirty = true;
self.commit_dim_style_undo(i, undo);
self.command_line
.push_output(&format!("DIMSTYLE: '{}' created.", name));
}
@ -832,6 +837,11 @@ impl OpenCADStudio {
let prop = parts.get(2).map(|s| s.to_lowercase()).unwrap_or_default();
let val_str = parts.get(3).map(|s| s.trim()).unwrap_or("");
if let Ok(val) = val_str.parse::<f64>() {
let undo = self.begin_dim_style_undo(
i,
"DIMSTYLE SET",
std::slice::from_ref(&style_name),
);
if let Some(ds) =
self.tabs[i].scene.document.dim_styles.get_mut(&style_name)
{
@ -882,8 +892,10 @@ impl OpenCADStudio {
return Some(Task::none());
}
}
self.push_undo_snapshot(i, "DIMSTYLE SET");
self.tabs[i].dirty = true;
self.tabs[i].scene
.invalidate_dim_style_dependencies(&style_name);
self.commit_dim_style_undo(i, undo);
self.command_line.push_output(&format!(
"DIMSTYLE: '{style_name}'.{prop} = {val:.3}"
));
@ -1121,10 +1133,15 @@ impl OpenCADStudio {
self.command_line
.push_error(&format!("{prefix}: style '{name}' already exists."));
} else {
let undo = self.begin_text_style_undo(
i,
"STYLE NEW",
std::slice::from_ref(&name),
);
let style = acadrust::tables::TextStyle::new(&name);
let _ = self.tabs[i].scene.document.text_styles.add(style);
self.push_undo_snapshot(i, "STYLE NEW");
self.tabs[i].dirty = true;
self.commit_text_style_undo(i, undo);
self.command_line
.push_output(&format!("{prefix}: style '{name}' created."));
}
@ -1136,12 +1153,21 @@ impl OpenCADStudio {
if style_name.is_empty() || font.is_empty() {
self.command_line
.push_error(&format!("Usage: {prefix} FONT <style> <font_file>"));
} else if let Some(s) =
} else {
let undo = self.begin_text_style_undo(
i,
"STYLE FONT",
std::slice::from_ref(&style_name),
);
if let Some(style) =
self.tabs[i].scene.document.text_styles.get_mut(&style_name)
{
s.font_file = font.clone();
self.push_undo_snapshot(i, "STYLE FONT");
style.font_file = font.clone();
self.tabs[i].dirty = true;
self.tabs[i]
.scene
.invalidate_text_style_dependencies(&style_name);
self.commit_text_style_undo(i, undo);
self.command_line.push_output(&format!(
"{prefix}: '{style_name}' font set to '{font}'."
));
@ -1150,17 +1176,26 @@ impl OpenCADStudio {
.push_error(&format!("{prefix}: style '{style_name}' not found."));
}
}
}
"WIDTH" | "W" => {
// STYLE WIDTH <name> <factor>
let style_name = parts.get(1).map(|s| s.trim()).unwrap_or("").to_string();
let factor_str = parts.get(2).map(|s| s.trim()).unwrap_or("");
if let Ok(factor) = factor_str.parse::<f64>() {
if let Some(s) =
let undo = self.begin_text_style_undo(
i,
"STYLE WIDTH",
std::slice::from_ref(&style_name),
);
if let Some(style) =
self.tabs[i].scene.document.text_styles.get_mut(&style_name)
{
s.width_factor = factor;
self.push_undo_snapshot(i, "STYLE WIDTH");
style.width_factor = factor;
self.tabs[i].dirty = true;
self.tabs[i]
.scene
.invalidate_text_style_dependencies(&style_name);
self.commit_text_style_undo(i, undo);
self.command_line.push_output(&format!(
"{prefix}: '{style_name}' width factor set to {factor:.3}."
));
@ -1179,12 +1214,20 @@ impl OpenCADStudio {
let style_name = parts.get(1).map(|s| s.trim()).unwrap_or("").to_string();
let angle_str = parts.get(2).map(|s| s.trim()).unwrap_or("");
if let Ok(deg) = angle_str.parse::<f64>() {
if let Some(s) =
let undo = self.begin_text_style_undo(
i,
"STYLE OBLIQUE",
std::slice::from_ref(&style_name),
);
if let Some(style) =
self.tabs[i].scene.document.text_styles.get_mut(&style_name)
{
s.oblique_angle = deg.to_radians();
self.push_undo_snapshot(i, "STYLE OBLIQUE");
style.oblique_angle = deg.to_radians();
self.tabs[i].dirty = true;
self.tabs[i]
.scene
.invalidate_text_style_dependencies(&style_name);
self.commit_text_style_undo(i, undo);
self.command_line.push_output(&format!(
"{prefix}: '{style_name}' oblique angle set to {deg:.1}°."
));

View file

@ -23,7 +23,8 @@ impl OpenCADStudio {
.into_iter()
.map(|(_, e)| e.common().layer.clone())
.collect();
self.push_undo_snapshot(i, "LAYOFF");
let names: Vec<String> = layers.iter().cloned().collect();
let undo = self.begin_layer_undo(i, "LAYOFF", &names);
for name in &layers {
if name == "0" {
continue;
@ -32,8 +33,9 @@ impl OpenCADStudio {
dl.turn_off();
}
}
self.tabs[i].scene.bump_geometry();
self.tabs[i].scene.invalidate_layer_dependencies(&names);
self.tabs[i].dirty = true;
self.commit_layer_undo(i, undo);
self.refresh_layer_panel();
self.command_line.push_info("Layer(s) turned off.");
}
@ -58,7 +60,8 @@ impl OpenCADStudio {
.into_iter()
.map(|(_, e)| e.common().layer.clone())
.collect();
self.push_undo_snapshot(i, "LAYFRZ");
let names: Vec<String> = layers.iter().cloned().collect();
let undo = self.begin_layer_undo(i, "LAYFRZ", &names);
for name in &layers {
if name == "0" {
continue;
@ -67,8 +70,9 @@ impl OpenCADStudio {
dl.freeze();
}
}
self.tabs[i].scene.bump_geometry();
self.tabs[i].scene.invalidate_layer_dependencies(&names);
self.tabs[i].dirty = true;
self.commit_layer_undo(i, undo);
self.refresh_layer_panel();
self.command_line.push_info("Layer(s) frozen.");
}
@ -266,10 +270,18 @@ impl OpenCADStudio {
"LAYERSTATE: no saved state named \"{arg}\"."
));
} else {
self.push_undo_snapshot(i, "LAYERSTATE");
let names: Vec<String> = self.tabs[i]
.scene
.document
.layers
.iter()
.map(|layer| layer.name.clone())
.collect();
let undo = self.begin_layer_undo(i, "LAYERSTATE", &names);
let n = self.tabs[i].restore_layer_state(arg).unwrap_or(0);
self.tabs[i].scene.bump_geometry();
self.tabs[i].scene.invalidate_layer_dependencies(&names);
self.tabs[i].dirty = true;
self.commit_layer_undo(i, undo);
self.refresh_layer_panel();
self.command_line.push_output(&format!(
"LAYERSTATE: restored \"{arg}\" ({n} layer(s))."
@ -312,14 +324,16 @@ impl OpenCADStudio {
.into_iter()
.map(|(_, e)| e.common().layer.clone())
.collect();
self.push_undo_snapshot(i, "LAYLCK");
let names: Vec<String> = layers.iter().cloned().collect();
let undo = self.begin_layer_undo(i, "LAYLCK", &names);
for name in &layers {
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
dl.lock();
}
}
self.tabs[i].scene.bump_geometry();
// Layer locking changes editability only.
self.tabs[i].dirty = true;
self.commit_layer_undo(i, undo);
self.refresh_layer_panel();
self.command_line.push_info("Layer(s) locked.");
}
@ -359,41 +373,43 @@ impl OpenCADStudio {
}
"LAYON" => {
self.push_undo_snapshot(i, "LAYON");
for name in self.tabs[i]
let names = self.tabs[i]
.scene
.document
.layers
.iter()
.map(|l| l.name.clone())
.collect::<Vec<_>>()
{
.collect::<Vec<_>>();
let undo = self.begin_layer_undo(i, "LAYON", &names);
for name in &names {
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
dl.turn_on();
}
}
self.tabs[i].scene.bump_geometry();
self.tabs[i].scene.invalidate_layer_dependencies(&names);
self.tabs[i].dirty = true;
self.commit_layer_undo(i, undo);
self.refresh_layer_panel();
self.command_line.push_info("All layers turned on.");
}
"LAYTHW" => {
self.push_undo_snapshot(i, "LAYTHW");
for name in self.tabs[i]
let names = self.tabs[i]
.scene
.document
.layers
.iter()
.map(|l| l.name.clone())
.collect::<Vec<_>>()
{
.collect::<Vec<_>>();
let undo = self.begin_layer_undo(i, "LAYTHW", &names);
for name in &names {
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
dl.thaw();
}
}
self.tabs[i].scene.bump_geometry();
self.tabs[i].scene.invalidate_layer_dependencies(&names);
self.tabs[i].dirty = true;
self.commit_layer_undo(i, undo);
self.refresh_layer_panel();
self.command_line.push_info("All layers thawed.");
}
@ -417,14 +433,16 @@ impl OpenCADStudio {
.into_iter()
.map(|(_, e)| e.common().layer.clone())
.collect();
self.push_undo_snapshot(i, "LAYULK");
let names: Vec<String> = layers.iter().cloned().collect();
let undo = self.begin_layer_undo(i, "LAYULK", &names);
for name in &layers {
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
dl.unlock();
}
}
self.tabs[i].scene.bump_geometry();
// Layer unlocking changes editability only.
self.tabs[i].dirty = true;
self.commit_layer_undo(i, undo);
self.refresh_layer_panel();
self.command_line.push_info("Layer(s) unlocked.");
}
@ -442,7 +460,6 @@ impl OpenCADStudio {
self.command_line
.push_error("LAYISO: select entities on the layers to isolate first.");
} else {
self.push_undo_snapshot(i, "LAYISO");
let names: Vec<String> = self.tabs[i]
.scene
.document
@ -450,15 +467,17 @@ impl OpenCADStudio {
.iter()
.map(|l| l.name.clone())
.collect();
for name in names {
if !sel_layers.contains(&name) {
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
let undo = self.begin_layer_undo(i, "LAYISO", &names);
for name in &names {
if !sel_layers.contains(name) {
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
dl.turn_off();
}
}
}
self.tabs[i].scene.bump_geometry();
self.tabs[i].scene.invalidate_layer_dependencies(&names);
self.tabs[i].dirty = true;
self.commit_layer_undo(i, undo);
self.refresh_layer_panel();
self.command_line
.push_info(&format!("LAYISO: isolated {} layer(s).", sel_layers.len()));
@ -511,7 +530,6 @@ impl OpenCADStudio {
// LAYUNISO — restore all layers that were turned off by LAYISO (turn all on)
"LAYUNISO" => {
self.push_undo_snapshot(i, "LAYUNISO");
let names: Vec<String> = self.tabs[i]
.scene
.document
@ -519,13 +537,15 @@ impl OpenCADStudio {
.iter()
.map(|l| l.name.clone())
.collect();
for name in names {
let undo = self.begin_layer_undo(i, "LAYUNISO", &names);
for name in &names {
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
dl.turn_on();
}
}
self.tabs[i].scene.bump_geometry();
self.tabs[i].scene.invalidate_layer_dependencies(&names);
self.tabs[i].dirty = true;
self.commit_layer_undo(i, undo);
self.refresh_layer_panel();
self.command_line
.push_info("LAYUNISO: all layers restored.");
@ -587,9 +607,10 @@ impl OpenCADStudio {
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
} else {
self.push_undo_snapshot(i, "UNGROUP");
let undo = self.begin_group_undo(i, "UNGROUP");
let count = self.tabs[i].scene.delete_groups_containing(&handles);
self.tabs[i].dirty = true;
self.commit_group_undo(i, undo);
if count > 0 {
self.command_line
.push_info(&format!("{} group(s) dissolved.", count));

View file

@ -520,7 +520,7 @@ impl HistorySnapshot {
.saturating_add(
d.structure
.as_ref()
.map_or(0, |doc| doc.objects.len().saturating_mul(192)),
.map_or(0, StructureSnapshot::estimated_bytes),
)
.saturating_add(d.selected_before.len().saturating_mul(16))
.saturating_add(d.selected_after.len().saturating_mul(16))
@ -546,10 +546,67 @@ pub(super) struct DeltaSnapshot {
pub(super) dirty_after: bool,
/// Opposite non-entity document state. `apply_delta_state` swaps this with
/// the live structure, so the same allocation shuttles between undo/redo.
pub(super) structure: Option<CadDocument>,
pub(super) structure: Option<StructureSnapshot>,
pub(super) label: String,
}
#[derive(Clone)]
pub(super) enum StructureSnapshot {
/// Compatibility fallback for genuinely broad structural commands.
Full(CadDocument),
/// Exact layer-table entries touched by one command.
Layers(Vec<TableEntryDelta<acadrust::tables::Layer>>),
/// Exact text-style entries touched by one command.
TextStyles(Vec<TableEntryDelta<acadrust::tables::TextStyle>>),
/// Exact dimension-style entries touched by one command.
DimStyles(Vec<TableEntryDelta<acadrust::tables::DimStyle>>),
/// Exact object-map entries touched by one command. This supports commands
/// such as groups/dictionaries without retaining every unrelated object.
Objects(Vec<ObjectEntryDelta>),
/// The bounded set of style tables, style objects, current-style pointers,
/// and matching ribbon state touched by one Style Manager transaction.
Styles {
before: super::style_ops::StyleStateSnapshot,
after: super::style_ops::StyleStateSnapshot,
text_names: Vec<String>,
dim_names: Vec<String>,
object_handles: Vec<Handle>,
},
}
impl StructureSnapshot {
pub(super) fn estimated_bytes(&self) -> usize {
match self {
Self::Full(doc) => doc.objects.len().saturating_mul(192),
Self::Layers(entries) => entries.len().saturating_mul(256),
Self::TextStyles(entries) => entries.len().saturating_mul(320),
Self::DimStyles(entries) => entries.len().saturating_mul(1024),
Self::Objects(entries) => entries.len().saturating_mul(384),
Self::Styles { before, after, .. } => before
.estimated_bytes()
.saturating_add(after.estimated_bytes()),
}
}
pub(super) fn is_full(&self) -> bool {
matches!(self, Self::Full(_))
}
}
#[derive(Clone)]
pub(super) struct TableEntryDelta<T> {
pub(super) name: String,
pub(super) before: Option<T>,
pub(super) after: Option<T>,
}
#[derive(Clone)]
pub(super) struct ObjectEntryDelta {
pub(super) handle: Handle,
pub(super) before: Option<acadrust::objects::ObjectType>,
pub(super) after: Option<acadrust::objects::ObjectType>,
}
#[derive(Default)]
pub(super) struct HistoryState {
pub(super) undo_stack: Vec<HistorySnapshot>,

View file

@ -1,5 +1,8 @@
use super::{
document::{DeltaSnapshot, HistorySnapshot, PendingHistorySnapshot},
document::{
DeltaSnapshot, HistorySnapshot, ObjectEntryDelta, PendingHistorySnapshot,
StructureSnapshot, TableEntryDelta,
},
OpenCADStudio,
};
use acadrust::{EntityType, Handle};
@ -71,6 +74,38 @@ pub(super) struct PendingDelta {
structure_before: Option<acadrust::CadDocument>,
}
pub(super) struct PendingLayerDelta {
label: String,
current_layout: String,
selected_before: Vec<Handle>,
dirty_before: bool,
before: Vec<(String, Option<acadrust::tables::Layer>)>,
}
pub(super) struct PendingTextStyleDelta {
label: String,
current_layout: String,
selected_before: Vec<Handle>,
dirty_before: bool,
before: Vec<(String, Option<acadrust::tables::TextStyle>)>,
}
pub(super) struct PendingDimStyleDelta {
label: String,
current_layout: String,
selected_before: Vec<Handle>,
dirty_before: bool,
before: Vec<(String, Option<acadrust::tables::DimStyle>)>,
}
pub(super) struct PendingObjectDelta {
label: String,
current_layout: String,
selected_before: Vec<Handle>,
dirty_before: bool,
before: FxHashMap<Handle, acadrust::objects::ObjectType>,
}
impl OpenCADStudio {
pub(super) fn history_label_from_active_cmd(&self, i: usize, fallback: &'static str) -> String {
self.tabs[i]
@ -248,7 +283,7 @@ impl OpenCADStudio {
selected_after,
dirty_before: pending.dirty_before,
dirty_after,
structure: structure_changed.then_some(pending.structure_before),
structure: structure_changed.then_some(StructureSnapshot::Full(pending.structure_before)),
label: pending.label,
};
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
@ -304,6 +339,263 @@ impl OpenCADStudio {
})
}
pub(super) fn begin_layer_undo(
&mut self,
i: usize,
label: impl Into<String>,
names: &[String],
) -> PendingLayerDelta {
self.finish_pending_history(i);
PendingLayerDelta {
label: label.into(),
current_layout: self.tabs[i].scene.current_layout.clone(),
selected_before: self.tabs[i].scene.selected.iter().copied().collect(),
dirty_before: self.tabs[i].dirty,
before: names
.iter()
.map(|name| {
(
name.clone(),
self.tabs[i].scene.document.layers.get(name).cloned(),
)
})
.collect(),
}
}
pub(super) fn commit_layer_undo(&mut self, i: usize, pending: PendingLayerDelta) {
let entries: Vec<_> = pending
.before
.into_iter()
.filter_map(|(name, before)| {
let after = self.tabs[i].scene.document.layers.get(&name).cloned();
(before != after).then_some(TableEntryDelta {
name,
before,
after,
})
})
.collect();
if entries.is_empty() {
return;
}
let selected_after = self.tabs[i].scene.selected.iter().copied().collect();
let delta = DeltaSnapshot {
entities: Vec::new(),
current_layout_before: pending.current_layout,
current_layout_after: self.tabs[i].scene.current_layout.clone(),
selected_before: pending.selected_before,
selected_after,
dirty_before: pending.dirty_before,
dirty_after: self.tabs[i].dirty,
structure: Some(StructureSnapshot::Layers(entries)),
label: pending.label,
};
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
}
pub(super) fn begin_text_style_undo(
&mut self,
i: usize,
label: impl Into<String>,
names: &[String],
) -> PendingTextStyleDelta {
self.finish_pending_history(i);
PendingTextStyleDelta {
label: label.into(),
current_layout: self.tabs[i].scene.current_layout.clone(),
selected_before: self.tabs[i].scene.selected.iter().copied().collect(),
dirty_before: self.tabs[i].dirty,
before: names
.iter()
.map(|name| {
(
name.clone(),
self.tabs[i].scene.document.text_styles.get(name).cloned(),
)
})
.collect(),
}
}
pub(super) fn commit_text_style_undo(&mut self, i: usize, pending: PendingTextStyleDelta) {
let entries: Vec<_> = pending
.before
.into_iter()
.filter_map(|(name, before)| {
let after = self.tabs[i].scene.document.text_styles.get(&name).cloned();
(before != after).then_some(TableEntryDelta {
name,
before,
after,
})
})
.collect();
if entries.is_empty() {
return;
}
let delta = DeltaSnapshot {
entities: Vec::new(),
current_layout_before: pending.current_layout,
current_layout_after: self.tabs[i].scene.current_layout.clone(),
selected_before: pending.selected_before,
selected_after: self.tabs[i].scene.selected.iter().copied().collect(),
dirty_before: pending.dirty_before,
dirty_after: self.tabs[i].dirty,
structure: Some(StructureSnapshot::TextStyles(entries)),
label: pending.label,
};
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
}
pub(super) fn begin_dim_style_undo(
&mut self,
i: usize,
label: impl Into<String>,
names: &[String],
) -> PendingDimStyleDelta {
self.finish_pending_history(i);
PendingDimStyleDelta {
label: label.into(),
current_layout: self.tabs[i].scene.current_layout.clone(),
selected_before: self.tabs[i].scene.selected.iter().copied().collect(),
dirty_before: self.tabs[i].dirty,
before: names
.iter()
.map(|name| {
(
name.clone(),
self.tabs[i].scene.document.dim_styles.get(name).cloned(),
)
})
.collect(),
}
}
pub(super) fn commit_dim_style_undo(&mut self, i: usize, pending: PendingDimStyleDelta) {
let entries: Vec<_> = pending
.before
.into_iter()
.filter_map(|(name, before)| {
let after = self.tabs[i].scene.document.dim_styles.get(&name).cloned();
(before != after).then_some(TableEntryDelta {
name,
before,
after,
})
})
.collect();
if entries.is_empty() {
return;
}
let delta = DeltaSnapshot {
entities: Vec::new(),
current_layout_before: pending.current_layout,
current_layout_after: self.tabs[i].scene.current_layout.clone(),
selected_before: pending.selected_before,
selected_after: self.tabs[i].scene.selected.iter().copied().collect(),
dirty_before: pending.dirty_before,
dirty_after: self.tabs[i].dirty,
structure: Some(StructureSnapshot::DimStyles(entries)),
label: pending.label,
};
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
}
fn group_object_state(&self, i: usize) -> FxHashMap<Handle, acadrust::objects::ObjectType> {
use acadrust::objects::ObjectType;
let document = &self.tabs[i].scene.document;
let dictionary = document.header.acad_group_dict_handle;
document
.objects
.iter()
.filter(|(handle, object)| {
**handle == dictionary || matches!(object, ObjectType::Group(_))
})
.map(|(handle, object)| (*handle, object.clone()))
.collect()
}
pub(super) fn begin_group_undo(
&mut self,
i: usize,
label: impl Into<String>,
) -> PendingObjectDelta {
self.finish_pending_history(i);
PendingObjectDelta {
label: label.into(),
current_layout: self.tabs[i].scene.current_layout.clone(),
selected_before: self.tabs[i].scene.selected.iter().copied().collect(),
dirty_before: self.tabs[i].dirty,
before: self.group_object_state(i),
}
}
pub(super) fn commit_group_undo(&mut self, i: usize, pending: PendingObjectDelta) {
let after = self.group_object_state(i);
let mut handles: HashSet<Handle> = pending.before.keys().copied().collect();
handles.extend(after.keys().copied());
let entries: Vec<_> = handles
.into_iter()
.filter_map(|handle| {
let before = pending.before.get(&handle).cloned();
let after = after.get(&handle).cloned();
(before != after).then_some(ObjectEntryDelta {
handle,
before,
after,
})
})
.collect();
if entries.is_empty() {
return;
}
let delta = DeltaSnapshot {
entities: Vec::new(),
current_layout_before: pending.current_layout,
current_layout_after: self.tabs[i].scene.current_layout.clone(),
selected_before: pending.selected_before,
selected_after: self.tabs[i].scene.selected.iter().copied().collect(),
dirty_before: pending.dirty_before,
dirty_after: self.tabs[i].dirty,
structure: Some(StructureSnapshot::Objects(entries)),
label: pending.label,
};
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
}
pub(super) fn commit_style_undo(
&mut self,
i: usize,
before: super::style_ops::StyleStateSnapshot,
after: super::style_ops::StyleStateSnapshot,
dirty_before: bool,
) {
if before == after {
return;
}
self.finish_pending_history(i);
let (text_names, dim_names, object_handles) = after.changed_keys(&before);
let delta = DeltaSnapshot {
entities: Vec::new(),
current_layout_before: self.tabs[i].scene.current_layout.clone(),
current_layout_after: self.tabs[i].scene.current_layout.clone(),
selected_before: self.tabs[i].scene.selected.iter().copied().collect(),
selected_after: self.tabs[i].scene.selected.iter().copied().collect(),
dirty_before,
dirty_after: true,
structure: Some(StructureSnapshot::Styles {
before,
after,
text_names,
dim_names,
object_handles,
}),
label: "STYLE".to_string(),
};
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
}
/// Copy is delta-safe only when no target is a Dimension and no complete
/// group is copied: dimensions clone fresh anonymous `*D` block records,
/// and complete group copies add Group objects / dictionary entries. Both
@ -397,8 +689,9 @@ impl OpenCADStudio {
.collect();
let selected_after = self.tabs[i].scene.selected.iter().copied().collect();
let dirty_after = self.tabs[i].dirty;
let mut structure = pending.structure_before;
let mut structure = pending.structure_before.map(StructureSnapshot::Full);
if let Some(before_structure) = structure.as_mut() {
if let StructureSnapshot::Full(before_structure) = before_structure {
let after_structure = self.tabs[i].scene.document.snapshot_structure();
let added_handles: Vec<Handle> = entities
.iter()
@ -415,6 +708,7 @@ impl OpenCADStudio {
structure = None;
}
}
}
let delta = DeltaSnapshot {
entities,
current_layout_before: pending.current_layout,
@ -443,9 +737,113 @@ impl OpenCADStudio {
// Install the chosen side of every entity image. Derived-cache, geometry
// and UI invalidation are deferred until every requested undo/redo step
// has been applied.
if let Some(structure) = d.structure.take() {
let inverse = self.tabs[i].scene.document.swap_structure(structure);
d.structure = Some(inverse);
if let Some(structure) = d.structure.as_mut() {
match structure {
StructureSnapshot::Full(stored) => {
let inverse = self.tabs[i].scene.document.swap_structure(
std::mem::replace(stored, acadrust::CadDocument::new()),
);
*stored = inverse;
self.tabs[i].scene.invalidate_dependency_index();
}
StructureSnapshot::Layers(entries) => {
let names: Vec<String> =
entries.iter().map(|entry| entry.name.clone()).collect();
for entry in entries {
let value = if undo {
entry.before.clone()
} else {
entry.after.clone()
};
if let Some(layer) = value {
self.tabs[i].scene.document.layers.add_or_replace(layer);
} else {
self.tabs[i].scene.document.layers.remove(&entry.name);
}
}
self.tabs[i].scene.invalidate_layer_dependencies(&names);
}
StructureSnapshot::TextStyles(entries) => {
let names: Vec<String> =
entries.iter().map(|entry| entry.name.clone()).collect();
for entry in entries {
let value = if undo {
entry.before.clone()
} else {
entry.after.clone()
};
if let Some(style) = value {
self.tabs[i]
.scene
.document
.text_styles
.add_or_replace(style);
} else {
self.tabs[i].scene.document.text_styles.remove(&entry.name);
}
}
for name in names {
self.tabs[i].scene.invalidate_text_style_dependencies(&name);
}
}
StructureSnapshot::DimStyles(entries) => {
let names: Vec<String> =
entries.iter().map(|entry| entry.name.clone()).collect();
for entry in entries {
let value = if undo {
entry.before.clone()
} else {
entry.after.clone()
};
if let Some(style) = value {
self.tabs[i].scene.document.dim_styles.add_or_replace(style);
} else {
self.tabs[i].scene.document.dim_styles.remove(&entry.name);
}
}
for name in names {
self.tabs[i].scene.invalidate_dim_style_dependencies(&name);
}
}
StructureSnapshot::Objects(entries) => {
for entry in entries {
let value = if undo {
entry.before.clone()
} else {
entry.after.clone()
};
if let Some(object) = value {
self.tabs[i]
.scene
.document
.objects
.insert(entry.handle, object);
} else {
self.tabs[i].scene.document.objects.remove(&entry.handle);
}
}
self.tabs[i].scene.invalidate_dependency_index();
}
StructureSnapshot::Styles {
before,
after,
text_names,
dim_names,
object_handles,
} => {
let snapshot = if undo { before } else { after };
self.restore_style_state(i, snapshot);
self.tabs[i]
.scene
.invalidate_text_style_dependencies_many(text_names);
self.tabs[i]
.scene
.invalidate_dim_style_dependencies_many(dim_names);
self.tabs[i]
.scene
.invalidate_object_style_dependencies(object_handles);
}
}
}
let changes = self.tabs[i].scene.apply_entity_delta(&d.entities, undo);
let scene = &mut self.tabs[i].scene;
@ -479,6 +877,7 @@ impl OpenCADStudio {
&mut self,
i: usize,
had_full: bool,
structure_changed: bool,
changes: &[(Handle, crate::scene::ChangeKind)],
) {
self.tabs[i].edit_revision = self.tabs[i].edit_revision.wrapping_add(1);
@ -524,7 +923,7 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].active_grip = None;
if had_full {
if structure_changed {
let doc_layers = self.tabs[i].scene.document.layers.clone();
let vp_info = self.tabs[i].scene.viewport_list();
self.tabs[i]
@ -555,6 +954,7 @@ impl OpenCADStudio {
let mut last_label = String::new();
let mut had_full = false;
let mut structure_changed = false;
let mut changes = Vec::new();
for _ in 0..steps {
let Some(snapshot) = self.tabs[i].history.undo_stack.pop() else {
@ -566,7 +966,8 @@ impl OpenCADStudio {
// Symmetric: undo applies the before side, then the same
// delta rides to the redo stack (it still holds the after
// side) — no current-state capture needed.
had_full |= d.structure.is_some();
structure_changed |= d.structure.is_some();
had_full |= d.structure.as_ref().is_some_and(StructureSnapshot::is_full);
changes.extend(self.apply_delta_state(i, &mut d, true));
self.tabs[i]
.history
@ -575,7 +976,7 @@ impl OpenCADStudio {
}
}
}
self.finish_history_apply(i, had_full, &changes);
self.finish_history_apply(i, had_full, structure_changed, &changes);
self.command_line
.push_output(&format!("Undo: {last_label}"));
}
@ -592,6 +993,7 @@ impl OpenCADStudio {
let mut last_label = String::new();
let mut had_full = false;
let mut structure_changed = false;
let mut changes = Vec::new();
for _ in 0..steps {
let Some(snapshot) = self.tabs[i].history.redo_stack.pop() else {
@ -600,7 +1002,8 @@ impl OpenCADStudio {
last_label = snapshot.label().to_string();
match snapshot {
HistorySnapshot::Delta(mut d) => {
had_full |= d.structure.is_some();
structure_changed |= d.structure.is_some();
had_full |= d.structure.as_ref().is_some_and(StructureSnapshot::is_full);
changes.extend(self.apply_delta_state(i, &mut d, false));
self.tabs[i]
.history
@ -609,7 +1012,7 @@ impl OpenCADStudio {
}
}
}
self.finish_history_apply(i, had_full, &changes);
self.finish_history_apply(i, had_full, structure_changed, &changes);
self.command_line
.push_output(&format!("Redo: {last_label}"));
}

View file

@ -168,7 +168,6 @@ use acadrust::CadDocument;
use iced::time::Instant;
use iced::window;
use iced::{mouse, Point, Task, Theme};
use std::sync::atomic::AtomicU8;
use std::sync::Arc;
pub(super) const POLY_START_DELAY_MS: u128 = 150;
@ -179,14 +178,15 @@ pub(super) const VARIES_LABEL: &str = "*VARIES*";
// loader thread, read by the UI overlay on every frame.
pub const OPEN_PHASE_READING: u8 = 0;
pub const OPEN_PHASE_PARSING: u8 = 1;
pub const OPEN_PHASE_CACHING: u8 = 2;
pub const OPEN_PHASE_FINALIZING: u8 = 3;
pub const OPEN_PHASE_XREF: u8 = 2;
pub const OPEN_PHASE_CACHING: u8 = 3;
pub const OPEN_PHASE_FINALIZING: u8 = 4;
#[derive(Debug, Clone)]
pub struct OpenProgress {
pub name: String,
pub size_bytes: u64,
pub phase: Arc<AtomicU8>,
pub state: Arc<crate::io::OpenProgressState>,
pub started: Instant,
}
@ -2098,6 +2098,9 @@ pub enum Message {
PlotWindowExport,
/// Callback after the user picks (or cancels) the window-export path.
PlotWindowExportPath(Option<std::path::PathBuf>),
/// Completion of a PDF/preview/print job performed outside the UI thread.
/// The boolean restores the Plot dialog after a preview.
BackgroundIoFinished(Result<String, String>, bool),
/// Send current layout to the system printer (via lp / lpr).
PrintToPrinter,
/// Callback from the async printer job.
@ -2299,6 +2302,8 @@ pub enum Message {
WblockSave(String),
/// Result of the WBLOCK save path dialog.
WblockSaveResult(String, Option<std::path::PathBuf>),
/// Background extraction/write completion.
WblockWriteFinished(String, std::path::PathBuf, Result<(), String>),
// ── DATAEXTRACTION ────────────────────────────────────────────────────
/// Save the pre-built CSV string to a file chosen by the user.
DataExtractionSave(String),
@ -2309,16 +2314,23 @@ pub enum Message {
StlExport,
/// Callback after the user picks (or cancels) the STL save path.
StlExportPath(Option<std::path::PathBuf>),
StlExportFinished(std::path::PathBuf, Result<(), String>),
// ── STEP export ───────────────────────────────────────────────────────
/// Trigger STEP AP203 export: show save dialog.
StepExport,
/// Callback after the user picks (or cancels) the STEP save path.
StepExportPath(Option<std::path::PathBuf>),
StepExportFinished(std::path::PathBuf, Result<(), String>),
// ── OBJ import ────────────────────────────────────────────────────────
/// Trigger OBJ import: show open-file dialog.
ObjImport,
/// Callback after the user picks (or cancels) the OBJ file path.
ObjImportPath(Option<std::path::PathBuf>),
ObjImportFinished(
u64,
std::path::PathBuf,
Result<crate::scene::model::mesh_model::MeshModel, String>,
),
}
impl OpenCADStudio {

View file

@ -539,8 +539,7 @@ impl OpenCADStudio {
/// Overwrite the live style state with a snapshot (used by commit's undo
/// dance and by discard).
fn restore_style_state(&mut self, snap: &StyleStateSnapshot) {
let i = self.active_tab;
pub(super) fn restore_style_state(&mut self, i: usize, snap: &StyleStateSnapshot) {
let doc = &mut self.tabs[i].scene.document;
doc.text_styles = snap.text_styles.clone();
doc.dim_styles = snap.dim_styles.clone();
@ -560,8 +559,10 @@ impl OpenCADStudio {
doc.header.multiline_style = snap.multiline_style.clone();
doc.header.current_table_style_name = snap.current_table.clone();
doc.header.current_mleader_style_name = snap.current_mleader.clone();
if i == self.active_tab {
self.ribbon.active_table_style = snap.active_table.clone();
self.ribbon.active_mleader_style = snap.active_mleader.clone();
}
self.tabs[i].active_mleader_style = snap.tab_active_mleader.clone();
}
@ -587,18 +588,26 @@ impl OpenCADStudio {
self.sync_ribbon_styles();
return;
};
// Capture the edited state, rewind to the baseline so the undo entry
// restores the pre-edit document, then re-apply the edits on top.
let edited = self.capture_style_state();
self.restore_style_state(&stage.baseline);
self.push_undo_snapshot(i, "STYLE");
self.restore_style_state(&edited);
let changed = edited != stage.baseline;
if changed {
self.tabs[i].dirty = true;
self.tabs[i].scene.bump_geometry();
let (text_names, dim_names, object_handles) = edited.changed_keys(&stage.baseline);
self.tabs[i].scene.invalidate_text_style_dependencies_many(&text_names);
self.tabs[i]
.scene
.invalidate_dim_style_dependencies_many(&dim_names);
self.tabs[i]
.scene
.invalidate_object_style_dependencies(&object_handles);
self.commit_style_undo(i, stage.baseline, edited.clone(), stage.dirty_at_open);
} else {
self.tabs[i].dirty = stage.dirty_at_open;
}
self.sync_ribbon_styles();
// Re-baseline so further edits in the still-open window stage afresh.
self.style_stage = Some(StyleStage {
dirty_at_open: true,
dirty_at_open: self.tabs[i].dirty,
baseline: edited,
});
}
@ -609,13 +618,14 @@ impl OpenCADStudio {
let Some(stage) = self.style_stage.take() else {
return;
};
self.restore_style_state(&stage.baseline);
self.restore_style_state(self.active_tab, &stage.baseline);
self.tabs[self.active_tab].dirty = stage.dirty_at_open;
self.sync_ribbon_styles();
}
}
/// Snapshot of every document field a style manager can touch.
#[derive(Clone, PartialEq)]
pub(super) struct StyleStateSnapshot {
text_styles: acadrust::tables::Table<TextStyle>,
dim_styles: acadrust::tables::Table<DimStyle>,
@ -630,6 +640,64 @@ pub(super) struct StyleStateSnapshot {
tab_active_mleader: String,
}
impl StyleStateSnapshot {
pub(super) fn estimated_bytes(&self) -> usize {
self.text_styles
.iter()
.count()
.saturating_mul(320)
.saturating_add(self.dim_styles.iter().count().saturating_mul(1024))
.saturating_add(self.style_objects.len().saturating_mul(512))
}
pub(super) fn changed_keys(&self, other: &Self) -> (Vec<String>, Vec<String>, Vec<Handle>) {
let text_names = self
.text_styles
.iter()
.filter(|style| other.text_styles.get(&style.name) != Some(*style))
.map(|style| style.name.clone())
.chain(
other
.text_styles
.iter()
.filter(|style| self.text_styles.get(&style.name) != Some(*style))
.map(|style| style.name.clone()),
)
.collect::<rustc_hash::FxHashSet<_>>()
.into_iter()
.collect();
let dim_names = self
.dim_styles
.iter()
.filter(|style| other.dim_styles.get(&style.name) != Some(*style))
.map(|style| style.name.clone())
.chain(
other
.dim_styles
.iter()
.filter(|style| self.dim_styles.get(&style.name) != Some(*style))
.map(|style| style.name.clone()),
)
.collect::<rustc_hash::FxHashSet<_>>()
.into_iter()
.collect();
let self_objects: rustc_hash::FxHashMap<_, _> =
self.style_objects.iter().map(|(h, o)| (*h, o)).collect();
let other_objects: rustc_hash::FxHashMap<_, _> =
other.style_objects.iter().map(|(h, o)| (*h, o)).collect();
let object_handles = self
.style_objects
.iter()
.chain(other.style_objects.iter())
.map(|(handle, _)| *handle)
.collect::<rustc_hash::FxHashSet<_>>()
.into_iter()
.filter(|handle| self_objects.get(handle) != other_objects.get(handle))
.collect();
(text_names, dim_names, object_handles)
}
}
/// An in-progress style-manager transaction.
pub(super) struct StyleStage {
dirty_at_open: bool,

View file

@ -20,6 +20,28 @@ use acadrust::{EntityType as AcadEntityType, Handle};
use iced::time::Instant;
use iced::{mouse, Point, Task};
pub(super) fn background_task<T, F, M>(work: F, map: M) -> Task<Message>
where
T: Send + 'static,
F: FnOnce() -> T + Send + 'static,
M: FnOnce(T) -> Message + Send + 'static,
{
#[cfg(not(target_arch = "wasm32"))]
{
let (tx, rx) = iced::futures::channel::oneshot::channel();
std::thread::spawn(move || {
let _ = tx.send(work());
});
Task::perform(
async move { rx.await.expect("background export worker dropped") },
map,
)
}
#[cfg(target_arch = "wasm32")]
{
Task::perform(async move { work() }, map)
}
}
impl OpenCADStudio {
/// Before a save, give every cached truck solid that still has no ACIS
@ -362,15 +384,16 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
// progress, so mark one. The browser picker + parse happen
// inside `pick_and_load_web`; the real name is unknown until
// then, so show a generic label meanwhile.
let state = std::sync::Arc::new(crate::io::OpenProgressState::new(
crate::app::OPEN_PHASE_READING,
));
self.opening = Some(crate::app::OpenProgress {
name: "Opening…".into(),
size_bytes: 0,
phase: std::sync::Arc::new(std::sync::atomic::AtomicU8::new(
crate::app::OPEN_PHASE_READING,
)),
state: state.clone(),
started: Instant::now(),
});
Task::perform(crate::io::pick_and_load_web(), Message::FileOpened)
Task::perform(crate::io::pick_and_load_web(state), Message::FileOpened)
}
}
@ -403,13 +426,15 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
}
}
pub(super) fn on_file_opened(&mut self, name: String, path: std::path::PathBuf, doc: acadrust::CadDocument, caches: crate::scene::DerivedCaches) -> Task<Message> {
pub(super) fn on_file_opened(&mut self, name: String, path: std::path::PathBuf, doc: acadrust::CadDocument,
mut caches: crate::scene::DerivedCaches,
) -> Task<Message> {
// If the user clicked Cancel while the parser was running, the
// overlay state was cleared and we silently drop the result.
if self.opening.is_none() {
return Task::none();
}
let open_started = self.opening.take().map(|p| p.started);
let open_started = self.opening.as_ref().map(|p| p.started);
let timings = caches.timings;
let entity_count = doc.entities().count();
self.command_line
@ -420,6 +445,30 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
caches.corrupt_dropped
));
}
if caches.xref_dropped > 0 {
self.command_line.push_error(&format!(
"Warning: {} corrupt xref entities dropped",
caches.xref_dropped
));
}
for info in &caches.xrefs {
match info.status {
crate::io::xref::XrefStatus::Loaded => {
self.command_line
.push_output(&format!("XREF Loaded \"{}\"", info.name));
}
crate::io::xref::XrefStatus::NotFound => {
self.command_line.push_error(&format!(
"XREF Not found: \"{}\" ({})",
info.name, info.path
));
}
crate::io::xref::XrefStatus::Unloaded => {
self.command_line
.push_info(&format!("XREF Unloaded (skipped): \"{}\"", info.name));
}
}
}
let thumbs_task = self.push_recent(path.clone());
let current_is_empty = {
@ -472,47 +521,6 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
1.0
};
// Auto-resolve XREFs relative to the opened file's directory.
let mut xref_ms = 0u32;
let mut xref_merged = false;
if let Some(base_dir) = path.parent() {
// xref content arrives un-purged: parser-garbage entities
// inside the referenced file can trigger infinite loops in
// tessellation. `resolve_xrefs` runs the corrupt-entity
// guard inline as it merges each xref, so no second
// full-document walk is needed here.
let t_xref = Instant::now();
let (xrefs, extra_dropped) =
crate::io::xref::resolve_xrefs(&mut self.tabs[i].scene.document, base_dir);
xref_ms = t_xref.elapsed().as_millis() as u32;
if extra_dropped > 0 {
self.command_line.push_error(&format!(
"Warning: {extra_dropped} corrupt xref entities dropped"
));
}
for info in &xrefs {
match info.status {
crate::io::xref::XrefStatus::Loaded => {
xref_merged = true;
self.command_line
.push_output(&format!("XREF Loaded \"{}\"", info.name));
}
crate::io::xref::XrefStatus::NotFound => {
self.command_line.push_error(&format!(
"XREF Not found: \"{}\" ({})",
info.name, info.path
));
}
crate::io::xref::XrefStatus::Unloaded => {
self.command_line.push_info(&format!(
"XREF Unloaded (skipped): \"{}\"",
info.name
));
}
}
}
}
// Open-time breakdown so regressions are visible immediately.
// `total` is wall time from the Open click to here (post-xref,
// pre-first-frame); the phase figures are the background-thread
@ -522,7 +530,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
.unwrap_or(0);
self.command_line.push_info(&format!(
" parse {}ms · purge {}ms · caches {}ms · xref {}ms · total {}ms",
timings.parse_ms, timings.purge_ms, timings.caches_ms, xref_ms, total_ms
timings.parse_ms, timings.purge_ms, timings.caches_ms, timings.xref_ms, total_ms
));
// Caches were built on the background thread inside open_path().
@ -532,18 +540,11 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
self.tabs[i].scene.images = caches.images;
self.tabs[i].scene.meshes = caches.meshes;
self.tabs[i].scene.block_meshes = caches.block_meshes;
let prepared_geometry = caches.prepared_geometry.take();
// Invalidate the wire cache so the new document is tessellated.
self.tabs[i].scene.bump_geometry();
// XREFs are merged into the document AFTER the background worker
// built the mesh caches above, so those caches contain none of
// the xref'd geometry. The wire pass rebuilds from the document
// each frame (bump_geometry covers it), but 3D-solid meshes are
// only tessellated by populate — run the incremental variant so
// the already-cached host solids are kept and only the newly
// merged xref solids (walls, floors, roofs) are tessellated,
// avoiding a full re-tessellation of the whole drawing. (#203)
if xref_merged {
self.tabs[i].scene.populate_missing_meshes_from_document();
if let Some(prepared) = prepared_geometry {
self.tabs[i].scene.install_prepared_open_geometry(prepared);
}
self.tabs[i].scene.selected = rustc_hash::FxHashSet::default();
self.tabs[i].scene.preview_wires = vec![];
@ -625,42 +626,38 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
};
#[cfg(target_arch = "wasm32")]
let interaction_task = Task::none();
if let Some(opening) = &self.opening {
opening
.state
.set(crate::app::OPEN_PHASE_FINALIZING, 10000, 1, 1);
}
self.opening.take();
let pending_open_task = self.drain_pending_open();
Task::batch([thumbs_task, pending_open_task, interaction_task])
}
pub(super) fn on_wblock_save_result_some(&mut self, block_name: String, path: std::path::PathBuf) -> Task<Message> {
pub(super) fn on_wblock_save_result_some(&mut self, block_name: String, path: std::path::PathBuf,
) -> Task<Message> {
let i = self.active_tab;
let result = if block_name == "*" {
let document = self.tabs[i].scene.document.clone();
let handles: Vec<_> = self.tabs[i].scene.selected.iter().copied().collect();
let worker_name = block_name.clone();
let worker_path = path.clone();
background_task(
move || {
let document = if worker_name == "*" {
crate::modules::insert::wblock::extract_entities_to_doc(
&self.tabs[i].scene.document,
&document,
&handles,
)
} else {
crate::modules::insert::wblock::extract_block_to_doc(
&self.tabs[i].scene.document,
&block_name,
)
};
match result {
Ok(doc) => match crate::io::save(&doc, &path) {
Ok(()) => {
let fname = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| path.to_string_lossy().into_owned());
self.command_line.push_output(&format!(
"WBLOCK Saved \"{block_name}\"\"{fname}\""
));
&document,
&worker_name)
}
Err(e) => self
.command_line
.push_error(&format!("WBLOCK save failed: {e}")),
.map_err(|e| e.to_string())?; crate::io::save(&document, &worker_path).map_err(|e| e.to_string())
},
Err(e) => self.command_line.push_error(&format!("WBLOCK: {e}")),
}
Task::none()
move |result| Message::WblockWriteFinished(block_name, path, result),)
}
pub(super) fn on_stl_export_path_some(&mut self, path: std::path::PathBuf) -> Task<Message> {
@ -675,21 +672,15 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
.values()
.filter_map(|s| s.lods.first().cloned())
.collect();
let mesh_refs: Vec<&crate::scene::model::mesh_model::MeshModel> = meshes.iter().collect();
match crate::io::stl::build_stl(&mesh_refs) {
Some(bytes) => match std::fs::write(&path, bytes) {
Ok(()) => self
.command_line
.push_output(&format!("STLOUT: exported to \"{}\"", path.display())),
Err(e) => self
.command_line
.push_error(&format!("STLOUT: write error: {e}")),
let worker_path = path.clone();
background_task(
move || {
let mesh_refs: Vec<_> = meshes.iter().collect();
let bytes = crate::io::stl::build_stl(&mesh_refs)
.ok_or_else(|| "no mesh data to export".to_string())?;
std::fs::write(&worker_path, bytes).map_err(|e| e.to_string())
},
None => self
.command_line
.push_error("STLOUT: no mesh data to export."),
}
Task::none()
move |result| Message::StlExportFinished(path, result),)
}
pub(super) fn on_step_export_path_some(&mut self, path: std::path::PathBuf) -> Task<Message> {
@ -701,63 +692,26 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
.values()
.filter_map(|s| s.lods.first().cloned())
.collect();
let mesh_refs: Vec<&crate::scene::model::mesh_model::MeshModel> = meshes.iter().collect();
match crate::io::step::build_step(&mesh_refs) {
Some(text) => match std::fs::write(&path, text.as_bytes()) {
Ok(()) => self
.command_line
.push_output(&format!("STEPOUT: exported to \"{}\"", path.display())),
Err(e) => self
.command_line
.push_error(&format!("STEPOUT: write error: {e}")),
let worker_path = path.clone();
background_task(
move || {
let mesh_refs: Vec<_> = meshes.iter().collect();
let text = crate::io::step::build_step(&mesh_refs)
.ok_or_else(|| "no mesh data to export".to_string())?;
std::fs::write(&worker_path, text.as_bytes()).map_err(|e| e.to_string())
},
None => self
.command_line
.push_error("STEPOUT: no mesh data to export."),
}
Task::none()
move |result| Message::StepExportFinished(path, result),)
}
pub(super) fn on_obj_import_path_some(&mut self, path: std::path::PathBuf) -> Task<Message> {
let src = match std::fs::read_to_string(&path) {
Ok(s) => s,
Err(e) => {
self.command_line
.push_error(&format!("IMPORTOBJ: read error: {e}"));
return Task::none();
}
};
let color = [0.7f32, 0.7, 0.85, 1.0];
match crate::io::obj::parse_obj(&src, color) {
None => {
self.command_line
.push_error("IMPORTOBJ: no usable geometry in file.");
}
Some(mut mesh) => {
let i = self.active_tab;
let file_stem = path
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "obj_mesh".into());
mesh.name = file_stem.clone();
self.push_undo_snapshot(i, "IMPORTOBJ");
use crate::modules::insert::solid3d_cmds::empty_solid3d;
let entity = empty_solid3d();
let handle = self.tabs[i].scene.add_entity(entity);
if !handle.is_null() {
self.tabs[i]
.scene
.meshes
.insert(handle, crate::scene::MeshLodSet::from_single(mesh));
self.tabs[i].dirty = true;
self.command_line.push_output(&format!(
"IMPORTOBJ: imported \"{}\" as mesh.",
file_stem
));
}
}
}
Task::none()
let tab_id = self.tabs[self.active_tab].id;
let worker_path = path.clone();
background_task(
move || {
let src = std::fs::read_to_string(&worker_path).map_err(|e| e.to_string())?; crate::io::obj::parse_obj(&src, [0.7, 0.7, 0.85, 1.0])
.ok_or_else(|| "no usable geometry in file".to_string())
},
move |result| Message::ObjImportFinished(tab_id, path, result),)
}
#[cfg(not(target_arch = "wasm32"))]
@ -770,6 +724,19 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
self.sync_truck_solids_to_acis(i);
}
#[cfg(target_arch = "wasm32")]
fn stamp_thumbnail(&mut self, i: usize, version: acadrust::DxfVersion) {
let scene = &self.tabs[i].scene;
let preview = crate::io::thumbnail::from_snapshot(
&scene.entity_wires(),
&scene.camera.borrow(),
scene.bg_color,
version >= acadrust::DxfVersion::AC1027,
self.vp_size,
);
self.tabs[i].scene.document.preview = preview;
}
#[cfg(not(target_arch = "wasm32"))]
pub(in crate::app) fn queue_native_save(
&mut self,
@ -1484,7 +1451,11 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
_ => (paper_w, paper_h),
};
match crate::io::pdf_export::export_pdf(
let plot_style = self.active_plot_style.clone();
let worker_path = path.clone();
background_task(
move || {
crate::io::pdf_export::export_pdf(
&wires,
hatches.as_slice(),
wipeouts.as_slice(),
@ -1495,23 +1466,20 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
rotation_deg,
1.0,
None,
&path,
self.active_plot_style.as_ref(),
) {
// Full path, not just the file name — when the export was
// driven by EXPORTPDF <path> the user needs to see where
// the file actually landed. (#369)
Ok(()) => self
.command_line
.push_info(&format!("Exported: {}", path.display())),
Err(e) => self.command_line.push_error(&format!("Export failed: {e}")),
}
Task::none()
&worker_path,
plot_style.as_ref(),
)
.map(|_| format!("Exported: {}", worker_path.display()))
.map_err(|e| format!("Export failed: {e}"))
},
|result| Message::BackgroundIoFinished(result, false),
)
}
/// Export the pending model-space plot window (set by PLOTWINDOW while on
/// the Model tab) to PDF, using the chosen paper size/orientation/scale.
pub(super) fn on_plot_window_export_path_some(&mut self, path: std::path::PathBuf) -> Task<Message> {
pub(super) fn on_plot_window_export_path_some(&mut self, path: std::path::PathBuf,
) -> Task<Message> {
use crate::io::paper_sizes::{sheet_mm, window_to_sheet, PlotScale};
let i = self.active_tab;
if self.tabs[i].scene.current_layout != "Model" {
@ -1569,7 +1537,12 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
// same as the wires) so the final sheet-mm rect lands at (ox, oy).
let clip = Some(((ox / scale) as f32, (oy / scale) as f32, win_w as f32, win_h as f32));
let res = crate::io::pdf_export::export_pdf(
let plot_style = self.active_plot_style.clone();
let worker_path = path.clone();
self.close_active_modal();
background_task(
move || {
crate::io::pdf_export::export_pdf(
&wires,
hatches.as_slice(),
wipeouts.as_slice(),
@ -1580,20 +1553,18 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
0,
scale as f32,
clip,
&path,
self.active_plot_style.as_ref(),
);
match res {
Ok(()) => {
self.command_line.push_info(&format!(
&worker_path,
plot_style.as_ref(),
)
.map(|_| {format!(
"Plotted window to {}",
path.file_name().unwrap_or_default().to_string_lossy()
));
self.close_active_modal();
}
Err(e) => self.command_line.push_error(&format!("Plot failed: {e}")),
}
Task::none()
worker_path
.file_name().unwrap_or_default().to_string_lossy()
)
}).map_err(|e| format!("Plot failed: {e}"))
},
|result| Message::BackgroundIoFinished(result, false),
)
}
/// Build the render inputs and page geometry for a full-layout plot: wires
@ -1678,12 +1649,12 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
self.layout_plot_params();
let plot_style = self.active_plot_style.clone();
self.command_line.push_info("Sending to system printer…");
Task::perform(
async move {
background_task(
move || {
iced::futures::executor::block_on(
crate::io::print_to_printer::print_wires(
wires, hatches, wipeouts, eff_w, eff_h, ox, oy, rotation_deg, plot_style,
)
.await
))
},
Message::PrintResult,
)
@ -2220,16 +2191,16 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
return Task::none();
};
let tmp = std::env::temp_dir().join("open_cad_studio_preview.pdf");
let exp = crate::io::pdf_export::export_pdf(
return background_task(
move || {
crate::io::pdf_export::export_pdf(
&w_wires, &w_hatches, &w_wipeouts, sw, sh, wox, woy, 0, wscale, wclip, &tmp,
plot_style.as_ref(),
).and_then(|_| crate::io::print_to_printer::open_in_viewer(&tmp))
.map(|_| "Opened plot preview.".to_string()).map_err(|e| format!("Preview failed: {e}"))
},
|result| Message::BackgroundIoFinished(result, true),
);
match exp.and_then(|_| crate::io::print_to_printer::open_in_viewer(&tmp)) {
Ok(()) => self.command_line.push_info("Opened plot preview."),
Err(e) => self.command_line.push_error(&format!("Preview failed: {e}")),
}
self.active_modal = Some(crate::app::ModalKind::Plot);
return Task::none();
}
if d.to_file {
// Tested clipped export (opens a save dialog).
@ -2244,25 +2215,27 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
return Task::none();
};
let tmp = std::env::temp_dir().join("open_cad_studio_print.pdf");
let exp = crate::io::pdf_export::export_pdf(
let opts = self.plot_print_options(&d);
return background_task(
move || {
crate::io::pdf_export::export_pdf(
&w_wires, &w_hatches, &w_wipeouts, sw, sh, wox, woy, 0, wscale, wclip, &tmp,
plot_style.as_ref(),
).and_then(|_| crate::io::print_to_printer::print_existing_pdf(&tmp, &opts))
.map(|printer| format!("Sent to printer: {printer}"))
.map_err(|e| format!("Print failed: {e}"))
},
|result| Message::BackgroundIoFinished(result, false),
);
let opts = self.plot_print_options(&d);
match exp.and_then(|_| crate::io::print_to_printer::print_existing_pdf(&tmp, &opts)) {
Ok(printer) => self
.command_line
.push_info(&format!("Sent to printer: {printer}")),
Err(e) => self.command_line.push_error(&format!("Print failed: {e}")),
}
return Task::none();
}
let (wires, hatches, wipeouts, eff_w, eff_h, ox, oy, rot) = self.layout_plot_params();
if preview {
let tmp = std::env::temp_dir().join("open_cad_studio_preview.pdf");
let res = crate::io::pdf_export::export_pdf(
return background_task(
move || {
crate::io::pdf_export::export_pdf(
&wires,
&hatches,
&wipeouts,
@ -2275,14 +2248,11 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
None,
&tmp,
plot_style.as_ref(),
).and_then(|_| crate::io::print_to_printer::open_in_viewer(&tmp))
.map(|_| "Opened plot preview.".to_string()).map_err(|e| format!("Preview failed: {e}"))
},
|result| Message::BackgroundIoFinished(result, true),
);
match res.and_then(|_| crate::io::print_to_printer::open_in_viewer(&tmp)) {
Ok(()) => self.command_line.push_info("Opened plot preview."),
Err(e) => self.command_line.push_error(&format!("Preview failed: {e}")),
}
// Preview leaves the dialog open for further tweaks.
self.active_modal = Some(crate::app::ModalKind::Plot);
return Task::none();
}
if d.to_file {
@ -2292,12 +2262,12 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
let opts = self.plot_print_options(&d);
self.command_line.push_info("Sending to system printer…");
Task::perform(
async move {
background_task(
move || {
iced::futures::executor::block_on(
crate::io::print_to_printer::print_wires_with(
wires, hatches, wipeouts, eff_w, eff_h, ox, oy, rot, plot_style, opts,
)
.await
))
},
Message::PrintResult,
)

View file

@ -449,20 +449,26 @@ impl OpenCADStudio {
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "unknown".into());
let phase = std::sync::Arc::new(std::sync::atomic::AtomicU8::new(
let progress = std::sync::Arc::new(crate::io::OpenProgressState::new(
super::OPEN_PHASE_READING,
));
self.opening = Some(super::OpenProgress {
name: name.clone(),
size_bytes,
phase: phase.clone(),
state: progress.clone(),
started: Instant::now(),
});
let size_label = format_size(size_bytes);
self.command_line
.push_info(&format!("Opening \"{name}\" ({size_label})…"));
let model_bg = self.default_bg_color.unwrap_or([
33.0 / 255.0,
40.0 / 255.0,
48.0 / 255.0,
1.0,
]);
Task::perform(
crate::io::open_path_with_phase(path, phase),
crate::io::open_path_with_phase(path, progress, model_bg),
Message::FileOpened,
)
}
@ -579,6 +585,19 @@ impl OpenCADStudio {
Message::WblockSaveResult(_, None) => Task::none(),
Message::WblockWriteFinished(block_name, path, result) => {
match result {
Ok(()) => self.command_line.push_output(&format!(
"WBLOCK Saved \"{block_name}\"\"{}\"",
path.display()
)),
Err(error) => self
.command_line
.push_error(&format!("WBLOCK save failed: {error}")),
}
Task::none()
}
Message::DataExtractionSave(csv) => {
let csv_clone = csv.clone();
Task::perform(
@ -643,6 +662,16 @@ impl OpenCADStudio {
Message::StlExportPath(None) => Task::none(),
Message::StlExportFinished(path, result) => {
match result {
Ok(()) => self
.command_line
.push_output(&format!("STLOUT: exported to \"{}\"", path.display())),
Err(error) => self.command_line.push_error(&format!("STLOUT: {error}")),
}
Task::none()
}
// ── STEP AP203 export ─────────────────────────────────────────
Message::StepExport => {
let i = self.active_tab;
@ -670,6 +699,16 @@ impl OpenCADStudio {
Message::StepExportPath(None) => Task::none(),
Message::StepExportFinished(path, result) => {
match result {
Ok(()) => self
.command_line
.push_output(&format!("STEPOUT: exported to \"{}\"", path.display())),
Err(error) => self.command_line.push_error(&format!("STEPOUT: {error}")),
}
Task::none()
}
// ── OBJ import ────────────────────────────────────────────────
Message::ObjImport => Task::perform(
async {
@ -688,6 +727,38 @@ impl OpenCADStudio {
Message::ObjImportPath(None) => Task::none(),
Message::ObjImportFinished(tab_id, path, result) => {
match result {
Err(error) => self.command_line.push_error(&format!("IMPORTOBJ: {error}")),
Ok(mut mesh) => {
let Some(i) = self.tabs.iter().position(|tab| tab.id == tab_id) else {
self.command_line
.push_info("IMPORTOBJ: target drawing was closed.");
return Task::none();
};
let file_stem = path
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "obj_mesh".into());
mesh.name = file_stem.clone();
self.push_undo_snapshot(i, "IMPORTOBJ");
let entity = crate::modules::insert::solid3d_cmds::empty_solid3d();
let handle = self.tabs[i].scene.add_entity(entity);
if !handle.is_null() {
self.tabs[i]
.scene
.meshes
.insert(handle, crate::scene::MeshLodSet::from_single(mesh));
self.tabs[i].dirty = true;
self.command_line.push_output(&format!(
"IMPORTOBJ: imported \"{file_stem}\" as mesh."
));
}
}
}
Task::none()
}
Message::SaveFile => self.on_save_file(),
Message::SaveAs => {
@ -1132,7 +1203,7 @@ impl OpenCADStudio {
let targets = self.layer_row_action_targets(i, idx);
if let Some(on) = on {
if !targets.is_empty() {
self.push_undo_snapshot(i, "LAYER OFF/ON");
let undo = self.begin_layer_undo(i, "LAYER OFF/ON", &targets);
for name in &targets {
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
dl.flags.off = !on;
@ -1143,8 +1214,9 @@ impl OpenCADStudio {
pl.visible = on;
}
}
self.tabs[i].scene.bump_geometry();
self.tabs[i].scene.invalidate_layer_dependencies(&targets);
self.tabs[i].dirty = true;
self.commit_layer_undo(i, undo);
self.command_line.push_output(&format!(
"{} layer(s) turned {}",
targets.len(),
@ -1171,7 +1243,7 @@ impl OpenCADStudio {
let targets = self.layer_row_action_targets(i, idx);
if let Some(locked) = locked {
if !targets.is_empty() {
self.push_undo_snapshot(i, "LAYER LOCK/UNLOCK");
let undo = self.begin_layer_undo(i, "LAYER LOCK/UNLOCK", &targets);
for name in &targets {
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
dl.flags.locked = locked;
@ -1182,8 +1254,9 @@ impl OpenCADStudio {
pl.locked = locked;
}
}
self.tabs[i].scene.bump_geometry();
// Lock state affects editability, not rendered geometry.
self.tabs[i].dirty = true;
self.commit_layer_undo(i, undo);
self.command_line.push_output(&format!(
"{} layer(s) {}",
targets.len(),
@ -1201,7 +1274,7 @@ impl OpenCADStudio {
let targets = self.layer_row_action_targets(i, idx);
if let Some(frozen) = frozen {
if !targets.is_empty() {
self.push_undo_snapshot(i, "LAYER FREEZE");
let undo = self.begin_layer_undo(i, "LAYER FREEZE", &targets);
for name in &targets {
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
if frozen {
@ -1216,8 +1289,9 @@ impl OpenCADStudio {
pl.frozen = frozen;
}
}
self.tabs[i].scene.bump_geometry();
self.tabs[i].scene.invalidate_layer_dependencies(&targets);
self.tabs[i].dirty = true;
self.commit_layer_undo(i, undo);
self.command_line.push_output(&format!(
"{} layer(s) {}",
targets.len(),
@ -1321,6 +1395,7 @@ impl OpenCADStudio {
// Apply to every selected layer (multi-select), not just one.
let names = self.selected_layer_names(i);
if !names.is_empty() {
let undo = self.begin_layer_undo(i, "LAYER COLOR", &names);
use crate::ui::window::layers::iced_color_from_acad;
let new_color = iced_color_from_acad(&AcadColor::Index(aci));
for name in &names {
@ -1334,10 +1409,11 @@ impl OpenCADStudio {
}
}
self.tabs[i].dirty = true;
self.commit_layer_undo(i, undo);
// ByLayer color is baked into the cached wires at
// tessellation time, so bump the geometry epoch to
// invalidate the wire cache and repaint with the new color.
self.tabs[i].scene.bump_geometry();
self.tabs[i].scene.invalidate_layer_dependencies(&names);
self.tabs[i].layers.color_picker_row = None;
self.tabs[i].layers.color_full_palette = false;
self.sync_ribbon_layers();
@ -1349,6 +1425,7 @@ impl OpenCADStudio {
let i = self.active_tab;
let names = self.selected_layer_names(i);
if !names.is_empty() {
let undo = self.begin_layer_undo(i, "LAYER LINETYPE", &names);
for name in &names {
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
dl.line_type = lt.clone();
@ -1360,8 +1437,9 @@ impl OpenCADStudio {
}
}
self.tabs[i].dirty = true;
self.commit_layer_undo(i, undo);
// Linetype is baked into the cached wires; repaint.
self.tabs[i].scene.bump_geometry();
self.tabs[i].scene.invalidate_layer_dependencies(&names);
}
Task::none()
}
@ -1370,6 +1448,7 @@ impl OpenCADStudio {
let i = self.active_tab;
let names = self.selected_layer_names(i);
if !names.is_empty() {
let undo = self.begin_layer_undo(i, "LAYER LINEWEIGHT", &names);
for name in &names {
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
dl.line_weight = lw;
@ -1381,8 +1460,9 @@ impl OpenCADStudio {
}
}
self.tabs[i].dirty = true;
self.commit_layer_undo(i, undo);
// Lineweight is baked into the cached wires; repaint.
self.tabs[i].scene.bump_geometry();
self.tabs[i].scene.invalidate_layer_dependencies(&names);
}
Task::none()
}
@ -3984,6 +4064,17 @@ impl OpenCADStudio {
}
Message::PlotWindowExportPath(Some(path)) => self.on_plot_window_export_path_some(path),
Message::BackgroundIoFinished(result, reopen_plot) => {
match result {
Ok(message) => self.command_line.push_info(&message),
Err(error) => self.command_line.push_error(&error),
}
if reopen_plot {
self.active_modal = Some(crate::app::ModalKind::Plot);
}
Task::none()
}
// ── Print to system printer ───────────────────────────────────────
Message::PrintToPrinter => self.on_print_to_printer(),
Message::PrintResult(Ok(printer)) => {

View file

@ -18,20 +18,58 @@ pub mod patterns;
pub mod update_check;
pub mod paper_sizes;
pub mod thumbnail;
#[cfg(target_arch = "wasm32")]
mod web_worker;
use crate::scene::DerivedCaches;
use acadrust::entities::EntityType;
use acadrust::io::dwg::DwgReader;
use acadrust::{CadDocument, DwgWriter, DxfReader, DxfWriter};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::atomic::{AtomicU16, AtomicU32, AtomicU8, Ordering};
use std::sync::Arc;
// Phase tags written into the shared atomic so the UI overlay can display a
// human-readable label. Kept in sync with the constants in `crate::app`.
const PHASE_PARSING: u8 = 1;
const PHASE_CACHING: u8 = 2;
const PHASE_FINALIZING: u8 = 3;
/// Thread-safe state shared by the native loader and the open overlay.
///
/// `basis_points` is monotonic in 0..=10000. `completed/total` describe the
/// current sub-stage and let diagnostics distinguish real progress from a
/// cosmetic timer.
#[derive(Debug)]
pub struct OpenProgressState {
pub phase: AtomicU8,
pub basis_points: AtomicU16,
pub completed: AtomicU32,
pub total: AtomicU32,
}
impl OpenProgressState {
pub fn new(phase: u8) -> Self {
Self {
phase: AtomicU8::new(phase),
basis_points: AtomicU16::new(0),
completed: AtomicU32::new(0),
total: AtomicU32::new(1),
}
}
pub fn set(&self, phase: u8, basis_points: u16, completed: usize, total: usize) {
self.completed
.store(completed.min(u32::MAX as usize) as u32, Ordering::Relaxed);
self.total.store(
total.max(1).min(u32::MAX as usize) as u32,
Ordering::Relaxed,
);
self.basis_points
.fetch_max(basis_points.min(10000), Ordering::Relaxed);
self.phase.store(phase, Ordering::Release);
}
pub fn set_fraction(&self, phase: u8, base: u16, span: u16, completed: usize, total: usize) {
let denominator = total.max(1) as u64;
let value = base as u64 + (completed.min(total.max(1)) as u64 * span as u64 / denominator);
self.set(phase, value.min(10000) as u16, completed, total);
}
}
// ── Open ──────────────────────────────────────────────────────────────────
@ -61,37 +99,103 @@ pub async fn pick_open_path() -> Option<(PathBuf, u64)> {
/// thread runs.
pub async fn open_path_with_phase(
path: PathBuf,
phase: Arc<AtomicU8>,
progress: Arc<OpenProgressState>,
model_bg: [f32; 4],
) -> Result<(String, PathBuf, CadDocument, DerivedCaches), String> {
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "unknown".into());
let path2 = path.clone();
let phase2 = phase.clone();
let (doc, caches) = std::thread::spawn(move || -> Result<_, String> {
let progress2 = progress.clone();
let (sender, receiver) = iced::futures::channel::oneshot::channel();
std::thread::Builder::new()
.name("ocs-file-open".to_string())
.spawn(move || {
let result = (|| -> Result<_, String> {
use iced::time::Instant;
phase2.store(PHASE_PARSING, Ordering::Relaxed);
progress2.set(crate::app::OPEN_PHASE_PARSING, 200, 0, 1000);
let t_parse = Instant::now();
let mut doc = load_file(&path2)?;
let parser_progress = {
let progress = Arc::clone(&progress2);
let callback: Arc<dyn Fn(u16) + Send + Sync> = Arc::new(move |value| {
progress.set_fraction(
crate::app::OPEN_PHASE_PARSING,
200,
5600,
value as usize,
1000,
);
});
callback
};
let mut doc = load_file_with_progress(&path2, Some(parser_progress))?;
let parse_ms = t_parse.elapsed().as_millis() as u32;
progress2.set(crate::app::OPEN_PHASE_PARSING, 5800, 1000, 1000);
let t_purge = Instant::now();
let dropped = purge_corrupt_entities(&mut doc);
let purge_ms = t_purge.elapsed().as_millis() as u32;
phase2.store(PHASE_CACHING, Ordering::Relaxed);
progress2.set(crate::app::OPEN_PHASE_XREF, 6000, 0, 1);
let t_xref = Instant::now();
let (xref_infos, xref_dropped) = if let Some(base_dir) = path2.parent() {
let xref_progress = {
let progress = Arc::clone(&progress2);
let callback: Arc<dyn Fn(usize, usize) + Send + Sync> =
Arc::new(move |completed, total| {
progress.set_fraction(
crate::app::OPEN_PHASE_XREF,
6000,
1400,
completed,
total,
);
});
callback
};
crate::io::xref::resolve_xrefs_with_progress(
&mut doc,
base_dir,
Some(xref_progress),
)
} else {
(Vec::new(), 0)
};
let xref_ms = t_xref.elapsed().as_millis() as u32;
progress2.set(crate::app::OPEN_PHASE_CACHING, 7400, 0, 10000);
let t_caches = Instant::now();
let mut caches = crate::scene::build_derived_caches(&doc);
let cache_progress = |value: u16| {
progress2.set_fraction(
crate::app::OPEN_PHASE_CACHING,
7400,
2200,
value as usize,
10000,
);
};
let mut caches = crate::scene::build_derived_caches_with_progress(&doc, &cache_progress);
caches.timings = crate::scene::OpenTimings {
parse_ms,
purge_ms,
caches_ms: t_caches.elapsed().as_millis() as u32,
xref_ms,
};
caches.corrupt_dropped = dropped;
phase2.store(PHASE_FINALIZING, Ordering::Relaxed);
caches.xref_dropped = xref_dropped;
caches.xrefs = xref_infos;
progress2.set(crate::app::OPEN_PHASE_FINALIZING, 9600, 0, 1);
let (prepared_doc, prepared_geometry) =
crate::scene::prepare_open_geometry(doc, &caches, model_bg);
doc = prepared_doc;
caches.prepared_geometry = Some(prepared_geometry);
progress2.set(crate::app::OPEN_PHASE_FINALIZING, 9950, 1, 1);
Ok((doc, caches))
})();
let _ = sender.send(result);
})
.join()
.map_err(|_| "parser thread panicked".to_string())??;
.map_err(|error| format!("failed to start parser thread: {error}"))?;
let (doc, caches) = receiver
.await
.map_err(|_| "parser thread stopped without a result".to_string())??;
Ok((name, path, doc, caches))
}
@ -102,6 +206,7 @@ pub async fn open_path_with_phase(
/// stands in for the document path.
#[cfg(target_arch = "wasm32")]
pub async fn pick_and_load_web(
progress: Arc<OpenProgressState>,
) -> Result<(String, PathBuf, CadDocument, DerivedCaches), String> {
let handle = crate::sys::file_dialog()
.set_title("Open CAD file")
@ -111,11 +216,23 @@ pub async fn pick_and_load_web(
.await
.ok_or_else(|| "Cancelled".to_string())?;
let name = handle.file_name();
progress.set(crate::app::OPEN_PHASE_READING, 500, 1, 2);
let bytes = handle.read().await;
let mut doc = load_bytes(&name, bytes)?;
progress.set(crate::app::OPEN_PHASE_PARSING, 1000, 0, 1);
let mut doc = match web_worker::parse_document(&name, bytes).await {
Ok(document) => document,
Err(error) => return Err(format!("Web parser worker: {error}")),
};
if name.to_ascii_lowercase().ends_with(".dxf") {
fix_dxf_dimension_rotations(&mut doc);
}
fix_viewport_status_flags(&mut doc);
fix_current_style_names(&mut doc);
progress.set(crate::app::OPEN_PHASE_CACHING, 7000, 0, 1);
let dropped = purge_corrupt_entities(&mut doc);
let mut caches = crate::scene::build_derived_caches(&doc);
caches.corrupt_dropped = dropped;
progress.set(crate::app::OPEN_PHASE_FINALIZING, 9900, 1, 1);
let path = PathBuf::from(&name);
Ok((name, path, doc, caches))
}
@ -168,6 +285,13 @@ fn sniff_dwg_or_dxf(path: &Path) -> String {
}
pub fn load_file(path: &Path) -> Result<CadDocument, String> {
load_file_with_progress(path, None)
}
pub(crate) fn load_file_with_progress(
path: &Path,
_progress: Option<Arc<dyn Fn(u16) + Send + Sync>>,
) -> Result<CadDocument, String> {
let ext = path
.extension()
.map(|e| e.to_string_lossy().to_lowercase())
@ -184,10 +308,15 @@ pub fn load_file(path: &Path) -> Result<CadDocument, String> {
match effective.as_str() {
"dwg" => {
#[cfg(not(target_arch = "wasm32"))]
let mut doc = DwgReader::from_mmap(path)
.map_err(|e| e.to_string())?
.read()
let mut doc = {
let mut reader = DwgReader::from_mmap(path)
.map_err(|e| e.to_string())?;
if let Some(progress) = _progress {
reader.set_progress_callback(progress);
}
reader.read()
.map_err(|e| e.to_string())?
};
#[cfg(target_arch = "wasm32")]
let mut doc = DwgReader::from_file(path)
.map_err(|e| e.to_string())?

77
src/io/web_worker.rs Normal file
View file

@ -0,0 +1,77 @@
use std::cell::RefCell;
use std::rc::Rc;
use acadrust::CadDocument;
use js_sys::{Array, Object, Reflect, Uint8Array};
use wasm_bindgen::closure::Closure;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{ErrorEvent, MessageEvent, Worker, WorkerOptions, WorkerType};
pub(super) async fn parse_document(name: &str, bytes: Vec<u8>) -> Result<CadDocument, String> {
let options = WorkerOptions::new();
options.set_type(WorkerType::Module);
let worker = Worker::new_with_options("ocs-parse-worker.js", &options).map_err(js_error)?;
let (sender, receiver) = iced::futures::channel::oneshot::channel();
let sender = Rc::new(RefCell::new(Some(sender)));
let message_sender = sender.clone();
let on_message = Closure::<dyn FnMut(MessageEvent)>::new(move |event: MessageEvent| {
let data = event.data();
let ok = Reflect::get(&data, &JsValue::from_str("ok"))
.ok()
.and_then(|value| value.as_bool())
.unwrap_or(false);
let result = if ok {
Reflect::get(&data, &JsValue::from_str("data"))
.map_err(js_error)
.and_then(|value| {
let bytes = Uint8Array::new(&value).to_vec();
bincode::deserialize(&bytes).map_err(|error| error.to_string())
})
} else {
Err(Reflect::get(&data, &JsValue::from_str("error"))
.ok()
.and_then(|value| value.as_string())
.unwrap_or_else(|| "CAD parser worker failed".to_string()))
};
if let Some(sender) = message_sender.borrow_mut().take() {
let _ = sender.send(result);
}
});
worker.set_onmessage(Some(on_message.as_ref().unchecked_ref()));
let error_sender = sender;
let on_error = Closure::<dyn FnMut(ErrorEvent)>::new(move |event: ErrorEvent| {
if let Some(sender) = error_sender.borrow_mut().take() {
let _ = sender.send(Err(event.message()));
}
});
worker.set_onerror(Some(on_error.as_ref().unchecked_ref()));
let payload = Object::new();
Reflect::set(
&payload,
&JsValue::from_str("name"),
&JsValue::from_str(name),
)
.map_err(js_error)?;
let input = Uint8Array::from(bytes.as_slice());
Reflect::set(&payload, &JsValue::from_str("bytes"), &input.buffer()).map_err(js_error)?;
let transfer = Array::new();
transfer.push(&input.buffer());
worker
.post_message_with_transfer(&payload, &transfer)
.map_err(js_error)?;
let result = receiver
.await
.map_err(|_| "CAD parser worker closed without a result".to_string())?;
worker.terminate();
result
}
fn js_error(value: JsValue) -> String {
value
.as_string()
.unwrap_or_else(|| format!("browser worker error: {value:?}"))
}

View file

@ -41,6 +41,19 @@ pub struct XrefInfo {
/// just like the host doc, so it gets the same corrupt-entity guard. Folding
/// it in here avoids a second full-document `entities()` walk after resolve.
pub fn resolve_xrefs(doc: &mut CadDocument, base_dir: &Path) -> (Vec<XrefInfo>, usize) {
resolve_xrefs_with_progress(doc, base_dir, None)
}
/// Resolve XREFs while reporting completed work units.
///
/// Each reference contributes 1000 parse units and 1000 merge units. Parsing
/// progress comes directly from the DWG reader when available, so one large
/// XREF advances smoothly instead of making the file-open bar appear frozen.
pub fn resolve_xrefs_with_progress(
doc: &mut CadDocument,
base_dir: &Path,
progress: Option<std::sync::Arc<dyn Fn(usize, usize) + Send + Sync>>,
) -> (Vec<XrefInfo>, usize) {
// Auto-resolve every xref — frustum + LOD culling keep GPU cost bounded.
let xref_entries: Vec<(String, String, Handle)> = doc
.block_records
@ -48,6 +61,11 @@ pub fn resolve_xrefs(doc: &mut CadDocument, base_dir: &Path) -> (Vec<XrefInfo>,
.filter(|br| (br.flags.is_xref || br.flags.is_xref_overlay) && !br.xref_path.is_empty())
.map(|br| (br.name.clone(), br.xref_path.clone(), br.handle))
.collect();
let xref_count = xref_entries.len();
let total_units = xref_count.saturating_mul(2000);
if let Some(progress) = &progress {
progress(0, total_units);
}
// Phase 1 — parse every referenced file in parallel. Each `load_file`
// reads and decodes an independent DWG/DXF and touches nothing in the host
@ -55,11 +73,40 @@ pub fn resolve_xrefs(doc: &mut CadDocument, base_dir: &Path) -> (Vec<XrefInfo>,
// instead of running back-to-back. (The merge in phase 2 mutates `doc`, so
// it stays serial.) `resolve_path` is pure and `base_dir` is shared &-ref.
use crate::par::prelude::*;
let parse_units: std::sync::Arc<Vec<std::sync::atomic::AtomicU16>> = std::sync::Arc::new(
(0..xref_count)
.map(|_| std::sync::atomic::AtomicU16::new(0))
.collect(),
);
let parsed: Vec<(String, String, Handle, Option<PathBuf>, Option<CadDocument>)> = xref_entries
.into_par_iter()
.map(|(block_name, raw_path, br_handle)| {
.enumerate()
.map(|(xref_index, (block_name, raw_path, br_handle))| {
let resolved = resolve_path(&raw_path, base_dir);
let xref_doc = resolved.as_ref().and_then(|p| super::load_file(p).ok());
let units = std::sync::Arc::clone(&parse_units);
let nested_progress = progress.as_ref().map(|progress| {
let progress = std::sync::Arc::clone(progress);
let callback: std::sync::Arc<dyn Fn(u16) + Send + Sync> =
std::sync::Arc::new(move |value| {
units[xref_index]
.store(value.min(1000), std::sync::atomic::Ordering::Relaxed);
let completed = units
.iter()
.map(|unit| unit.load(std::sync::atomic::Ordering::Relaxed) as usize)
.sum();
progress(completed, total_units);
});
callback
});
let xref_doc = resolved.as_ref().and_then(|p| super::load_file_with_progress(p, nested_progress).ok());
parse_units[xref_index].store(1000, std::sync::atomic::Ordering::Relaxed);
if let Some(progress) = &progress {
let completed = parse_units
.iter()
.map(|unit| unit.load(std::sync::atomic::Ordering::Relaxed) as usize)
.sum();
progress(completed, total_units);
}
(block_name, raw_path, br_handle, resolved, xref_doc)
})
.collect();
@ -68,7 +115,8 @@ pub fn resolve_xrefs(doc: &mut CadDocument, base_dir: &Path) -> (Vec<XrefInfo>,
// block order (par_iter preserves it), so handle allocation is deterministic.
let mut result = Vec::with_capacity(parsed.len());
let mut dropped = 0usize;
for (block_name, raw_path, br_handle, resolved, xref_doc) in parsed {
for (merge_index, (block_name, raw_path, br_handle, resolved, xref_doc)) in parsed.into_iter().enumerate()
{
let status = if let Some(xref_doc) = xref_doc {
ensure_block_entities(doc, &block_name);
dropped += merge_xref_into_block(doc, &block_name, br_handle, xref_doc);
@ -85,6 +133,14 @@ pub fn resolve_xrefs(doc: &mut CadDocument, base_dir: &Path) -> (Vec<XrefInfo>,
.unwrap_or(raw_path),
status,
});
if let Some(progress) = &progress {
progress(
xref_count
.saturating_mul(1000)
.saturating_add((merge_index + 1).saturating_mul(1000)),
total_units,
);
}
}
(result, dropped)

View file

@ -0,0 +1,99 @@
//! Shared broad phase for interactive modify commands.
//!
//! The scene already narrows the cursor pick. Commands still need fast access
//! to the picked analytic entity and, for intersection-heavy operations such
//! as TRIM, to nearby boundary entities. Keeping this compact command-local
//! index avoids cloning/scanning the complete drawing on every mouse move.
use acadrust::{EntityType, Handle};
use rustc_hash::{FxHashMap, FxHashSet};
use crate::scene::convert::tess::entity_world_aabb_f64;
use crate::scene::pick::quadtree::QuadTree;
pub(super) struct ModifyEntityIndex {
by_handle: FxHashMap<Handle, usize>,
tree: Option<QuadTree>,
unbounded: Vec<Handle>,
}
impl ModifyEntityIndex {
pub(super) fn build(entities: &[EntityType]) -> Self {
let mut by_handle = FxHashMap::default();
let mut bounded = Vec::new();
let mut unbounded = Vec::new();
let mut world = [
f64::INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NEG_INFINITY,
];
for (index, entity) in entities.iter().enumerate() {
let handle = entity.common().handle;
by_handle.insert(handle, index);
if let Some(aabb) = entity_world_aabb_f64(entity) {
world[0] = world[0].min(aabb[0]);
world[1] = world[1].min(aabb[1]);
world[2] = world[2].max(aabb[2]);
world[3] = world[3].max(aabb[3]);
bounded.push((handle, aabb));
} else {
unbounded.push(handle);
}
}
let tree = if bounded.is_empty() {
None
} else {
let span = (world[2] - world[0]).max(world[3] - world[1]).max(1.0);
let pad = span * 1.0e-9 + 1.0e-6;
let mut tree = QuadTree::new([
world[0] - pad,
world[1] - pad,
world[2] + pad,
world[3] + pad,
]);
for (handle, aabb) in bounded {
tree.insert(handle, aabb);
}
Some(tree)
};
Self {
by_handle,
tree,
unbounded,
}
}
#[inline]
pub(super) fn get<'a>(
&self,
entities: &'a [EntityType],
handle: Handle,
) -> Option<&'a EntityType> {
self.by_handle
.get(&handle)
.and_then(|index| entities.get(*index))
}
pub(super) fn nearby_handles(
&self,
entities: &[EntityType],
handle: Handle,
) -> Option<FxHashSet<Handle>> {
let entity = self.get(entities, handle)?;
let aabb = entity_world_aabb_f64(entity)?;
let span = (aabb[2] - aabb[0]).max(aabb[3] - aabb[1]).max(1.0);
let pad = span * 1.0e-10 + 1.0e-7;
let query = [aabb[0] - pad, aabb[1] - pad, aabb[2] + pad, aabb[3] + pad];
let mut handles: FxHashSet<Handle> = self
.tree
.as_ref()
.map(|tree| tree.query_rect(query).into_iter().collect())
.unwrap_or_default();
handles.extend(self.unbounded.iter().copied());
Some(handles)
}
}

View file

@ -21,6 +21,8 @@ use crate::modules::draw::defaults;
use crate::modules::IconKind;
use crate::scene::model::wire_model::WireModel;
use super::entity_index::ModifyEntityIndex;
// ── Dropdown constants ─────────────────────────────────────────────────────
pub const DROPDOWN_ID: &str = "fillet_chamfer";
@ -1204,6 +1206,7 @@ pub struct FilletCommand {
radius: f64,
step: FilletStep,
all_entities: Vec<EntityType>,
entity_index: ModifyEntityIndex,
/// First-object pick to restore after a radius entry made mid-selection
/// (i.e. "R" pressed after the first object was already picked), so the
/// command resumes at the second pick instead of restarting selection.
@ -1212,10 +1215,12 @@ pub struct FilletCommand {
impl FilletCommand {
pub fn new(radius: f64, all_entities: Vec<EntityType>) -> Self {
let entity_index = ModifyEntityIndex::build(&all_entities);
Self {
radius: radius as f64,
step: FilletStep::First,
all_entities,
entity_index,
resume_second: None,
}
}
@ -1348,9 +1353,8 @@ impl CadCommand for FilletCommand {
FilletStep::WaitingForRadius => return CmdResult::NeedPoint,
FilletStep::First => {
let e1 = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.entity_index
.get(&self.all_entities, handle)
.and_then(|entity| match entity {
EntityType::LwPolyline(p) => {
Some(FilletEntity::from_lwpoly(p, handle, click))
@ -1380,9 +1384,8 @@ impl CadCommand for FilletCommand {
}
let e2 = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.entity_index
.get(&self.all_entities, handle)
.and_then(|entity| match entity {
EntityType::LwPolyline(p) => {
Some(FilletEntity::from_lwpoly(p, handle, click))
@ -1426,9 +1429,8 @@ impl CadCommand for FilletCommand {
FilletStep::WaitingForRadius => vec![],
FilletStep::First => {
let pts = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.entity_index
.get(&self.all_entities, handle)
.and_then(|e| match e {
EntityType::LwPolyline(p) => Some(lwpoly_seg_hover_pts(p, click)),
_ => FilletEntity::from_entity(e).map(|fe| entity_pts(&fe.to_entity_type())),
@ -1449,9 +1451,8 @@ impl CadCommand for FilletCommand {
let e1 = e1.clone();
let click1 = *click1;
let e2 = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.entity_index
.get(&self.all_entities, handle)
.and_then(|entity| match entity {
EntityType::LwPolyline(p) => {
Some(FilletEntity::from_lwpoly(p, handle, click))
@ -1583,6 +1584,7 @@ pub struct ChamferCommand {
dist2: f64,
step: ChamferStep,
all_entities: Vec<EntityType>,
entity_index: ModifyEntityIndex,
/// First-object pick (line or polyline segment) to restore after a
/// distance entry made mid-selection, so the command resumes at the
/// second pick instead of restarting selection.
@ -1591,11 +1593,13 @@ pub struct ChamferCommand {
impl ChamferCommand {
pub fn new(dist: f64, all_entities: Vec<EntityType>) -> Self {
let entity_index = ModifyEntityIndex::build(&all_entities);
Self {
dist1: dist as f64,
dist2: defaults::get_chamfer_dist2(),
step: ChamferStep::First,
all_entities,
entity_index,
resume_pick: None,
}
}
@ -1777,9 +1781,7 @@ impl CadCommand for ChamferCommand {
}
ChamferStep::First => {
match self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.entity_index.get(&self.all_entities, handle)
{
Some(EntityType::Line(l)) => {
self.step = ChamferStep::Second {
@ -1821,9 +1823,8 @@ impl CadCommand for ChamferCommand {
}
let l2 = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.entity_index
.get(&self.all_entities, handle)
.and_then(|e| {
if let EntityType::Line(l) = e {
Some(l.clone())
@ -1857,9 +1858,8 @@ impl CadCommand for ChamferCommand {
ChamferStep::WaitingForDist1 | ChamferStep::WaitingForDist2 => return vec![],
ChamferStep::First => {
let pts = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.entity_index
.get(&self.all_entities, handle)
.and_then(|e| match e {
EntityType::Line(l) => Some(line_pts(l)),
EntityType::LwPolyline(p) => Some(lwpoly_seg_hover_pts(p, click)),
@ -1880,9 +1880,8 @@ impl CadCommand for ChamferCommand {
let l1 = l1.clone();
let click1 = *click1;
let l2 = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.entity_index
.get(&self.all_entities, handle)
.and_then(|e| {
if let EntityType::Line(l) = e {
Some(l.clone())

View file

@ -4,6 +4,7 @@ pub mod attedit;
pub mod break_cmd;
pub mod copy;
pub mod delete;
mod entity_index;
pub mod explode;
pub mod fillet;
pub mod join;

View file

@ -25,6 +25,8 @@ use crate::modules::draw::defaults;
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
use super::entity_index::ModifyEntityIndex;
// ── Ribbon definition ──────────────────────────────────────────────────────
pub fn tool() -> ToolDef {
@ -691,6 +693,7 @@ enum Step {
pub struct OffsetCommand {
step: Step,
all_entities: Vec<EntityType>,
entity_index: ModifyEntityIndex,
/// Pre-selected offsettable objects (pick-first, #422); consumed when the
/// distance step resolves.
preselected: Vec<EntityType>,
@ -712,9 +715,11 @@ pub fn is_offsettable(e: &EntityType) -> bool {
impl OffsetCommand {
pub fn new(all_entities: Vec<EntityType>) -> Self {
let entity_index = ModifyEntityIndex::build(&all_entities);
Self {
step: Step::Distance,
all_entities,
entity_index,
preselected: Vec::new(),
}
}
@ -722,9 +727,11 @@ impl OffsetCommand {
/// Pick-first flow (#422): the distance step still comes first, then the
/// pre-selected objects go straight to the side step.
pub fn with_selection(all_entities: Vec<EntityType>, targets: Vec<EntityType>) -> Self {
let entity_index = ModifyEntityIndex::build(&all_entities);
Self {
step: Step::Distance,
all_entities,
entity_index,
preselected: targets,
}
}
@ -801,9 +808,7 @@ impl CadCommand for OffsetCommand {
}
let entity = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.entity_index.get(&self.all_entities, handle)
.cloned();
// Accept every type compute_offset can offset — including XLine (#296),
@ -881,9 +886,7 @@ impl CadCommand for OffsetCommand {
return vec![];
}
if let Some(entity) = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.entity_index.get(&self.all_entities, handle)
{
let pts = entity_wire_pts(entity);
if !pts.is_empty() {

View file

@ -27,6 +27,8 @@ use crate::modules::draw::modify::spline_ops::{
use crate::modules::IconKind;
use crate::scene::model::wire_model::WireModel;
use super::entity_index::ModifyEntityIndex;
// ── Dropdown constants ─────────────────────────────────────────────────────
pub const DROPDOWN_ID: &str = "trim_extend";
@ -269,6 +271,20 @@ enum Geo {
},
}
impl Geo {
fn handle(&self) -> Handle {
match self {
Self::Line { handle, .. }
| Self::Arc { handle, .. }
| Self::Circle { handle, .. }
| Self::Ray { handle, .. }
| Self::InfLine { handle, .. }
| Self::Ellipse { handle, .. }
| Self::Spline { handle, .. } => *handle,
}
}
}
fn build_geos(entities: &[EntityType]) -> Vec<Geo> {
let mut out = Vec::new();
for e in entities {
@ -2797,6 +2813,7 @@ fn crossing_preview_wire(p1: [f64; 2], cursor: [f64; 2], name: &str) -> WireMode
pub struct TrimCommand {
all_entities: Vec<EntityType>,
entity_index: ModifyEntityIndex,
geos: Vec<Geo>,
mode: TrimMode,
/// Cutting-edge selection; empty = every object cuts (quick mode).
@ -2809,9 +2826,11 @@ pub struct TrimCommand {
impl TrimCommand {
pub fn new(all_entities: Vec<EntityType>) -> Self {
let entity_index = ModifyEntityIndex::build(&all_entities);
let geos = build_geos(&all_entities);
Self {
all_entities,
entity_index,
geos,
mode: TrimMode::Pick,
edge_set: Vec::new(),
@ -2823,6 +2842,7 @@ impl TrimCommand {
/// Boundary geometry from the edge selection (or everything when none),
/// with the Edge option's implied extrapolation applied on top.
fn rebuild_geos(&mut self) {
self.entity_index = ModifyEntityIndex::build(&self.all_entities);
self.geos = if self.edge_set.is_empty() {
build_geos(&self.all_entities)
} else {
@ -2839,6 +2859,23 @@ impl TrimCommand {
}
}
/// Exact analytic boundaries whose boxes overlap the picked entity. The
/// host already narrowed the cursor pick; this second broad phase keeps
/// TRIM preview/intersection work local on dense drawings.
fn nearby_geos(&self, handle: Handle) -> Vec<Geo> {
if self.implied_edges {
return self.geos.clone();
}
let Some(handles) = self.entity_index.nearby_handles(&self.all_entities, handle) else {
return self.geos.clone();
};
self.geos
.iter()
.filter(|geo| handles.contains(&geo.handle()))
.cloned()
.collect()
}
fn fence_run(
&mut self,
fence: &[[f64; 2]],
@ -2974,7 +3011,8 @@ impl CadCommand for TrimCommand {
pick_extend_at(&self.all_entities, &self.geos, handle, px, py)
.map(|e| vec![e])
} else {
pick_trim_at(&self.all_entities, &self.geos, handle, px, py)
let geos = self.nearby_geos(handle);
pick_trim_at(&self.all_entities, &geos, handle, px, py)
};
if let Some(new_entities) = new_entities {
// Snapshot is updated in on_entity_replaced once we know
@ -3108,10 +3146,10 @@ impl CadCommand for TrimCommand {
return vec![];
}
let nearby_geos = self.nearby_geos(handle);
let geos = nearby_geos.as_slice();
let entity = self
.all_entities
.iter()
.find(|e| e.common().handle == handle);
.entity_index.get(&self.all_entities, handle);
let mut hover_wires = match entity {
Some(EntityType::Line(l)) => {
@ -3119,7 +3157,7 @@ impl CadCommand for TrimCommand {
let ay = l.start.y;
let bx = l.end.x;
let by = l.end.y;
let ts = line_seg_ts(ax, ay, bx, by, handle, &self.geos);
let ts = line_seg_ts(ax, ay, bx, by, handle, geos);
if ts.is_empty() {
return vec![];
}
@ -3152,7 +3190,7 @@ impl CadCommand for TrimCommand {
let cy = a.center.y;
let a0 = a.start_angle;
let a1 = a.end_angle;
let ts = arc_seg_ts(cx, cy, a.radius, a0, a1, handle, &self.geos);
let ts = arc_seg_ts(cx, cy, a.radius, a0, a1, handle, geos);
if ts.is_empty() {
return vec![];
}
@ -3176,7 +3214,7 @@ impl CadCommand for TrimCommand {
Some(EntityType::Circle(c)) => {
let cx = c.center.x;
let cy = c.center.y;
let ts = arc_seg_ts(cx, cy, c.radius, 0.0, TAU, handle, &self.geos);
let ts = arc_seg_ts(cx, cy, c.radius, 0.0, TAU, handle, geos);
if ts.len() < 2 {
return vec![];
}
@ -3205,7 +3243,7 @@ impl CadCommand for TrimCommand {
let by = r.base_point.y;
let ex = bx + r.direction.x * TRIM_EXTENT;
let ey = by + r.direction.y * TRIM_EXTENT;
let ts = line_seg_ts(bx, by, ex, ey, handle, &self.geos);
let ts = line_seg_ts(bx, by, ex, ey, handle, geos);
if ts.is_empty() {
return vec![];
}
@ -3245,7 +3283,7 @@ impl CadCommand for TrimCommand {
let ey_start = by - x.direction.y * TRIM_EXTENT;
let ex_end = bx + x.direction.x * TRIM_EXTENT;
let ey_end = by + x.direction.y * TRIM_EXTENT;
let ts = line_seg_ts(ex_start, ey_start, ex_end, ey_end, handle, &self.geos);
let ts = line_seg_ts(ex_start, ey_start, ex_end, ey_end, handle, geos);
if ts.is_empty() {
return vec![];
}
@ -3294,7 +3332,7 @@ impl CadCommand for TrimCommand {
t1 += TAU;
}
let ts = ellipse_seg_ts(
e.center.x, e.center.y, a, b, nx, ny, t0, t1, handle, &self.geos,
e.center.x, e.center.y, a, b, nx, ny, t0, t1, handle, geos,
);
if ts.is_empty() {
return vec![];
@ -3321,7 +3359,7 @@ impl CadCommand for TrimCommand {
out
}
Some(EntityType::Spline(s)) => {
let ts = spline_seg_ts(s, handle, &self.geos);
let ts = spline_seg_ts(s, handle, geos);
if ts.is_empty() {
return vec![];
}
@ -3348,7 +3386,7 @@ impl CadCommand for TrimCommand {
out
}
Some(EntityType::LwPolyline(p)) => {
let Some(survivors) = trim_lwpolyline(p, pt.x as f64, pt.y as f64, &self.geos)
let Some(survivors) = trim_lwpolyline(p, pt.x as f64, pt.y as f64, geos)
else {
return vec![];
};
@ -3371,7 +3409,7 @@ impl CadCommand for TrimCommand {
if self.implied_edges && !hover_wires.is_empty() {
if let (Some(orig), Some(pieces)) = (
entity,
pick_trim_at(&self.all_entities, &self.geos, handle, pt.x, pt.y),
pick_trim_at(&self.all_entities, geos, handle, pt.x, pt.y),
) {
let cuts = piece_cut_points(orig, &pieces);
hover_wires.extend(implied_cut_guides(&self.all_entities, handle, &cuts));
@ -3477,6 +3515,7 @@ impl CadCommand for TrimCommand {
pub struct ExtendCommand {
all_entities: Vec<EntityType>,
entity_index: ModifyEntityIndex,
geos: Vec<Geo>,
mode: TrimMode,
/// Boundary-edge selection; empty = every object is a boundary.
@ -3489,9 +3528,11 @@ pub struct ExtendCommand {
impl ExtendCommand {
pub fn new(all_entities: Vec<EntityType>) -> Self {
let entity_index = ModifyEntityIndex::build(&all_entities);
let geos = build_geos(&all_entities);
Self {
all_entities,
entity_index,
geos,
mode: TrimMode::Pick,
edge_set: Vec::new(),
@ -3501,6 +3542,7 @@ impl ExtendCommand {
}
fn rebuild_geos(&mut self) {
self.entity_index = ModifyEntityIndex::build(&self.all_entities);
self.geos = if self.edge_set.is_empty() {
build_geos(&self.all_entities)
} else {
@ -3681,18 +3723,14 @@ impl CadCommand for ExtendCommand {
let mut out: Vec<WireModel> = self
.edge_set
.iter()
.filter_map(|h| {
self.all_entities.iter().find(|e| e.common().handle == *h)
})
.filter_map(|h| self.entity_index.get(&self.all_entities, *h))
.map(|e| {
WireModel::solid("edge_sel".into(), entity_pts(e), OPT_YELLOW, false)
})
.collect();
if !handle.is_null() && !self.edge_set.contains(&handle) {
if let Some(e) = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.entity_index.get(&self.all_entities, handle)
{
let mut c = OPT_YELLOW;
c[3] = 0.45;
@ -3732,9 +3770,7 @@ impl CadCommand for ExtendCommand {
}
let entity = self
.all_entities
.iter()
.find(|e| e.common().handle == handle);
.entity_index.get(&self.all_entities, handle);
match entity {
Some(EntityType::Line(l)) => {
let ax = l.start.x;

View file

@ -15,7 +15,7 @@
// a visited set so a self-referential block produces a marker rather than
// recursing forever.
use rustc_hash::FxHashMap as HashMap;
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
use std::sync::Arc;
use acadrust::types::{Color as AcadColor, LineWeight, Transform, Vector3};
@ -153,6 +153,29 @@ pub struct BlockDefn {
#[derive(Default, Debug)]
pub struct BlockCache {
defns: HashMap<String, Arc<BlockDefn>>,
prototype_blocks: HashSet<String>,
/// Fully expanded prototype for repeated, non-array inserts. The key omits
/// translation but includes the linear transform and every inherited style
/// input. A matching insert therefore reuses all nested expansion/style
/// work and only applies its translation to the immutable prototype.
expansion_prototypes: std::sync::Mutex<
HashMap<ExpansionPrototypeKey, Arc<std::sync::Mutex<Option<Arc<CachedExpansion>>>>>,
>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct ExpansionPrototypeKey {
block_name: String,
linear: [u64; 9],
insert_style: Vec<u32>,
selected: bool,
is_xref: bool,
}
#[derive(Debug)]
struct CachedExpansion {
translation: [f64; 3],
wires: Arc<Vec<WireModel>>,
}
impl BlockCache {
@ -181,6 +204,18 @@ impl BlockCache {
use crate::par::prelude::*;
let mut cache = Self::new();
let referenced = collect_referenced_blocks(doc);
let mut reference_counts: HashMap<String, usize> = HashMap::default();
for entity in doc.entities() {
if let EntityType::Insert(insert) = entity {
*reference_counts
.entry(insert.block_name.clone())
.or_default() += insert.instance_count();
}
}
cache.prototype_blocks = reference_counts
.into_iter()
.filter_map(|(name, count)| (count > 1).then_some(name))
.collect();
// Each defn is built independently: nested INSERTs are stored as
// by-name references (`LocalSub::Nested`), never expanded here, so a
// block's build never depends on another block's defn. That makes the
@ -269,7 +304,6 @@ impl BlockCache {
/// Walk all entities + all block_record contents collecting every distinct
/// `block_name` that appears in an Insert (transitively).
fn collect_referenced_blocks(doc: &CadDocument) -> Vec<String> {
use rustc_hash::FxHashSet as HashSet;
let mut seen: HashSet<String> = HashSet::default();
let mut queue: Vec<String> = Vec::new();
@ -795,6 +829,61 @@ pub fn expand_insert(
xform = xform.then(&scale_about_p);
}
let name = ins_handle.value().to_string();
let prototype_key = if view_aabb.is_none()
&& world_per_pixel.is_none()
&& !ins.is_array()
&& cache.prototype_blocks.contains(&ins.block_name)
{
Some(expansion_prototype_key(
ins,
&xform,
ins_resolved_color,
ins_pat_len,
ins_pat,
ins_lw_px,
ins_layer,
selected,
pslt_factor,
is_xref,
bg_color,
anno_scale,
))
} else {
None
};
let prototype_slot = prototype_key.as_ref().map(|key| {
let mut prototypes = cache
.expansion_prototypes
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
Arc::clone(
prototypes
.entry(key.clone())
.or_insert_with(|| Arc::new(std::sync::Mutex::new(None))),
)
});
let mut prototype_guard = prototype_slot
.as_ref()
.map(|slot| slot.lock().unwrap_or_else(|poisoned| poisoned.into_inner()));
if let Some(cached) = prototype_guard
.as_ref()
.and_then(|guard| guard.as_ref())
.cloned()
{
let translation = transform_translation(&xform);
let delta = [
translation[0] - cached.translation[0],
translation[1] - cached.translation[1],
translation[2] - cached.translation[2],
];
return Some(
cached
.wires
.iter()
.map(|wire| translated_prototype_wire(wire, &name, delta))
.collect(),
);
}
let mut batches = Batches::default();
let mut visited: Vec<String> = Vec::with_capacity(8);
@ -848,7 +937,135 @@ pub fn expand_insert(
};
expand_defn(defn, &base_xform, &ctx, &mut batches, &mut visited, 0, (0.0, 1.0));
}
Some(batches.finalize(&name, selected, bg_color))
let wires = batches.finalize(&name, selected, bg_color);
if let Some(guard) = prototype_guard.as_mut() {
let cached = Arc::new(CachedExpansion {
translation: transform_translation(&xform),
wires: Arc::new(wires.clone()),
});
**guard = Some(cached);
}
Some(wires)
}
fn transform_translation(transform: &Transform) -> [f64; 3] {
[
transform.matrix.m[0][3],
transform.matrix.m[1][3],
transform.matrix.m[2][3],
]
}
#[allow(clippy::too_many_arguments)]
fn expansion_prototype_key(
ins: &acadrust::entities::Insert,
transform: &Transform,
ins_color: [f32; 4],
ins_pat_len: f32,
ins_pat: [f32; 8],
ins_lw_px: f32,
ins_layer: crate::scene::view::render::InheritStyle,
selected: bool,
pslt_factor: f32,
is_xref: bool,
bg_color: [f32; 4],
anno_scale: f32,
) -> ExpansionPrototypeKey {
let matrix = &transform.matrix.m;
let linear = [
matrix[0][0].to_bits(),
matrix[0][1].to_bits(),
matrix[0][2].to_bits(),
matrix[1][0].to_bits(),
matrix[1][1].to_bits(),
matrix[1][2].to_bits(),
matrix[2][0].to_bits(),
matrix[2][1].to_bits(),
matrix[2][2].to_bits(),
];
let mut insert_style = Vec::with_capacity(32);
insert_style.extend(ins_color.map(f32::to_bits));
insert_style.push(ins_pat_len.to_bits());
insert_style.extend(ins_pat.map(f32::to_bits));
insert_style.push(ins_lw_px.to_bits());
insert_style.extend(ins_layer.color.map(f32::to_bits));
insert_style.push(ins_layer.pat_len.to_bits());
insert_style.extend(ins_layer.pat.map(f32::to_bits));
insert_style.push(ins_layer.lw_px.to_bits());
insert_style.push(pslt_factor.to_bits());
insert_style.extend(bg_color.map(f32::to_bits));
insert_style.push(anno_scale.to_bits());
ExpansionPrototypeKey {
block_name: ins.block_name.clone(),
linear,
insert_style,
selected,
is_xref,
}
}
fn translated_prototype_wire(source: &WireModel, name: &str, delta: [f64; 3]) -> WireModel {
let mut wire = source.clone();
wire.name = name.to_string();
translate_double_single(&mut wire.points, &mut wire.points_low, delta);
translate_double_single(&mut wire.fill_tris, &mut wire.fill_tris_low, delta);
translate_double_single(&mut wire.pick_tris, &mut wire.pick_tris_low, delta);
for (point, _) in &mut wire.snap_pts {
point.x += delta[0];
point.y += delta[1];
point.z += delta[2];
}
for point in &mut wire.key_vertices {
point[0] += delta[0];
point[1] += delta[1];
point[2] += delta[2];
}
let delta_f32 = [delta[0] as f32, delta[1] as f32, delta[2] as f32];
for tangent in &mut wire.tangent_geoms {
match tangent {
TangentGeom::Line { p1, p2 } => {
for axis in 0..3 {
p1[axis] += delta_f32[axis];
p2[axis] += delta_f32[axis];
}
}
TangentGeom::Circle { center, .. } => {
for axis in 0..3 {
center[axis] += delta_f32[axis];
}
}
}
}
if !wire.text_verts.is_empty() {
wire.text_verts =
crate::scene::model::wire_model::map_text_verts(&wire.text_verts, |x, y, z| {
(x + delta[0], y + delta[1], z + delta[2])
});
}
if wire.aabb != WireModel::UNBOUNDED_AABB {
wire.aabb[0] += delta_f32[0];
wire.aabb[1] += delta_f32[1];
wire.aabb[2] += delta_f32[0];
wire.aabb[3] += delta_f32[1];
}
wire
}
fn translate_double_single(points: &mut [[f32; 3]], lows: &mut Vec<[f32; 3]>, delta: [f64; 3]) {
if points.is_empty() {
return;
}
if lows.len() != points.len() {
lows.resize(points.len(), [0.0; 3]);
}
for (point, low) in points.iter_mut().zip(lows.iter_mut()) {
for axis in 0..3 {
let value = point[axis] as f64 + low[axis] as f64 + delta[axis];
let high = value as f32;
point[axis] = high;
low[axis] = (value - high as f64) as f32;
}
}
}
fn aabb_pixel_size(local_aabb: [f32; 4], world_per_pixel: f32) -> f32 {

View file

@ -199,6 +199,7 @@ impl Scene {
};
if !handle.is_null() {
self.invalidate_dependency_index();
if let Some(model) = hatch_seed {
self.hatches.insert(handle, model);
}
@ -275,6 +276,7 @@ impl Scene {
}
}
}
self.invalidate_dependency_index();
self.bump_geometry();
true
}
@ -353,6 +355,7 @@ impl Scene {
return false;
};
*slot = entity;
self.invalidate_dependency_index();
// Drop stale derived caches for this handle, then reseed for the new
// entity's type (which may differ from the old one).
@ -2092,6 +2095,7 @@ impl Scene {
/// Rebuild hatch / image / mesh caches after the document is modified
/// outside the normal `add_entity` path (e.g. REFCLOSE SAVE).
pub fn rebuild_derived_caches(&mut self) {
self.invalidate_dependency_index();
self.populate_hatches_from_document_unbumped();
self.populate_images_from_document_unbumped();
self.populate_meshes_impl(false, false);

View file

@ -45,6 +45,22 @@ pub(super) struct EntityIndex {
pub unbounded_handles: Vec<Handle>,
}
#[derive(Default)]
struct DependencyTargets {
render_handles: HashSet<Handle>,
source_handles: HashSet<Handle>,
touches_block_definition: bool,
}
#[derive(Default)]
struct SceneDependencyIndex {
layers: HashMap<String, DependencyTargets>,
text_styles: HashMap<String, DependencyTargets>,
dim_styles: HashMap<String, DependencyTargets>,
object_styles: HashMap<Handle, DependencyTargets>,
blocks: HashMap<String, HashSet<Handle>>,
}
fn hatch_interaction_aabb(hatch: &model::hatch_model::HatchModel) -> Option<[f64; 4]> {
let mut aabb = [
f64::INFINITY,
@ -238,6 +254,21 @@ struct ResidentWireLayout {
marker_start: usize,
}
#[derive(Clone)]
pub struct PreparedOpenGeometry {
pub wires: Arc<Vec<WireModel>>,
pub interaction_index: Option<Arc<crate::scene::pick::interaction_index::InteractionIndex>>,
}
impl std::fmt::Debug for PreparedOpenGeometry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PreparedOpenGeometry")
.field("wires", &self.wires.len())
.field("interaction_index", &self.interaction_index.is_some())
.finish()
}
}
#[derive(Debug)]
pub(crate) struct WireGpuPatch {
pub(crate) changes: Arc<Vec<(Handle, ChangeKind)>>,
@ -357,6 +388,15 @@ pub struct DerivedCaches {
/// Reported back to the UI so the user knows when a file had parser-junk
/// entities silently dropped.
pub corrupt_dropped: usize,
/// Corrupt entities dropped while resolving referenced drawings.
pub xref_dropped: usize,
/// XREF resolution results produced by the loader worker. Keeping these in
/// the open bundle prevents parsing and merging references on the UI thread.
pub xrefs: Vec<crate::io::xref::XrefInfo>,
/// Model wire set and its spatial interaction index, prepared on the loader
/// thread. Installing these prevents the first visible frame from paying a
/// whole-drawing tessellation/index build while the progress overlay freezes.
pub prepared_geometry: Option<PreparedOpenGeometry>,
/// Background-thread open-phase timings in milliseconds (parse, purge,
/// derived-cache build). Filled in by `open_path_with_phase`; surfaced in
/// the open-complete breakdown log so open-time regressions are visible.
@ -369,11 +409,30 @@ pub struct OpenTimings {
pub parse_ms: u32,
pub purge_ms: u32,
pub caches_ms: u32,
pub xref_ms: u32,
}
/// Build hatch / image / mesh caches from a document without needing `&mut Scene`.
/// Intended to run on a background thread during file load.
pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
build_derived_caches_impl(doc, None)
}
/// Build open-time caches while reporting monotonic progress in 0..=10000.
///
/// The callback is UI-agnostic and may run from Rayon workers. Callers should
/// keep it cheap, normally just updating atomics.
pub fn build_derived_caches_with_progress(
doc: &CadDocument,
progress: &(dyn Fn(u16) + Sync),
) -> DerivedCaches {
build_derived_caches_impl(doc, Some(progress))
}
fn build_derived_caches_impl(
doc: &CadDocument,
progress: Option<&(dyn Fn(u16) + Sync)>,
) -> DerivedCaches {
// A new drawing must not inherit the previous one's resolved images — drop
// the memoised set so each reference re-reads / re-fetches once here (and
// stays cached across this document's later cache rebuilds).
@ -428,7 +487,8 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
let mut image_handles: Vec<Handle> = Vec::new();
let mut mesh_handles: Vec<Handle> = Vec::new();
let mut centers: Vec<[f64; 3]> = Vec::new();
for e in doc.entities() {
let entity_total = doc.entity_count().max(1);
for (index, e) in doc.entities().enumerate() {
let h = e.common().handle;
match e {
EntityType::Hatch(_) | EntityType::Solid(_) => hatch_handles.push(h),
@ -444,6 +504,14 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
if let Some(c) = offset_centroid(e, model_block, &prep) {
centers.push(c);
}
if index & 0x1fff == 0 {
if let Some(progress) = progress {
progress(((index as u64 * 4000) / entity_total as u64) as u16);
}
}
}
if let Some(progress) = progress {
progress(4000);
}
let (local_center, local_extent_max) = cluster_extent_from_centers(centers, &doc.header);
@ -453,6 +521,19 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
// the per-layout adaptation kicks in later regardless).
const LOAD_BG: [f32; 4] = [33.0 / 255.0, 40.0 / 255.0, 48.0 / 255.0, 1.0];
let detail_total = hatch_handles
.len()
.saturating_add(image_handles.len())
.saturating_add(mesh_handles.len())
.max(1);
let detail_done = std::sync::atomic::AtomicUsize::new(0);
let report_detail = |done: usize| {
if let Some(progress) = progress {
let value = 4000u64 + done as u64 * 6000 / detail_total as u64;
progress(value.min(10000) as u16);
}
};
// hatches
let hatches: HashMap<Handle, HatchModel> = hatch_handles
.par_iter()
@ -465,15 +546,23 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
EntityType::Solid(solid) => Some(Scene::solid_hatch_model(solid, color)),
_ => None,
};
model.map(|m| (handle, m))
let result = model.map(|m| (handle, m));
let done = detail_done.fetch_add(1, Ordering::Relaxed) + 1;
if done & 0xff == 0 || done == detail_total {
report_detail(done);
}
result
})
.collect();
// images
let images: HashMap<Handle, ImageModel> = image_handles
.par_iter()
.filter_map(|&handle| match doc.get_entity(handle)? {
EntityType::RasterImage(img) => ImageModel::from_raster_image(img).map(|m| (handle, m)),
.filter_map(|&handle| {
let result = match doc.get_entity(handle)? {
EntityType::RasterImage(img) => {
ImageModel::from_raster_image(img).map(|m| (handle, m))
}
EntityType::Ole2Frame(ole) => ImageModel::from_ole2frame(ole).map(|m| (handle, m)),
EntityType::Underlay(u) => match doc.objects.get(&u.definition_handle) {
Some(acadrust::objects::ObjectType::UnderlayDefinition(def)) => {
@ -482,6 +571,12 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
_ => None,
},
_ => None,
};
let done = detail_done.fetch_add(1, Ordering::Relaxed) + 1;
if done & 0xff == 0 || done == detail_total {
report_detail(done);
}
result
})
.collect();
@ -511,10 +606,16 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
let (raw, ..) = view::render::render_style_for(doc, e);
let color = view::render::adapt_to_bg(raw, LOAD_BG);
let top_level = layout_blocks.contains(&e.common().owner_handle);
crate::entities::solid3d::tessellate_volume(e, color, facet_res, isolines).map(|m| {
let result = crate::entities::solid3d::tessellate_volume(e, color, facet_res, isolines)
.map(|m| {
let m = if top_level { offset_mesh_lod_set(m) } else { m };
(handle, m, top_level)
})
});
let done = detail_done.fetch_add(1, Ordering::Relaxed) + 1;
if done & 0xff == 0 || done == detail_total {
report_detail(done);
}
result
})
.collect();
let mut meshes: HashMap<Handle, MeshLodSet> = HashMap::default();
@ -527,6 +628,10 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
}
}
if let Some(progress) = progress {
progress(10000);
}
DerivedCaches {
local_extent_max,
local_center,
@ -535,10 +640,54 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
meshes,
block_meshes,
corrupt_dropped: 0,
xref_dropped: 0,
xrefs: Vec::new(),
prepared_geometry: None,
timings: OpenTimings::default(),
}
}
/// Prepare the expensive first Model wire set and spatial interaction index on
/// the loader thread. The temporary `Scene` never crosses threads (it contains
/// `Rc`/`RefCell` state); only its Send-safe document and immutable prepared
/// geometry are returned.
pub fn prepare_open_geometry(
doc: CadDocument,
caches: &DerivedCaches,
model_bg: [f32; 4],
) -> (CadDocument, PreparedOpenGeometry) {
let mut scene = Scene::new();
scene.document = doc;
scene.local_extent_max = caches.local_extent_max;
scene.local_center = caches.local_center;
scene.bg_color = model_bg;
let cannoscale_value = scene.document.header.annotation_scale_value;
scene.annotation_scale = if cannoscale_value > 1e-9 {
(1.0 / cannoscale_value) as f32
} else {
1.0
};
scene.current_layout = "Model".to_string();
let camera = scene.camera.borrow().clone();
let wires = scene.model_tile_wires_arc(0, &camera, 1.0, 1.0);
let interaction_index = if scene.interaction_index_worthwhile(&wires) {
let index =
Arc::new(crate::scene::pick::interaction_index::InteractionIndex::build(&wires));
index.prepare_screen();
Some(index)
} else {
None
};
let doc = std::mem::replace(&mut scene.document, CadDocument::new());
(
doc,
PreparedOpenGeometry {
wires,
interaction_index,
},
)
}
/// Mirrors `cache::block_cache::SANE_EXTENT` — wire coords past this magnitude
/// are treated as corruption rather than precision-relevant geometry.
const CLUSTER_SANE_EXTENT: f64 = 1.0e8;
@ -1165,13 +1314,10 @@ pub struct Scene {
/// filtered variants. Viewports sharing a frozen set share one entry (like
/// the resident wire set). Empty for a viewport with no frozen layers (it
/// reuses the unfiltered `*_arc` sets directly).
frozen_hatch_cache:
RefCell<HashMap<(String, u64), (u64, u64, Arc<Vec<HatchModel>>)>>,
frozen_wipeout_cache:
RefCell<HashMap<(String, u64), (u64, Arc<Vec<HatchModel>>)>>,
frozen_hatch_cache: RefCell<HashMap<(String, u64), (u64, u64, Arc<Vec<HatchModel>>)>>,
frozen_wipeout_cache: RefCell<HashMap<(String, u64), (u64, Arc<Vec<HatchModel>>)>>,
frozen_image_cache: RefCell<HashMap<u64, (u64, Arc<Vec<ImageModel>>)>>,
frozen_mesh_cache:
RefCell<HashMap<(String, u64), (u64, Arc<Vec<MeshLodSet>>)>>,
frozen_mesh_cache: RefCell<HashMap<(String, u64), (u64, Arc<Vec<MeshLodSet>>)>>,
/// Cached block-INSERT hatches for hit-testing, keyed by geometry_epoch.
/// Building this explodes every model-space INSERT, so without the cache a
/// heavy block-instanced drawing re-explodes thousands of inserts on every
@ -1258,12 +1404,16 @@ pub struct Scene {
/// Reverse map: entity_handle → block_record_handle, built from entity_handles lists.
/// Keyed by geometry_epoch. Eliminates the O(B) fallback scan in belongs_to_visible_block.
entity_block_map_cache: RefCell<Option<(u64, HashMap<Handle, Handle>)>>,
/// Reverse dependencies from layer/style/block definitions to the top-level
/// entities whose resident wire runs actually change. Kept independent from
/// `geometry_epoch`: a layer colour toggle can reuse the index, invalidate
/// only its dependants, and avoid a whole-document scan on every toggle.
dependency_index_cache: RefCell<Option<SceneDependencyIndex>>,
/// Tessellated block definitions in block-local coords, keyed by render
/// background and block epoch. Model and Paper adapt black/white colours
/// differently; retaining both variants prevents a full block rebuild on
/// every layout-tab switch.
block_defn_cache:
RefCell<HashMap<[u32; 4], (u64, Arc<cache::block_cache::BlockCache>)>>,
block_defn_cache: RefCell<HashMap<[u32; 4], (u64, Arc<cache::block_cache::BlockCache>)>>,
/// Spatial index + always-emit list for top-level entities
/// (Phase 2.1). Lazily rebuilt by `entity_index()` on
/// `geometry_epoch` change. See `EntityIndex` for what each side
@ -1455,6 +1605,7 @@ impl Scene {
annotation_affects_wires: std::cell::Cell::new(None),
model_extents_cache: RefCell::new(None),
entity_block_map_cache: RefCell::new(None),
dependency_index_cache: RefCell::new(None),
block_defn_cache: RefCell::new(HashMap::default()),
entity_index_cache: RefCell::new(None),
last_render_aspect: std::cell::Cell::new(16.0 / 9.0),
@ -1555,6 +1706,30 @@ impl Scene {
arc
}
/// Install loader-thread geometry into this scene's Model resident cache.
///
/// The target scene has a different epoch from the temporary loader scene,
/// so only immutable geometry is transferred and re-stamped here. The
/// optional interaction index is cached against the exact same `Arc`.
pub fn install_prepared_open_geometry(&self, prepared: PreparedOpenGeometry) {
let block = self.model_space_block_handle();
let key = Self::resident_wire_key(block, self.bg_color, None, None);
let gen = WIRE_CONTENT_GEN.fetch_add(1, Ordering::Relaxed);
self.last_model_wire_gen.set(gen);
self.resident_wire_sets.borrow_mut().insert(
key,
ResidentWireSet {
epoch: self.geometry_epoch,
gen,
wires: Arc::clone(&prepared.wires),
layout: None,
},
);
if let Some(index) = prepared.interaction_index {
self.cache_interaction_index(self.geometry_epoch, prepared.wires, index);
}
}
/// Push one delta onto the journal, evicting the oldest past the cap and
/// raising the floor so a consumer that fell behind falls back to a full
/// rebuild. Every `geometry_epoch` bump must call this exactly once so the
@ -1779,6 +1954,10 @@ impl Scene {
}
}
if !changes.is_empty() {
self.invalidate_dependency_index();
}
changes
}
@ -3245,30 +3424,7 @@ impl Scene {
} else {
self.paper_bg_color
};
let key = {
let mut k: u64 = 0xcbf2_9ce4_8422_2325;
let mut mix = |x: u64| k = k.rotate_left(17) ^ x.wrapping_mul(0x9E37_79B9_7F4A_7C15);
mix(block.value());
for c in bg {
mix(c.to_bits() as u64);
}
mix(anno_scale_override
.map(|a| a.to_bits() as u64)
.unwrap_or(u64::MAX));
match frozen_layers {
Some(f) => {
// Order-independent fold of the frozen-layer set.
let mut acc: u64 = 0;
for h in f {
acc ^= h.value().wrapping_mul(0x9E37_79B9_7F4A_7C15);
}
mix(acc);
mix(f.len() as u64);
}
None => mix(u64::MAX - 1),
}
k
};
let key = Self::resident_wire_key(block, bg, anno_scale_override, frozen_layers);
{
let sets = self.resident_wire_sets.borrow();
if let Some(set) = sets.get(&key) {
@ -3331,6 +3487,36 @@ impl Scene {
arc
}
fn resident_wire_key(
block: Handle,
bg: [f32; 4],
anno_scale_override: Option<f32>,
frozen_layers: Option<&HashSet<Handle>>,
) -> u64 {
let mut key: u64 = 0xcbf2_9ce4_8422_2325;
let mut mix =
|value: u64| key = key.rotate_left(17) ^ value.wrapping_mul(0x9E37_79B9_7F4A_7C15);
mix(block.value());
for component in bg {
mix(component.to_bits() as u64);
}
mix(anno_scale_override
.map(|scale| scale.to_bits() as u64)
.unwrap_or(u64::MAX));
match frozen_layers {
Some(frozen) => {
let mut signature = 0u64;
for handle in frozen {
signature ^= handle.value().wrapping_mul(0x9E37_79B9_7F4A_7C15);
}
mix(signature);
mix(frozen.len() as u64);
}
None => mix(u64::MAX - 1),
}
key
}
/// The GPU wire-arena handoff for a viewport whose content id is `gen`:
/// `(prev_gen, changed handles)` when the Model set reached `gen` via an
/// incremental resident patch, else `None`. Read (not consumed) so every
@ -6375,6 +6561,253 @@ impl Scene {
})
}
fn rebuild_dependency_index(&self) -> SceneDependencyIndex {
let layout_blocks: HashSet<Handle> = self
.document
.objects
.values()
.filter_map(|object| match object {
acadrust::objects::ObjectType::Layout(layout) if !layout.block_record.is_null() => {
Some(layout.block_record)
}
_ => None,
})
.collect();
let block_names: HashMap<Handle, String> = self
.document
.block_records
.iter()
.map(|record| (record.handle, record.name.to_ascii_uppercase()))
.collect();
let membership: HashMap<Handle, Handle> = self
.document
.block_records
.iter()
.flat_map(|record| {
record
.entity_handles
.iter()
.copied()
.map(move |handle| (handle, record.handle))
})
.collect();
let mut roots: HashMap<String, HashSet<Handle>> = HashMap::default();
let mut parents: HashMap<String, HashSet<String>> = HashMap::default();
for entity in self.document.entities() {
let EntityType::Insert(insert) = entity else {
continue;
};
let target = insert.block_name.to_ascii_uppercase();
let common = &insert.common;
let owner = if common.owner_handle.is_null() {
membership
.get(&common.handle)
.copied()
.unwrap_or(Handle::NULL)
} else {
common.owner_handle
};
if owner.is_null() || layout_blocks.contains(&owner) {
roots.entry(target).or_default().insert(common.handle);
} else if let Some(parent) = block_names.get(&owner) {
parents.entry(target).or_default().insert(parent.clone());
}
}
// Propagate top-level INSERT users through nested block references.
// Fixed-point form is cycle-safe and block graphs are normally shallow.
let mut changed = true;
while changed {
changed = false;
for (child, parent_names) in &parents {
let inherited: Vec<Handle> = parent_names
.iter()
.flat_map(|parent| roots.get(parent).into_iter().flatten().copied())
.collect();
let entry = roots.entry(child.clone()).or_default();
let before = entry.len();
entry.extend(inherited);
changed |= entry.len() != before;
}
}
let mut index = SceneDependencyIndex {
blocks: roots.clone(),
..SceneDependencyIndex::default()
};
for entity in self.document.entities() {
let common = entity.common();
let owner = if common.owner_handle.is_null() {
membership
.get(&common.handle)
.copied()
.unwrap_or(Handle::NULL)
} else {
common.owner_handle
};
let inside_block = !owner.is_null() && !layout_blocks.contains(&owner);
let render_handles: HashSet<Handle> = if inside_block {
block_names
.get(&owner)
.and_then(|name| roots.get(name))
.cloned()
.unwrap_or_default()
} else {
std::iter::once(common.handle).collect()
};
let add = |map: &mut HashMap<String, DependencyTargets>, name: &str| {
let target = map.entry(name.to_ascii_uppercase()).or_default();
target.render_handles.extend(render_handles.iter().copied());
target.source_handles.insert(common.handle);
target.touches_block_definition |= inside_block;
};
let add_handle = |map: &mut HashMap<Handle, DependencyTargets>,
handle: Option<Handle>| {
let Some(handle) = handle.filter(|handle| !handle.is_null()) else {
return;
};
let target = map.entry(handle).or_default();
target.render_handles.extend(render_handles.iter().copied());
target.source_handles.insert(common.handle);
target.touches_block_definition |= inside_block;
};
add(&mut index.layers, &common.layer);
match entity {
EntityType::Text(text) => add(&mut index.text_styles, &text.style),
EntityType::MText(text) => add(&mut index.text_styles, &text.style),
EntityType::AttributeDefinition(attribute) => {
add(&mut index.text_styles, &attribute.text_style)
}
EntityType::AttributeEntity(attribute) => {
add(&mut index.text_styles, &attribute.text_style)
}
EntityType::Insert(insert) => {
for attribute in &insert.attributes {
add(&mut index.text_styles, &attribute.text_style);
}
}
EntityType::Dimension(dimension) => {
add(&mut index.dim_styles, &dimension.base().style_name)
}
EntityType::Table(table) => {
add_handle(&mut index.object_styles, table.table_style_handle)
}
EntityType::MultiLeader(leader) => {
add_handle(&mut index.object_styles, leader.style_handle)
}
EntityType::MLine(line) => add_handle(&mut index.object_styles, line.style_handle),
_ => {}
}
}
index
}
fn dependency_targets(&self, kind: &str, names: &[String]) -> DependencyTargets {
if self.dependency_index_cache.borrow().is_none() {
*self.dependency_index_cache.borrow_mut() = Some(self.rebuild_dependency_index());
}
let cache = self.dependency_index_cache.borrow();
let index = cache.as_ref().unwrap();
let map = match kind {
"layer" => &index.layers,
"text" => &index.text_styles,
"dim" => &index.dim_styles,
_ => unreachable!(),
};
let mut combined = DependencyTargets::default();
for name in names {
let Some(target) = map.get(&name.to_ascii_uppercase()) else {
continue;
};
combined
.render_handles
.extend(target.render_handles.iter().copied());
combined
.source_handles
.extend(target.source_handles.iter().copied());
combined.touches_block_definition |= target.touches_block_definition;
}
combined
}
fn invalidate_dependency_targets(&mut self, targets: DependencyTargets) {
if targets.render_handles.is_empty() {
return;
}
if targets.touches_block_definition {
self.block_epoch = GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed);
}
let sources: Vec<Handle> = targets.source_handles.iter().copied().collect();
self.recolor_meshes_for_handles(&sources);
let changes: Vec<(Handle, ChangeKind)> = targets
.render_handles
.into_iter()
.map(|handle| (handle, ChangeKind::Modified))
.collect();
self.bump_entities(&changes);
}
pub fn invalidate_layer_dependencies(&mut self, names: &[String]) {
let targets = self.dependency_targets("layer", names);
self.invalidate_dependency_targets(targets);
}
pub fn invalidate_text_style_dependencies(&mut self, name: &str) {
self.invalidate_text_style_dependencies_many(&[name.to_string()]);
}
pub fn invalidate_text_style_dependencies_many(&mut self, names: &[String]) {
let targets = self.dependency_targets("text", names);
self.invalidate_dependency_targets(targets);
}
pub fn invalidate_dim_style_dependencies(&mut self, name: &str) {
self.invalidate_dim_style_dependencies_many(&[name.to_string()]);
}
pub fn invalidate_dim_style_dependencies_many(&mut self, names: &[String]) {
let targets = self.dependency_targets("dim", names);
self.invalidate_dependency_targets(targets);
}
pub fn invalidate_object_style_dependencies(&mut self, handles: &[Handle]) {
if self.dependency_index_cache.borrow().is_none() {
*self.dependency_index_cache.borrow_mut() = Some(self.rebuild_dependency_index());
}
let mut combined = DependencyTargets::default();
if let Some(index) = self.dependency_index_cache.borrow().as_ref() {
for handle in handles {
let Some(target) = index.object_styles.get(handle) else {
continue;
};
combined
.render_handles
.extend(target.render_handles.iter().copied());
combined
.source_handles
.extend(target.source_handles.iter().copied());
combined.touches_block_definition |= target.touches_block_definition;
}
}
self.invalidate_dependency_targets(combined);
}
pub fn block_dependency_handles(&self, name: &str) -> Vec<Handle> {
if self.dependency_index_cache.borrow().is_none() {
*self.dependency_index_cache.borrow_mut() = Some(self.rebuild_dependency_index());
}
self.dependency_index_cache
.borrow()
.as_ref()
.and_then(|index| index.blocks.get(&name.to_ascii_uppercase()))
.map(|handles| handles.iter().copied().collect())
.unwrap_or_default()
}
pub(crate) fn invalidate_dependency_index(&self) {
self.dependency_index_cache.borrow_mut().take();
}
/// Spatial index + always-emit list for top-level entities. Lazily
/// rebuilt on `geometry_epoch` change.
///

View file

@ -47,7 +47,7 @@ pub struct Pipeline {
/// Used to draw ghost copies of selected wires through occluding geometry.
wire_xray_pipeline: wgpu::RenderPipeline,
/// Layout for the per-wire `WireConst` storage buffer (group 1 of the wire /
/// xray pipelines). `Some` only on the fast native path; `None` in packed
/// xray pipelines). `Some` on native/WebGPU fast paths; `None` in packed
/// compatibility mode. Passed to `WireGpu::from_run` / `from_batch`.
pub(crate) wire_const_bgl: Option<wgpu::BindGroupLayout>,
wipeout_pipeline: wgpu::RenderPipeline,
@ -338,8 +338,8 @@ impl Pipeline {
});
// ── Wire pipeline ──────────────────────────────────────────────────
// Select once per device. The fast native path hoists shared constants
// into storage; compatibility mode keeps them in 10 packed attributes.
// Select once per device. Native and WebGPU hoist shared constants into
// storage when limits allow; WebGL2/compat keeps packed attributes.
let wire_mode = wire_gpu::WirePipelineMode::select(device);
let renderer_mode_name =
if wire_mode.uses_storage() { "fast-storage" } else { "packed-compat" };
@ -357,12 +357,9 @@ impl Pipeline {
renderer_mode_name,
device.limits().max_storage_buffers_per_shader_stage
);
#[cfg(not(target_arch = "wasm32"))]
let wire_const_bgl = wire_mode
.uses_storage()
.then(|| wire_gpu::WireConst::bind_group_layout(device));
#[cfg(target_arch = "wasm32")]
let wire_const_bgl: Option<wgpu::BindGroupLayout> = None;
let mut wire_bgls: Vec<&wgpu::BindGroupLayout> = vec![&frame_bgl];
if let Some(bgl) = &wire_const_bgl {
wire_bgls.push(bgl);
@ -379,7 +376,6 @@ impl Pipeline {
let wire_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("wire.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(match wire_mode {
#[cfg(not(target_arch = "wasm32"))]
wire_gpu::WirePipelineMode::IndexedStorage => {
include_str!("../../shaders/wire_indexed.wgsl")
}
@ -3157,6 +3153,14 @@ fn create_msaa_texture(
pub struct MultiPipeline {
pub(crate) inners: Vec<Pipeline>,
format: wgpu::TextureFormat,
/// Stable viewport identity → pipeline slot map. Paper viewports used to
/// occupy slots by their current list position, so switching layouts or
/// scrolling a sheet could assign an existing viewport to a different
/// slot and throw away all of its GPU caches. Keep the association across
/// tab switches and only recycle genuinely cold slots.
pub(crate) slot_by_instance: rustc_hash::FxHashMap<u64, usize>,
slot_last_used: Vec<u64>,
slot_clock: u64,
/// The resident wire batches, keyed by `wire_content_id` and shared across
/// every slot (and every pane — one `MultiPipeline` backs all of them) that
/// renders the same content. `prepare` builds an entry once on a cache miss
@ -3189,8 +3193,70 @@ impl MultiPipeline {
let n = n.max(1);
while self.inners.len() < n {
self.inners.push(Pipeline::new(device, queue, self.format));
self.slot_last_used.push(0);
}
}
/// Resolve stable slots for the viewport identities in one primitive.
/// Thirty-two hot slots cover ordinary tiled/paper drawings. A cold slot
/// is recycled only after several other prepare calls, which prevents
/// sibling Model panes prepared in the same frame from evicting each
/// other. If every slot is still hot, growing is safer than a visible
/// rebuild hitch.
pub(crate) fn resolve_slots(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
instance_ids: &[u64],
) -> Vec<usize> {
const SOFT_LIMIT: usize = 32;
const HOT_WINDOW: u64 = 8;
self.slot_clock = self.slot_clock.wrapping_add(1).max(1);
let now = self.slot_clock;
let reserved: rustc_hash::FxHashSet<u64> = instance_ids.iter().copied().collect();
let mut slots = Vec::with_capacity(instance_ids.len());
for &instance_id in instance_ids {
let slot = if let Some(&slot) = self.slot_by_instance.get(&instance_id) {
slot
} else {
let vacant = self
.inners
.iter()
.position(|inner| inner.slot_id == u64::MAX);
let recyclable = vacant.or_else(|| {
(self.inners.len() >= SOFT_LIMIT)
.then(|| {
self.inners
.iter()
.enumerate()
.filter(|(_, inner)| !reserved.contains(&inner.slot_id))
.filter(|(slot, _)| {
now.saturating_sub(self.slot_last_used[*slot]) > HOT_WINDOW
})
.min_by_key(|(slot, _)| self.slot_last_used[*slot])
.map(|(slot, _)| slot)
})
.flatten()
});
let slot = recyclable.unwrap_or_else(|| {
let slot = self.inners.len();
self.ensure_len(device, queue, slot + 1);
slot
});
let old_id = self.inners[slot].slot_id;
if old_id != u64::MAX {
self.slot_by_instance.remove(&old_id);
}
self.slot_by_instance.insert(instance_id, slot);
slot
};
self.slot_last_used[slot] = now;
slots.push(slot);
}
slots
}
}
/// Send wgpu's uncaptured validation errors to stderr instead of the default
@ -3227,6 +3293,9 @@ impl iced::widget::shader::Pipeline for MultiPipeline {
Self {
inners: vec![Pipeline::new(device, queue, format)],
format,
slot_by_instance: rustc_hash::FxHashMap::default(),
slot_last_used: vec![0],
slot_clock: 0,
wire_buffer_cache: rustc_hash::FxHashMap::default(),
}
}

View file

@ -58,18 +58,17 @@ fn instance_buffer_mapped<T: bytemuck::Pod>(
// ── Instance layout ───────────────────────────────────────────────────────
// ── Native: slim per-segment instance + shared per-wire constants ───────────
// ── WebGPU/native: slim per-segment instance + shared constants ─────────────
//
// Every segment of a wire used to carry the wire's color / line-weight / dash
// pattern / draw-depth (~44 B) on each instance — re-fetched once per segment
// even though it's constant along the wire. On native we hoist those into a
// even though it's constant along the wire. On storage-capable adapters we hoist those into a
// per-wire `WireConst` storage buffer indexed by `wire_id`, so the instance
// keeps only the per-segment data (endpoints + arc-length distances). Cuts the
// instance from 104 B to one 64-byte cache line and removes the redundant
// per-segment re-fetch of the shared constants. WebGL2 has no vertex-stage
// storage buffers, so the wasm build below keeps the original self-contained
// fat instance.
#[cfg(not(target_arch = "wasm32"))]
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct WireInstance {
@ -87,8 +86,6 @@ pub struct WireInstance {
/// exactly one 64-byte cache line.
pub taper_ratio: [u16; 2],
}
#[cfg(not(target_arch = "wasm32"))]
impl WireInstance {
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
// Must match `InstanceIn` in wire_indexed.wgsl.
@ -110,11 +107,10 @@ impl WireInstance {
}
}
/// Per-wire constants shared by every segment of a wire (native only). std430
/// Per-wire constants shared by every segment of a wire. std430
/// layout: three vec4 then eight scalars = 80 B, matching `WireConst` in
/// wire_indexed.wgsl. `align_end` / `align_total` carry the "A"-type endpoint
/// alignment (see `wire_distances`); 0.0 total = no alignment.
#[cfg(not(target_arch = "wasm32"))]
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct WireConst {
@ -134,8 +130,6 @@ pub struct WireConst {
pub _pad1: f32,
pub _pad2: f32,
}
#[cfg(not(target_arch = "wasm32"))]
impl WireConst {
/// Bind-group layout for the per-wire storage buffer (group 1 of the wire /
/// xray pipelines). Read-only storage, visible to the vertex stage.
@ -158,8 +152,8 @@ impl WireConst {
// ── Packed compatibility instance (no vertex-stage storage) ────────────────
//
// Web always uses this layout. Native selects it at runtime for adapters whose
// storage-buffer limits are insufficient, or when --compat-renderer is set.
// WebGL2 and limited native adapters use this layout. WebGPU selects the
// indexed storage path when its reported limits satisfy the same requirement.
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct PackedWireInstance {
@ -224,21 +218,15 @@ impl PackedWireInstance {
}
}
#[cfg(target_arch = "wasm32")]
pub type WireInstance = PackedWireInstance;
/// Wire and hatch pipelines switch together: the fast path uses storage
/// buffers; the compatibility path carries wire constants in packed vertex
/// attributes and hatch data in a texture.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WirePipelineMode {
#[cfg(not(target_arch = "wasm32"))]
IndexedStorage,
Packed,
}
#[cfg(not(target_arch = "wasm32"))]
fn select_native_pipeline(max_storage_buffers_per_stage: u32, forced: bool) -> WirePipelineMode {
fn select_pipeline(max_storage_buffers_per_stage: u32, forced: bool) -> WirePipelineMode {
const REQUIRED_STORAGE_BUFFERS_PER_STAGE: u32 = 5;
if forced || max_storage_buffers_per_stage < REQUIRED_STORAGE_BUFFERS_PER_STAGE {
WirePipelineMode::Packed
@ -249,23 +237,15 @@ fn select_native_pipeline(max_storage_buffers_per_stage: u32, forced: bool) -> W
impl WirePipelineMode {
pub fn select(device: &wgpu::Device) -> Self {
#[cfg(target_arch = "wasm32")]
{
let _ = device;
Self::Packed
}
#[cfg(not(target_arch = "wasm32"))]
{
select_native_pipeline(
device.limits().max_storage_buffers_per_shader_stage,
crate::cli::gui_config().compat_renderer,
)
}
let forced = crate::cli::gui_config().compat_renderer;
#[cfg(target_arch = "wasm32")]
let forced = false;
select_pipeline(device.limits().max_storage_buffers_per_shader_stage, forced)
}
pub fn uses_storage(self) -> bool {
match self {
#[cfg(not(target_arch = "wasm32"))]
Self::IndexedStorage => true,
Self::Packed => false,
}
@ -273,7 +253,6 @@ impl WirePipelineMode {
pub fn layout<'a>(self) -> wgpu::VertexBufferLayout<'a> {
match self {
#[cfg(not(target_arch = "wasm32"))]
Self::IndexedStorage => WireInstance::layout(),
Self::Packed => PackedWireInstance::layout(),
}
@ -494,9 +473,8 @@ fn emit_wire_packed(
instances
}
/// Native: emit slim per-segment instances (positions + distances + `wire_id`)
/// Storage path: emit slim per-segment instances (positions + distances + `wire_id`)
/// plus the one `WireConst` record every segment of this wire shares.
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn emit_wire_native(
wire: &WireModel,
wire_id: u32,
@ -579,11 +557,10 @@ pub(crate) fn wire_draw_depth(
}
}
/// Build the shared per-wire `WireConst` storage buffer and its bind group
/// (native only). All instance-buffer chunks from one build reference the same
/// Build the shared per-wire `WireConst` storage buffer and its bind group.
/// All instance-buffer chunks from one build reference the same
/// buffer via their global `wire_id`, so a single bind group is cloned into
/// each chunk.
#[cfg(not(target_arch = "wasm32"))]
fn build_const_bind_group(
device: &wgpu::Device,
bgl: &wgpu::BindGroupLayout,
@ -677,7 +654,6 @@ impl WireGpu {
mesh_edge: bool,
const_bgl: Option<&wgpu::BindGroupLayout>,
) -> Vec<Self> {
#[cfg(not(target_arch = "wasm32"))]
if let Some(const_bgl) = const_bgl {
const MAX_INSTANCES: usize =
268_435_456 / std::mem::size_of::<WireInstance>();
@ -769,7 +745,6 @@ impl WireGpu {
if total_segs == 0 {
return vec![];
}
#[cfg(not(target_arch = "wasm32"))]
if let Some(const_bgl) = const_bgl {
// GPU max buffer size is 256 MB; chunk to stay within the limit.
const MAX_INSTANCES: usize =

View file

@ -391,8 +391,11 @@ impl Scene {
// Report the exact erased handles so derived caches drop just those and
// the resident set removes only their wires (bump_entities drops them
// from the tessellation memos too).
if !erased.is_empty() {
self.invalidate_dependency_index();
self.bump_entities(&erased);
}
}
/// Restore erased Arc-backed entities without re-linking their still-present
/// block-record handles. Used by OOPS and history replay.
@ -414,6 +417,7 @@ impl Scene {
}
}
if !changes.is_empty() {
self.invalidate_dependency_index();
self.bump_entities(&changes);
}
restored

View file

@ -152,13 +152,6 @@ pub struct Primitive {
pub(in crate::scene) viewports: Vec<ViewportData>,
/// Background color used to clear each viewport's MSAA buffer.
pub(in crate::scene) bg_color: [f32; 4],
/// First `MultiPipeline` inner slot this primitive owns. Paper space (one
/// shader widget, many viewports) uses 0. Per-pane Model widgets each own a
/// distinct slot (= their tile index) so several shader widgets can share
/// the type-keyed pipeline storage without clobbering one another — all
/// `prepare` calls run before all `render` calls, so disjoint slots are
/// safe.
pub(in crate::scene) base_slot: usize,
/// One input-to-render sample, carried only when PERF tracing is enabled.
pub(in crate::scene) nav_perf: Option<NavPerfSample>,
}
@ -250,10 +243,11 @@ impl shader::Primitive for Primitive {
let phys = viewport.physical_size();
let full_size = Size::new(phys.width, phys.height);
let scale = viewport.scale_factor() as f32;
pipeline.ensure_len(device, queue, self.base_slot + self.viewports.len());
let instance_ids: Vec<u64> = self.viewports.iter().map(|vp| vp.instance_id).collect();
let slots = pipeline.resolve_slots(device, queue, &instance_ids);
for (i, vp) in self.viewports.iter().enumerate() {
let inner = &mut pipeline.inners[self.base_slot + i];
let inner = &mut pipeline.inners[slots[i]];
// Pipeline slots are addressed by list index, but off-canvas
// viewports are dropped from the list — so a slot can be reused by a
// DIFFERENT viewport across frames (e.g. the first viewport scrolls
@ -442,7 +436,10 @@ impl shader::Primitive for Primitive {
// the whole wire buffer. Only for the scissor-free, mesh-free
// (single-batch) Model set; scissored paper viewports and mixed
// 2D/3D sets fall through to the shared batched path below.
#[cfg(not(target_arch = "wasm32"))]
let mut arena_served = false;
#[cfg(target_arch = "wasm32")]
let arena_served = false;
#[cfg(not(target_arch = "wasm32"))]
let _perf = crate::perf::enabled();
#[cfg(not(target_arch = "wasm32"))]
@ -895,9 +892,12 @@ impl shader::Primitive for Primitive {
let ch = clip.height as f32;
let clip_right = clip.x + clip.width;
let clip_bottom = clip.y + clip.height;
for (i, vp) in self.viewports.iter().enumerate() {
let Some(inner) = pipeline.inners.get(self.base_slot + i) else {
break;
for vp in &self.viewports {
let Some(slot) = pipeline.slot_by_instance.get(&vp.instance_id) else {
continue;
};
let Some(inner) = pipeline.inners.get(*slot) else {
continue;
};
// Where the viewport would land on the surface in absolute
// pixels (i32 because either edge may stick off the canvas).
@ -1433,14 +1433,9 @@ impl Scene {
sample.build_ms = nav_build_started.elapsed().as_secs_f64() * 1000.0;
sample
});
// Model panes permanently own slots 0..N. Paper starts after them so
// its sheet/content viewports never evict the Model slot's wire arena,
// textures, mesh batches and render cache during a layout-tab switch.
let base_slot = self.model_tiles.borrow().len();
Primitive {
viewports,
bg_color,
base_slot,
nav_perf: perf_nav,
}
}
@ -1466,7 +1461,6 @@ impl Scene {
return Primitive {
viewports: vec![],
bg_color,
base_slot: tile_idx,
nav_perf: None,
};
};
@ -1514,7 +1508,6 @@ impl Scene {
Primitive {
viewports,
bg_color,
base_slot: tile_idx,
nav_perf: perf_nav,
}
}

View file

@ -1,7 +1,7 @@
//! Modal overlay shown while a CAD file is being loaded.
//!
//! Displays the file name, size, current phase, an indeterminate animated
//! progress bar, and a Cancel button.
//! Displays the file name, size, current phase, measured progress, and a
//! Cancel button.
use iced::time::Instant;
use iced::widget::{button, column, container, row, stack, text, Space};
@ -10,20 +10,18 @@ use std::sync::atomic::Ordering;
use crate::app::{
Message, OpenProgress, OPEN_PHASE_CACHING, OPEN_PHASE_FINALIZING, OPEN_PHASE_PARSING,
OPEN_PHASE_READING,
OPEN_PHASE_READING, OPEN_PHASE_XREF,
};
const CARD_WIDTH: f32 = 420.0;
const BAR_TRACK_WIDTH: f32 = 380.0;
const BAR_TRACK_HEIGHT: f32 = 6.0;
const BAR_WINDOW_WIDTH: f32 = 100.0;
/// Period of one back-and-forth bounce, in milliseconds.
const BAR_PERIOD_MS: f32 = 1800.0;
fn phase_label(phase: u8) -> &'static str {
match phase {
OPEN_PHASE_READING => "Reading file…",
OPEN_PHASE_PARSING => "Parsing entities…",
OPEN_PHASE_XREF => "Loading references…",
OPEN_PHASE_CACHING => "Building scene caches…",
OPEN_PHASE_FINALIZING => "Finalizing…",
_ => "Working…",
@ -46,29 +44,22 @@ fn format_size(bytes: u64) -> String {
}
}
/// Compute the left-offset of the moving highlight inside the track.
/// Bounces left↔right using a triangle wave so the user sees motion even when
/// the actual phase atomic stays put for a while.
fn bar_offset(elapsed_ms: f32) -> f32 {
let travel = (BAR_TRACK_WIDTH - BAR_WINDOW_WIDTH).max(0.0);
let cycle = (elapsed_ms / BAR_PERIOD_MS).fract();
let tri = if cycle < 0.5 {
cycle * 2.0
} else {
(1.0 - cycle) * 2.0
};
tri * travel
}
pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Message> {
let phase = progress.state.phase.load(Ordering::Acquire);
let basis_points = progress
.state
.basis_points
.load(Ordering::Relaxed)
.min(10000);
let fraction = basis_points as f32 / 10000.0;
let fill_width = BAR_TRACK_WIDTH * fraction;
let trailing = (BAR_TRACK_WIDTH - fill_width).max(0.0);
pub fn view<'a>(progress: &'a OpenProgress, now: Instant) -> Element<'a, Message> {
let phase = progress.phase.load(Ordering::Relaxed);
let elapsed_ms = now.saturating_duration_since(progress.started).as_millis() as f32;
// ── Animated indeterminate bar ────────────────────────────────────────
let offset = bar_offset(elapsed_ms);
let trailing = (BAR_TRACK_WIDTH - BAR_WINDOW_WIDTH - offset).max(0.0);
let bar_window: Element<'_, Message> = container(Space::new().width(Length::Fixed(BAR_WINDOW_WIDTH)).height(Length::Fixed(BAR_TRACK_HEIGHT)))
let bar_fill: Element<'_, Message> = container(
Space::new()
.width(Length::Fixed(fill_width))
.height(Length::Fixed(BAR_TRACK_HEIGHT)),
)
.style(|_: &Theme| container::Style {
background: Some(Background::Color(Color {
r: 0.30,
@ -84,11 +75,8 @@ pub fn view<'a>(progress: &'a OpenProgress, now: Instant) -> Element<'a, Message
})
.into();
let bar_moving: Element<'_, Message> = row![
Space::new()
.width(Length::Fixed(offset))
.height(Length::Fixed(BAR_TRACK_HEIGHT)),
bar_window,
let bar_value: Element<'_, Message> = row![
bar_fill,
Space::new()
.width(Length::Fixed(trailing))
.height(Length::Fixed(BAR_TRACK_HEIGHT)),
@ -111,7 +99,7 @@ pub fn view<'a>(progress: &'a OpenProgress, now: Instant) -> Element<'a, Message
},
..Default::default()
}),
bar_moving,
bar_value,
]
.width(Length::Fixed(BAR_TRACK_WIDTH))
.height(Length::Fixed(BAR_TRACK_HEIGHT)),
@ -136,7 +124,11 @@ pub fn view<'a>(progress: &'a OpenProgress, now: Instant) -> Element<'a, Message
a: 1.0,
});
let phase_line = text(phase_label(phase))
let phase_line = text(format!(
"{} {:.1}%",
phase_label(phase),
basis_points as f32 / 100.0
))
.size(12)
.color(Color {
r: 0.70,

20
web/ocs-parse-worker.js Normal file
View file

@ -0,0 +1,20 @@
import init, { parse_document } from "./worker_pkg/ocs_web_worker.js";
const ready = init();
self.onmessage = async ({ data }) => {
try {
await ready;
const encoded = parse_document(data.name, new Uint8Array(data.bytes));
// wasm-bindgen returns a view into WebAssembly.Memory. Copy to a standalone
// ArrayBuffer before transferring it, otherwise the worker's wasm memory
// itself would be detached.
const transferable = encoded.slice();
self.postMessage({ ok: true, data: transferable.buffer }, [transferable.buffer]);
} catch (error) {
self.postMessage({
ok: false,
error: error instanceof Error ? error.message : String(error),
});
}
};