perf: keep heavy work off UI thread
This commit is contained in:
parent
12bf126646
commit
deec48ad99
75 changed files with 11703 additions and 519 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -5,6 +5,7 @@ target
|
||||||
|
|
||||||
# Trunk web build output (wasm/js bundle)
|
# Trunk web build output (wasm/js bundle)
|
||||||
dist
|
dist
|
||||||
|
web/worker_pkg
|
||||||
|
|
||||||
# These are backup files generated by rustfmt
|
# These are backup files generated by rustfmt
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
|
|
|
||||||
16
Cargo.lock
generated
16
Cargo.lock
generated
|
|
@ -17,6 +17,7 @@ name = "OpenCADStudio"
|
||||||
version = "0.8.7"
|
version = "0.8.7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"acadrust",
|
"acadrust",
|
||||||
|
"bincode",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"clap",
|
"clap",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
|
|
@ -74,7 +75,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "acadrust"
|
name = "acadrust"
|
||||||
version = "0.4.0"
|
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 = [
|
dependencies = [
|
||||||
"ahash 0.8.12",
|
"ahash 0.8.12",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|
@ -2305,8 +2306,6 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_wgpu"
|
name = "iced_wgpu"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ff144a999b0ca0f8a10257934500060240825c42e950ec0ebee9c8ae30561c13"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.1",
|
"bitflags 2.13.1",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
|
|
@ -3667,6 +3666,17 @@ dependencies = [
|
||||||
"thiserror 1.0.69",
|
"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]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.4"
|
version = "1.21.4"
|
||||||
|
|
|
||||||
15
Cargo.toml
15
Cargo.toml
|
|
@ -11,6 +11,7 @@ build = "build.rs"
|
||||||
# verifies it.
|
# verifies it.
|
||||||
members = [
|
members = [
|
||||||
"crates/ocs_plugin_api",
|
"crates/ocs_plugin_api",
|
||||||
|
"crates/ocs_web_worker",
|
||||||
"crates/dwg-thumbnailer",
|
"crates/dwg-thumbnailer",
|
||||||
"crates/dwg-thumbnailer-win",
|
"crates/dwg-thumbnailer-win",
|
||||||
]
|
]
|
||||||
|
|
@ -57,7 +58,7 @@ rfd = "0.17"
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
# Opt-in logging via --log / RUST_LOG (surfaces wgpu / iced / winit diagnostics).
|
# Opt-in logging via --log / RUST_LOG (surfaces wgpu / iced / winit diagnostics).
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
acadrust = "0.4"
|
acadrust = { version = "0.4", features = ["serde"] }
|
||||||
# Shared DWG embedded-preview extraction (Start-page + file-manager thumbnails).
|
# Shared DWG embedded-preview extraction (Start-page + file-manager thumbnails).
|
||||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
|
|
@ -90,7 +91,11 @@ windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_Window
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# Track the verified DWG round-trip, I/O, and unified PERF fixes.
|
# 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]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
# Native enables the plugin host runtime (out-of-process plugins).
|
# Native enables the plugin host runtime (out-of-process plugins).
|
||||||
|
|
@ -119,6 +124,7 @@ wasm-bindgen = "0.2"
|
||||||
# web font loader (#141).
|
# web font loader (#141).
|
||||||
wasm-bindgen-futures = "0.4"
|
wasm-bindgen-futures = "0.4"
|
||||||
js-sys = "0.3"
|
js-sys = "0.3"
|
||||||
|
bincode = "1.3"
|
||||||
# window.open (external URLs) + Blob/anchor file downloads (Save) + fetch()
|
# window.open (external URLs) + Blob/anchor file downloads (Save) + fetch()
|
||||||
# (Response) for the lazy per-script web font loader (#141).
|
# (Response) for the lazy per-script web font loader (#141).
|
||||||
web-sys = { version = "0.3", features = [
|
web-sys = { version = "0.3", features = [
|
||||||
|
|
@ -130,6 +136,11 @@ web-sys = { version = "0.3", features = [
|
||||||
"Blob",
|
"Blob",
|
||||||
"Url",
|
"Url",
|
||||||
"Response",
|
"Response",
|
||||||
|
"Worker",
|
||||||
|
"WorkerOptions",
|
||||||
|
"WorkerType",
|
||||||
|
"MessageEvent",
|
||||||
|
"ErrorEvent",
|
||||||
# Async clipboard read for paste into the text/MText editors (iced's own
|
# Async clipboard read for paste into the text/MText editors (iced's own
|
||||||
# clipboard read is a no-op on the web).
|
# clipboard read is a no-op on the web).
|
||||||
"Clipboard",
|
"Clipboard",
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,11 @@ target = "index.html"
|
||||||
# on the rust <link> in index.html (truck-meshalgo → lzma-sys C deps cannot
|
# on the rust <link> in index.html (truck-meshalgo → lzma-sys C deps cannot
|
||||||
# cross-compile to wasm).
|
# cross-compile to wasm).
|
||||||
|
|
||||||
|
[[hooks]]
|
||||||
|
stage = "pre_build"
|
||||||
|
command = "sh"
|
||||||
|
command_arguments = ["scripts/build-web-worker.sh"]
|
||||||
|
|
||||||
[serve]
|
[serve]
|
||||||
# Cross-origin isolation headers enable SharedArrayBuffer, which wasm threads
|
# Cross-origin isolation headers enable SharedArrayBuffer, which wasm threads
|
||||||
# (rayon) need. GitHub Pages cannot set these; a self-hosted server (e.g. the
|
# (rayon) need. GitHub Pages cannot set these; a self-hosted server (e.g. the
|
||||||
|
|
|
||||||
146
crates/iced_wgpu/Cargo.toml
Normal file
146
crates/iced_wgpu/Cargo.toml
Normal 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
19
crates/iced_wgpu/LICENSE
Normal 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.
|
||||||
8
crates/iced_wgpu/OCS_PATCH.md
Normal file
8
crates/iced_wgpu/OCS_PATCH.md
Normal 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.
|
||||||
20
crates/iced_wgpu/README.md
Normal file
20
crates/iced_wgpu/README.md
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
# `iced_wgpu`
|
||||||
|
[][documentation]
|
||||||
|
[](https://crates.io/crates/iced_wgpu)
|
||||||
|
[](https://github.com/iced-rs/iced/blob/master/LICENSE)
|
||||||
|
[](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
|
||||||
132
crates/iced_wgpu/src/buffer.rs
Normal file
132
crates/iced_wgpu/src/buffer.rs
Normal 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)
|
||||||
|
}
|
||||||
205
crates/iced_wgpu/src/color.rs
Normal file
205
crates/iced_wgpu/src/color.rs
Normal 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
|
||||||
|
}
|
||||||
78
crates/iced_wgpu/src/engine.rs
Normal file
78
crates/iced_wgpu/src/engine.rs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
785
crates/iced_wgpu/src/geometry.rs
Normal file
785
crates/iced_wgpu/src/geometry.rs
Normal 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),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
542
crates/iced_wgpu/src/image/atlas.rs
Normal file
542
crates/iced_wgpu/src/image/atlas.rs
Normal 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,
|
||||||
|
),
|
||||||
|
}],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
52
crates/iced_wgpu/src/image/atlas/allocation.rs
Normal file
52
crates/iced_wgpu/src/image/atlas/allocation.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
108
crates/iced_wgpu/src/image/atlas/allocator.rs
Normal file
108
crates/iced_wgpu/src/image/atlas/allocator.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
27
crates/iced_wgpu/src/image/atlas/entry.rs
Normal file
27
crates/iced_wgpu/src/image/atlas/entry.rs
Normal 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,
|
||||||
|
}
|
||||||
22
crates/iced_wgpu/src/image/atlas/layer.rs
Normal file
22
crates/iced_wgpu/src/image/atlas/layer.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
636
crates/iced_wgpu/src/image/cache.rs
Normal file
636
crates/iced_wgpu/src/image/cache.rs
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
758
crates/iced_wgpu/src/image/mod.rs
Normal file
758
crates/iced_wgpu/src/image/mod.rs
Normal 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);
|
||||||
|
}
|
||||||
16
crates/iced_wgpu/src/image/null.rs
Normal file
16
crates/iced_wgpu/src/image/null.rs
Normal 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) {}
|
||||||
|
}
|
||||||
123
crates/iced_wgpu/src/image/raster.rs
Normal file
123
crates/iced_wgpu/src/image/raster.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
231
crates/iced_wgpu/src/image/vector.rs
Normal file
231
crates/iced_wgpu/src/image/vector.rs
Normal 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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
405
crates/iced_wgpu/src/layer.rs
Normal file
405
crates/iced_wgpu/src/layer.rs
Normal 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
984
crates/iced_wgpu/src/lib.rs
Normal file
|
|
@ -0,0 +1,984 @@
|
||||||
|
//! A [`wgpu`] renderer for [Iced].
|
||||||
|
//!
|
||||||
|
//! 
|
||||||
|
//!
|
||||||
|
//! [`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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
242
crates/iced_wgpu/src/primitive.rs
Normal file
242
crates/iced_wgpu/src/primitive.rs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
362
crates/iced_wgpu/src/quad.rs
Normal file
362
crates/iced_wgpu/src/quad.rs
Normal 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],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
187
crates/iced_wgpu/src/quad/gradient.rs
Normal file
187
crates/iced_wgpu/src/quad/gradient.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
162
crates/iced_wgpu/src/quad/solid.rs
Normal file
162
crates/iced_wgpu/src/quad/solid.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
84
crates/iced_wgpu/src/settings.rs
Normal file
84
crates/iced_wgpu/src/settings.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
37
crates/iced_wgpu/src/shader/blit.wgsl
Normal file
37
crates/iced_wgpu/src/shader/blit.wgsl
Normal 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);
|
||||||
|
}
|
||||||
14
crates/iced_wgpu/src/shader/color.wgsl
Normal file
14
crates/iced_wgpu/src/shader/color.wgsl
Normal 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);
|
||||||
|
}
|
||||||
3
crates/iced_wgpu/src/shader/color/linear_rgb.wgsl
Normal file
3
crates/iced_wgpu/src/shader/color/linear_rgb.wgsl
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
fn interpolate_color(from_: vec4<f32>, to_: vec4<f32>, factor: f32) -> vec4<f32> {
|
||||||
|
return mix(from_, to_, factor);
|
||||||
|
}
|
||||||
129
crates/iced_wgpu/src/shader/image.wgsl
Normal file
129
crates/iced_wgpu/src/shader/image.wgsl
Normal 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;
|
||||||
|
}
|
||||||
13
crates/iced_wgpu/src/shader/quad.wgsl
Normal file
13
crates/iced_wgpu/src/shader/quad.wgsl
Normal 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;
|
||||||
|
}
|
||||||
184
crates/iced_wgpu/src/shader/quad/gradient.wgsl
Normal file
184
crates/iced_wgpu/src/shader/quad/gradient.wgsl
Normal 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);
|
||||||
|
}
|
||||||
102
crates/iced_wgpu/src/shader/quad/solid.wgsl
Normal file
102
crates/iced_wgpu/src/shader/quad/solid.wgsl
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
crates/iced_wgpu/src/shader/triangle.wgsl
Normal file
5
crates/iced_wgpu/src/shader/triangle.wgsl
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
struct Globals {
|
||||||
|
transform: mat4x4<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
@group(0) @binding(0) var<uniform> globals: Globals;
|
||||||
127
crates/iced_wgpu/src/shader/triangle/gradient.wgsl
Normal file
127
crates/iced_wgpu/src/shader/triangle/gradient.wgsl
Normal 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);
|
||||||
|
}
|
||||||
24
crates/iced_wgpu/src/shader/triangle/solid.wgsl
Normal file
24
crates/iced_wgpu/src/shader/triangle/solid.wgsl
Normal 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;
|
||||||
|
}
|
||||||
7
crates/iced_wgpu/src/shader/vertex.wgsl
Normal file
7
crates/iced_wgpu/src/shader/vertex.wgsl
Normal 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));
|
||||||
|
}
|
||||||
648
crates/iced_wgpu/src/text.rs
Normal file
648
crates/iced_wgpu/src/text.rs
Normal 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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
963
crates/iced_wgpu/src/triangle.rs
Normal file
963
crates/iced_wgpu/src/triangle.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
371
crates/iced_wgpu/src/triangle/msaa.rs
Normal file
371
crates/iced_wgpu/src/triangle/msaa.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
5
crates/iced_wgpu/src/window.rs
Normal file
5
crates/iced_wgpu/src/window.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
//! Display rendering results on windows.
|
||||||
|
pub mod compositor;
|
||||||
|
|
||||||
|
pub use compositor::Compositor;
|
||||||
|
pub use wgpu::Surface;
|
||||||
358
crates/iced_wgpu/src/window/compositor.rs
Normal file
358
crates/iced_wgpu/src/window/compositor.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
15
crates/ocs_web_worker/Cargo.toml
Normal file
15
crates/ocs_web_worker/Cargo.toml
Normal 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"
|
||||||
32
crates/ocs_web_worker/src/lib.rs
Normal file
32
crates/ocs_web_worker/src/lib.rs
Normal 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()))
|
||||||
|
}
|
||||||
|
|
@ -45,6 +45,8 @@
|
||||||
<!-- Per-script Noto subsets, fetched lazily at runtime (one alphabet per
|
<!-- Per-script Noto subsets, fetched lazily at runtime (one alphabet per
|
||||||
file) so CAD text renders non-Latin scripts on the web. (#141) -->
|
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-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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="loading">
|
<div id="loading">
|
||||||
|
|
|
||||||
10
scripts/build-web-worker.sh
Normal file
10
scripts/build-web-worker.sh
Normal 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
|
||||||
|
|
@ -1052,9 +1052,10 @@ impl OpenCADStudio {
|
||||||
self.tabs[i].active_cmd = None;
|
self.tabs[i].active_cmd = None;
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
self.tabs[i].scene.clear_preview_wire();
|
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].scene.create_group(name.clone(), handles);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
self.commit_group_undo(i, undo);
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_info(&format!("Group \"{}\" created.", name));
|
.push_info(&format!("Group \"{}\" created.", name));
|
||||||
}
|
}
|
||||||
|
|
@ -1062,9 +1063,10 @@ impl OpenCADStudio {
|
||||||
self.tabs[i].active_cmd = None;
|
self.tabs[i].active_cmd = None;
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
self.tabs[i].scene.clear_preview_wire();
|
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);
|
let count = self.tabs[i].scene.delete_groups_containing(&handles);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
self.commit_group_undo(i, undo);
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_info(&format!("{} group(s) dissolved.", count));
|
.push_info(&format!("{} group(s) dissolved.", count));
|
||||||
|
|
|
||||||
|
|
@ -817,10 +817,15 @@ impl OpenCADStudio {
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_error(&format!("DIMSTYLE: '{}' already exists.", name));
|
.push_error(&format!("DIMSTYLE: '{}' already exists.", name));
|
||||||
} else {
|
} else {
|
||||||
|
let undo = self.begin_dim_style_undo(
|
||||||
|
i,
|
||||||
|
"DIMSTYLE NEW",
|
||||||
|
std::slice::from_ref(&name),
|
||||||
|
);
|
||||||
let style = DimStyle::new(&name);
|
let style = DimStyle::new(&name);
|
||||||
let _ = self.tabs[i].scene.document.dim_styles.add(style);
|
let _ = self.tabs[i].scene.document.dim_styles.add(style);
|
||||||
self.push_undo_snapshot(i, "DIMSTYLE NEW");
|
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
self.commit_dim_style_undo(i, undo);
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(&format!("DIMSTYLE: '{}' created.", name));
|
.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 prop = parts.get(2).map(|s| s.to_lowercase()).unwrap_or_default();
|
||||||
let val_str = parts.get(3).map(|s| s.trim()).unwrap_or("");
|
let val_str = parts.get(3).map(|s| s.trim()).unwrap_or("");
|
||||||
if let Ok(val) = val_str.parse::<f64>() {
|
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) =
|
if let Some(ds) =
|
||||||
self.tabs[i].scene.document.dim_styles.get_mut(&style_name)
|
self.tabs[i].scene.document.dim_styles.get_mut(&style_name)
|
||||||
{
|
{
|
||||||
|
|
@ -882,8 +892,10 @@ impl OpenCADStudio {
|
||||||
return Some(Task::none());
|
return Some(Task::none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.push_undo_snapshot(i, "DIMSTYLE SET");
|
|
||||||
self.tabs[i].dirty = true;
|
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!(
|
self.command_line.push_output(&format!(
|
||||||
"DIMSTYLE: '{style_name}'.{prop} = {val:.3}"
|
"DIMSTYLE: '{style_name}'.{prop} = {val:.3}"
|
||||||
));
|
));
|
||||||
|
|
@ -1121,10 +1133,15 @@ impl OpenCADStudio {
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_error(&format!("{prefix}: style '{name}' already exists."));
|
.push_error(&format!("{prefix}: style '{name}' already exists."));
|
||||||
} else {
|
} else {
|
||||||
|
let undo = self.begin_text_style_undo(
|
||||||
|
i,
|
||||||
|
"STYLE NEW",
|
||||||
|
std::slice::from_ref(&name),
|
||||||
|
);
|
||||||
let style = acadrust::tables::TextStyle::new(&name);
|
let style = acadrust::tables::TextStyle::new(&name);
|
||||||
let _ = self.tabs[i].scene.document.text_styles.add(style);
|
let _ = self.tabs[i].scene.document.text_styles.add(style);
|
||||||
self.push_undo_snapshot(i, "STYLE NEW");
|
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
self.commit_text_style_undo(i, undo);
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(&format!("{prefix}: style '{name}' created."));
|
.push_output(&format!("{prefix}: style '{name}' created."));
|
||||||
}
|
}
|
||||||
|
|
@ -1136,18 +1153,28 @@ impl OpenCADStudio {
|
||||||
if style_name.is_empty() || font.is_empty() {
|
if style_name.is_empty() || font.is_empty() {
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_error(&format!("Usage: {prefix} FONT <style> <font_file>"));
|
.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)
|
self.tabs[i].scene.document.text_styles.get_mut(&style_name)
|
||||||
{
|
{
|
||||||
s.font_file = font.clone();
|
style.font_file = font.clone();
|
||||||
self.push_undo_snapshot(i, "STYLE FONT");
|
|
||||||
self.tabs[i].dirty = true;
|
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!(
|
self.command_line.push_output(&format!(
|
||||||
"{prefix}: '{style_name}' font set to '{font}'."
|
"{prefix}: '{style_name}' font set to '{font}'."
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_error(&format!("{prefix}: style '{style_name}' not found."));
|
.push_error(&format!("{prefix}: style '{style_name}' not found."));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"WIDTH" | "W" => {
|
"WIDTH" | "W" => {
|
||||||
|
|
@ -1155,12 +1182,20 @@ impl OpenCADStudio {
|
||||||
let style_name = parts.get(1).map(|s| s.trim()).unwrap_or("").to_string();
|
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("");
|
let factor_str = parts.get(2).map(|s| s.trim()).unwrap_or("");
|
||||||
if let Ok(factor) = factor_str.parse::<f64>() {
|
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)
|
self.tabs[i].scene.document.text_styles.get_mut(&style_name)
|
||||||
{
|
{
|
||||||
s.width_factor = factor;
|
style.width_factor = factor;
|
||||||
self.push_undo_snapshot(i, "STYLE WIDTH");
|
|
||||||
self.tabs[i].dirty = true;
|
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!(
|
self.command_line.push_output(&format!(
|
||||||
"{prefix}: '{style_name}' width factor set to {factor:.3}."
|
"{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 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("");
|
let angle_str = parts.get(2).map(|s| s.trim()).unwrap_or("");
|
||||||
if let Ok(deg) = angle_str.parse::<f64>() {
|
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)
|
self.tabs[i].scene.document.text_styles.get_mut(&style_name)
|
||||||
{
|
{
|
||||||
s.oblique_angle = deg.to_radians();
|
style.oblique_angle = deg.to_radians();
|
||||||
self.push_undo_snapshot(i, "STYLE OBLIQUE");
|
|
||||||
self.tabs[i].dirty = true;
|
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!(
|
self.command_line.push_output(&format!(
|
||||||
"{prefix}: '{style_name}' oblique angle set to {deg:.1}°."
|
"{prefix}: '{style_name}' oblique angle set to {deg:.1}°."
|
||||||
));
|
));
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@ impl OpenCADStudio {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(_, e)| e.common().layer.clone())
|
.map(|(_, e)| e.common().layer.clone())
|
||||||
.collect();
|
.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 {
|
for name in &layers {
|
||||||
if name == "0" {
|
if name == "0" {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -32,8 +33,9 @@ impl OpenCADStudio {
|
||||||
dl.turn_off();
|
dl.turn_off();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
self.commit_layer_undo(i, undo);
|
||||||
self.refresh_layer_panel();
|
self.refresh_layer_panel();
|
||||||
self.command_line.push_info("Layer(s) turned off.");
|
self.command_line.push_info("Layer(s) turned off.");
|
||||||
}
|
}
|
||||||
|
|
@ -58,7 +60,8 @@ impl OpenCADStudio {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(_, e)| e.common().layer.clone())
|
.map(|(_, e)| e.common().layer.clone())
|
||||||
.collect();
|
.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 {
|
for name in &layers {
|
||||||
if name == "0" {
|
if name == "0" {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -67,8 +70,9 @@ impl OpenCADStudio {
|
||||||
dl.freeze();
|
dl.freeze();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
self.commit_layer_undo(i, undo);
|
||||||
self.refresh_layer_panel();
|
self.refresh_layer_panel();
|
||||||
self.command_line.push_info("Layer(s) frozen.");
|
self.command_line.push_info("Layer(s) frozen.");
|
||||||
}
|
}
|
||||||
|
|
@ -266,10 +270,18 @@ impl OpenCADStudio {
|
||||||
"LAYERSTATE: no saved state named \"{arg}\"."
|
"LAYERSTATE: no saved state named \"{arg}\"."
|
||||||
));
|
));
|
||||||
} else {
|
} 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);
|
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.tabs[i].dirty = true;
|
||||||
|
self.commit_layer_undo(i, undo);
|
||||||
self.refresh_layer_panel();
|
self.refresh_layer_panel();
|
||||||
self.command_line.push_output(&format!(
|
self.command_line.push_output(&format!(
|
||||||
"LAYERSTATE: restored \"{arg}\" ({n} layer(s))."
|
"LAYERSTATE: restored \"{arg}\" ({n} layer(s))."
|
||||||
|
|
@ -312,14 +324,16 @@ impl OpenCADStudio {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(_, e)| e.common().layer.clone())
|
.map(|(_, e)| e.common().layer.clone())
|
||||||
.collect();
|
.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 {
|
for name in &layers {
|
||||||
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
||||||
dl.lock();
|
dl.lock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.bump_geometry();
|
// Layer locking changes editability only.
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
self.commit_layer_undo(i, undo);
|
||||||
self.refresh_layer_panel();
|
self.refresh_layer_panel();
|
||||||
self.command_line.push_info("Layer(s) locked.");
|
self.command_line.push_info("Layer(s) locked.");
|
||||||
}
|
}
|
||||||
|
|
@ -359,41 +373,43 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
|
|
||||||
"LAYON" => {
|
"LAYON" => {
|
||||||
self.push_undo_snapshot(i, "LAYON");
|
let names = self.tabs[i]
|
||||||
for name in self.tabs[i]
|
|
||||||
.scene
|
.scene
|
||||||
.document
|
.document
|
||||||
.layers
|
.layers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|l| l.name.clone())
|
.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) {
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
|
||||||
dl.turn_on();
|
dl.turn_on();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
self.commit_layer_undo(i, undo);
|
||||||
self.refresh_layer_panel();
|
self.refresh_layer_panel();
|
||||||
self.command_line.push_info("All layers turned on.");
|
self.command_line.push_info("All layers turned on.");
|
||||||
}
|
}
|
||||||
|
|
||||||
"LAYTHW" => {
|
"LAYTHW" => {
|
||||||
self.push_undo_snapshot(i, "LAYTHW");
|
let names = self.tabs[i]
|
||||||
for name in self.tabs[i]
|
|
||||||
.scene
|
.scene
|
||||||
.document
|
.document
|
||||||
.layers
|
.layers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|l| l.name.clone())
|
.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) {
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
|
||||||
dl.thaw();
|
dl.thaw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
self.commit_layer_undo(i, undo);
|
||||||
self.refresh_layer_panel();
|
self.refresh_layer_panel();
|
||||||
self.command_line.push_info("All layers thawed.");
|
self.command_line.push_info("All layers thawed.");
|
||||||
}
|
}
|
||||||
|
|
@ -417,14 +433,16 @@ impl OpenCADStudio {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(_, e)| e.common().layer.clone())
|
.map(|(_, e)| e.common().layer.clone())
|
||||||
.collect();
|
.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 {
|
for name in &layers {
|
||||||
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
||||||
dl.unlock();
|
dl.unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.bump_geometry();
|
// Layer unlocking changes editability only.
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
self.commit_layer_undo(i, undo);
|
||||||
self.refresh_layer_panel();
|
self.refresh_layer_panel();
|
||||||
self.command_line.push_info("Layer(s) unlocked.");
|
self.command_line.push_info("Layer(s) unlocked.");
|
||||||
}
|
}
|
||||||
|
|
@ -442,7 +460,6 @@ impl OpenCADStudio {
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_error("LAYISO: select entities on the layers to isolate first.");
|
.push_error("LAYISO: select entities on the layers to isolate first.");
|
||||||
} else {
|
} else {
|
||||||
self.push_undo_snapshot(i, "LAYISO");
|
|
||||||
let names: Vec<String> = self.tabs[i]
|
let names: Vec<String> = self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
.document
|
.document
|
||||||
|
|
@ -450,15 +467,17 @@ impl OpenCADStudio {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|l| l.name.clone())
|
.map(|l| l.name.clone())
|
||||||
.collect();
|
.collect();
|
||||||
for name in names {
|
let undo = self.begin_layer_undo(i, "LAYISO", &names);
|
||||||
if !sel_layers.contains(&name) {
|
for name in &names {
|
||||||
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
|
if !sel_layers.contains(name) {
|
||||||
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
||||||
dl.turn_off();
|
dl.turn_off();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
self.commit_layer_undo(i, undo);
|
||||||
self.refresh_layer_panel();
|
self.refresh_layer_panel();
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_info(&format!("LAYISO: isolated {} layer(s).", sel_layers.len()));
|
.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 — restore all layers that were turned off by LAYISO (turn all on)
|
||||||
"LAYUNISO" => {
|
"LAYUNISO" => {
|
||||||
self.push_undo_snapshot(i, "LAYUNISO");
|
|
||||||
let names: Vec<String> = self.tabs[i]
|
let names: Vec<String> = self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
.document
|
.document
|
||||||
|
|
@ -519,13 +537,15 @@ impl OpenCADStudio {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|l| l.name.clone())
|
.map(|l| l.name.clone())
|
||||||
.collect();
|
.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) {
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
|
||||||
dl.turn_on();
|
dl.turn_on();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
self.commit_layer_undo(i, undo);
|
||||||
self.refresh_layer_panel();
|
self.refresh_layer_panel();
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_info("LAYUNISO: all layers restored.");
|
.push_info("LAYUNISO: all layers restored.");
|
||||||
|
|
@ -587,9 +607,10 @@ impl OpenCADStudio {
|
||||||
self.command_line.push_info(&cmd.prompt());
|
self.command_line.push_info(&cmd.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||||
} else {
|
} 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);
|
let count = self.tabs[i].scene.delete_groups_containing(&handles);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
self.commit_group_undo(i, undo);
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_info(&format!("{} group(s) dissolved.", count));
|
.push_info(&format!("{} group(s) dissolved.", count));
|
||||||
|
|
|
||||||
|
|
@ -520,7 +520,7 @@ impl HistorySnapshot {
|
||||||
.saturating_add(
|
.saturating_add(
|
||||||
d.structure
|
d.structure
|
||||||
.as_ref()
|
.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_before.len().saturating_mul(16))
|
||||||
.saturating_add(d.selected_after.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,
|
pub(super) dirty_after: bool,
|
||||||
/// Opposite non-entity document state. `apply_delta_state` swaps this with
|
/// Opposite non-entity document state. `apply_delta_state` swaps this with
|
||||||
/// the live structure, so the same allocation shuttles between undo/redo.
|
/// 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,
|
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)]
|
#[derive(Default)]
|
||||||
pub(super) struct HistoryState {
|
pub(super) struct HistoryState {
|
||||||
pub(super) undo_stack: Vec<HistorySnapshot>,
|
pub(super) undo_stack: Vec<HistorySnapshot>,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
use super::{
|
use super::{
|
||||||
document::{DeltaSnapshot, HistorySnapshot, PendingHistorySnapshot},
|
document::{
|
||||||
|
DeltaSnapshot, HistorySnapshot, ObjectEntryDelta, PendingHistorySnapshot,
|
||||||
|
StructureSnapshot, TableEntryDelta,
|
||||||
|
},
|
||||||
OpenCADStudio,
|
OpenCADStudio,
|
||||||
};
|
};
|
||||||
use acadrust::{EntityType, Handle};
|
use acadrust::{EntityType, Handle};
|
||||||
|
|
@ -71,6 +74,38 @@ pub(super) struct PendingDelta {
|
||||||
structure_before: Option<acadrust::CadDocument>,
|
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 {
|
impl OpenCADStudio {
|
||||||
pub(super) fn history_label_from_active_cmd(&self, i: usize, fallback: &'static str) -> String {
|
pub(super) fn history_label_from_active_cmd(&self, i: usize, fallback: &'static str) -> String {
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
|
|
@ -248,7 +283,7 @@ impl OpenCADStudio {
|
||||||
selected_after,
|
selected_after,
|
||||||
dirty_before: pending.dirty_before,
|
dirty_before: pending.dirty_before,
|
||||||
dirty_after,
|
dirty_after,
|
||||||
structure: structure_changed.then_some(pending.structure_before),
|
structure: structure_changed.then_some(StructureSnapshot::Full(pending.structure_before)),
|
||||||
label: pending.label,
|
label: pending.label,
|
||||||
};
|
};
|
||||||
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
|
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
|
/// Copy is delta-safe only when no target is a Dimension and no complete
|
||||||
/// group is copied: dimensions clone fresh anonymous `*D` block records,
|
/// group is copied: dimensions clone fresh anonymous `*D` block records,
|
||||||
/// and complete group copies add Group objects / dictionary entries. Both
|
/// and complete group copies add Group objects / dictionary entries. Both
|
||||||
|
|
@ -397,11 +689,12 @@ impl OpenCADStudio {
|
||||||
.collect();
|
.collect();
|
||||||
let selected_after = self.tabs[i].scene.selected.iter().copied().collect();
|
let selected_after = self.tabs[i].scene.selected.iter().copied().collect();
|
||||||
let dirty_after = self.tabs[i].dirty;
|
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 Some(before_structure) = structure.as_mut() {
|
||||||
let after_structure = self.tabs[i].scene.document.snapshot_structure();
|
if let StructureSnapshot::Full(before_structure) = before_structure {
|
||||||
let added_handles: Vec<Handle> = entities
|
let after_structure = self.tabs[i].scene.document.snapshot_structure();
|
||||||
.iter()
|
let added_handles: Vec<Handle> = entities
|
||||||
|
.iter()
|
||||||
.filter_map(|(handle, before, after)| {
|
.filter_map(|(handle, before, after)| {
|
||||||
(before.is_none() && after.is_some()).then_some(*handle)
|
(before.is_none() && after.is_some()).then_some(*handle)
|
||||||
})
|
})
|
||||||
|
|
@ -414,6 +707,7 @@ impl OpenCADStudio {
|
||||||
if *before_structure == after_structure {
|
if *before_structure == after_structure {
|
||||||
structure = None;
|
structure = None;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let delta = DeltaSnapshot {
|
let delta = DeltaSnapshot {
|
||||||
entities,
|
entities,
|
||||||
|
|
@ -443,9 +737,113 @@ impl OpenCADStudio {
|
||||||
// Install the chosen side of every entity image. Derived-cache, geometry
|
// Install the chosen side of every entity image. Derived-cache, geometry
|
||||||
// and UI invalidation are deferred until every requested undo/redo step
|
// and UI invalidation are deferred until every requested undo/redo step
|
||||||
// has been applied.
|
// has been applied.
|
||||||
if let Some(structure) = d.structure.take() {
|
if let Some(structure) = d.structure.as_mut() {
|
||||||
let inverse = self.tabs[i].scene.document.swap_structure(structure);
|
match structure {
|
||||||
d.structure = Some(inverse);
|
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 changes = self.tabs[i].scene.apply_entity_delta(&d.entities, undo);
|
||||||
let scene = &mut self.tabs[i].scene;
|
let scene = &mut self.tabs[i].scene;
|
||||||
|
|
@ -479,6 +877,7 @@ impl OpenCADStudio {
|
||||||
&mut self,
|
&mut self,
|
||||||
i: usize,
|
i: usize,
|
||||||
had_full: bool,
|
had_full: bool,
|
||||||
|
structure_changed: bool,
|
||||||
changes: &[(Handle, crate::scene::ChangeKind)],
|
changes: &[(Handle, crate::scene::ChangeKind)],
|
||||||
) {
|
) {
|
||||||
self.tabs[i].edit_revision = self.tabs[i].edit_revision.wrapping_add(1);
|
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].active_cmd = None;
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
self.tabs[i].active_grip = None;
|
self.tabs[i].active_grip = None;
|
||||||
if had_full {
|
if structure_changed {
|
||||||
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
||||||
let vp_info = self.tabs[i].scene.viewport_list();
|
let vp_info = self.tabs[i].scene.viewport_list();
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
|
|
@ -555,6 +954,7 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
let mut last_label = String::new();
|
let mut last_label = String::new();
|
||||||
let mut had_full = false;
|
let mut had_full = false;
|
||||||
|
let mut structure_changed = false;
|
||||||
let mut changes = Vec::new();
|
let mut changes = Vec::new();
|
||||||
for _ in 0..steps {
|
for _ in 0..steps {
|
||||||
let Some(snapshot) = self.tabs[i].history.undo_stack.pop() else {
|
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
|
// Symmetric: undo applies the before side, then the same
|
||||||
// delta rides to the redo stack (it still holds the after
|
// delta rides to the redo stack (it still holds the after
|
||||||
// side) — no current-state capture needed.
|
// 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));
|
changes.extend(self.apply_delta_state(i, &mut d, true));
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
.history
|
.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
|
self.command_line
|
||||||
.push_output(&format!("Undo: {last_label}"));
|
.push_output(&format!("Undo: {last_label}"));
|
||||||
}
|
}
|
||||||
|
|
@ -592,6 +993,7 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
let mut last_label = String::new();
|
let mut last_label = String::new();
|
||||||
let mut had_full = false;
|
let mut had_full = false;
|
||||||
|
let mut structure_changed = false;
|
||||||
let mut changes = Vec::new();
|
let mut changes = Vec::new();
|
||||||
for _ in 0..steps {
|
for _ in 0..steps {
|
||||||
let Some(snapshot) = self.tabs[i].history.redo_stack.pop() else {
|
let Some(snapshot) = self.tabs[i].history.redo_stack.pop() else {
|
||||||
|
|
@ -600,7 +1002,8 @@ impl OpenCADStudio {
|
||||||
last_label = snapshot.label().to_string();
|
last_label = snapshot.label().to_string();
|
||||||
match snapshot {
|
match snapshot {
|
||||||
HistorySnapshot::Delta(mut d) => {
|
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));
|
changes.extend(self.apply_delta_state(i, &mut d, false));
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
.history
|
.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
|
self.command_line
|
||||||
.push_output(&format!("Redo: {last_label}"));
|
.push_output(&format!("Redo: {last_label}"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,6 @@ use acadrust::CadDocument;
|
||||||
use iced::time::Instant;
|
use iced::time::Instant;
|
||||||
use iced::window;
|
use iced::window;
|
||||||
use iced::{mouse, Point, Task, Theme};
|
use iced::{mouse, Point, Task, Theme};
|
||||||
use std::sync::atomic::AtomicU8;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub(super) const POLY_START_DELAY_MS: u128 = 150;
|
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.
|
// loader thread, read by the UI overlay on every frame.
|
||||||
pub const OPEN_PHASE_READING: u8 = 0;
|
pub const OPEN_PHASE_READING: u8 = 0;
|
||||||
pub const OPEN_PHASE_PARSING: u8 = 1;
|
pub const OPEN_PHASE_PARSING: u8 = 1;
|
||||||
pub const OPEN_PHASE_CACHING: u8 = 2;
|
pub const OPEN_PHASE_XREF: u8 = 2;
|
||||||
pub const OPEN_PHASE_FINALIZING: u8 = 3;
|
pub const OPEN_PHASE_CACHING: u8 = 3;
|
||||||
|
pub const OPEN_PHASE_FINALIZING: u8 = 4;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct OpenProgress {
|
pub struct OpenProgress {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub size_bytes: u64,
|
pub size_bytes: u64,
|
||||||
pub phase: Arc<AtomicU8>,
|
pub state: Arc<crate::io::OpenProgressState>,
|
||||||
pub started: Instant,
|
pub started: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2098,6 +2098,9 @@ pub enum Message {
|
||||||
PlotWindowExport,
|
PlotWindowExport,
|
||||||
/// Callback after the user picks (or cancels) the window-export path.
|
/// Callback after the user picks (or cancels) the window-export path.
|
||||||
PlotWindowExportPath(Option<std::path::PathBuf>),
|
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).
|
/// Send current layout to the system printer (via lp / lpr).
|
||||||
PrintToPrinter,
|
PrintToPrinter,
|
||||||
/// Callback from the async printer job.
|
/// Callback from the async printer job.
|
||||||
|
|
@ -2299,6 +2302,8 @@ pub enum Message {
|
||||||
WblockSave(String),
|
WblockSave(String),
|
||||||
/// Result of the WBLOCK save path dialog.
|
/// Result of the WBLOCK save path dialog.
|
||||||
WblockSaveResult(String, Option<std::path::PathBuf>),
|
WblockSaveResult(String, Option<std::path::PathBuf>),
|
||||||
|
/// Background extraction/write completion.
|
||||||
|
WblockWriteFinished(String, std::path::PathBuf, Result<(), String>),
|
||||||
// ── DATAEXTRACTION ────────────────────────────────────────────────────
|
// ── DATAEXTRACTION ────────────────────────────────────────────────────
|
||||||
/// Save the pre-built CSV string to a file chosen by the user.
|
/// Save the pre-built CSV string to a file chosen by the user.
|
||||||
DataExtractionSave(String),
|
DataExtractionSave(String),
|
||||||
|
|
@ -2309,16 +2314,23 @@ pub enum Message {
|
||||||
StlExport,
|
StlExport,
|
||||||
/// Callback after the user picks (or cancels) the STL save path.
|
/// Callback after the user picks (or cancels) the STL save path.
|
||||||
StlExportPath(Option<std::path::PathBuf>),
|
StlExportPath(Option<std::path::PathBuf>),
|
||||||
|
StlExportFinished(std::path::PathBuf, Result<(), String>),
|
||||||
// ── STEP export ───────────────────────────────────────────────────────
|
// ── STEP export ───────────────────────────────────────────────────────
|
||||||
/// Trigger STEP AP203 export: show save dialog.
|
/// Trigger STEP AP203 export: show save dialog.
|
||||||
StepExport,
|
StepExport,
|
||||||
/// Callback after the user picks (or cancels) the STEP save path.
|
/// Callback after the user picks (or cancels) the STEP save path.
|
||||||
StepExportPath(Option<std::path::PathBuf>),
|
StepExportPath(Option<std::path::PathBuf>),
|
||||||
|
StepExportFinished(std::path::PathBuf, Result<(), String>),
|
||||||
// ── OBJ import ────────────────────────────────────────────────────────
|
// ── OBJ import ────────────────────────────────────────────────────────
|
||||||
/// Trigger OBJ import: show open-file dialog.
|
/// Trigger OBJ import: show open-file dialog.
|
||||||
ObjImport,
|
ObjImport,
|
||||||
/// Callback after the user picks (or cancels) the OBJ file path.
|
/// Callback after the user picks (or cancels) the OBJ file path.
|
||||||
ObjImportPath(Option<std::path::PathBuf>),
|
ObjImportPath(Option<std::path::PathBuf>),
|
||||||
|
ObjImportFinished(
|
||||||
|
u64,
|
||||||
|
std::path::PathBuf,
|
||||||
|
Result<crate::scene::model::mesh_model::MeshModel, String>,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OpenCADStudio {
|
impl OpenCADStudio {
|
||||||
|
|
|
||||||
|
|
@ -539,8 +539,7 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
/// Overwrite the live style state with a snapshot (used by commit's undo
|
/// Overwrite the live style state with a snapshot (used by commit's undo
|
||||||
/// dance and by discard).
|
/// dance and by discard).
|
||||||
fn restore_style_state(&mut self, snap: &StyleStateSnapshot) {
|
pub(super) fn restore_style_state(&mut self, i: usize, snap: &StyleStateSnapshot) {
|
||||||
let i = self.active_tab;
|
|
||||||
let doc = &mut self.tabs[i].scene.document;
|
let doc = &mut self.tabs[i].scene.document;
|
||||||
doc.text_styles = snap.text_styles.clone();
|
doc.text_styles = snap.text_styles.clone();
|
||||||
doc.dim_styles = snap.dim_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.multiline_style = snap.multiline_style.clone();
|
||||||
doc.header.current_table_style_name = snap.current_table.clone();
|
doc.header.current_table_style_name = snap.current_table.clone();
|
||||||
doc.header.current_mleader_style_name = snap.current_mleader.clone();
|
doc.header.current_mleader_style_name = snap.current_mleader.clone();
|
||||||
self.ribbon.active_table_style = snap.active_table.clone();
|
if i == self.active_tab {
|
||||||
self.ribbon.active_mleader_style = snap.active_mleader.clone();
|
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();
|
self.tabs[i].active_mleader_style = snap.tab_active_mleader.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -587,18 +588,26 @@ impl OpenCADStudio {
|
||||||
self.sync_ribbon_styles();
|
self.sync_ribbon_styles();
|
||||||
return;
|
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();
|
let edited = self.capture_style_state();
|
||||||
self.restore_style_state(&stage.baseline);
|
let changed = edited != stage.baseline;
|
||||||
self.push_undo_snapshot(i, "STYLE");
|
if changed {
|
||||||
self.restore_style_state(&edited);
|
self.tabs[i].dirty = true;
|
||||||
self.tabs[i].dirty = true;
|
let (text_names, dim_names, object_handles) = edited.changed_keys(&stage.baseline);
|
||||||
self.tabs[i].scene.bump_geometry();
|
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();
|
self.sync_ribbon_styles();
|
||||||
// Re-baseline so further edits in the still-open window stage afresh.
|
// Re-baseline so further edits in the still-open window stage afresh.
|
||||||
self.style_stage = Some(StyleStage {
|
self.style_stage = Some(StyleStage {
|
||||||
dirty_at_open: true,
|
dirty_at_open: self.tabs[i].dirty,
|
||||||
baseline: edited,
|
baseline: edited,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -609,13 +618,14 @@ impl OpenCADStudio {
|
||||||
let Some(stage) = self.style_stage.take() else {
|
let Some(stage) = self.style_stage.take() else {
|
||||||
return;
|
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.tabs[self.active_tab].dirty = stage.dirty_at_open;
|
||||||
self.sync_ribbon_styles();
|
self.sync_ribbon_styles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Snapshot of every document field a style manager can touch.
|
/// Snapshot of every document field a style manager can touch.
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
pub(super) struct StyleStateSnapshot {
|
pub(super) struct StyleStateSnapshot {
|
||||||
text_styles: acadrust::tables::Table<TextStyle>,
|
text_styles: acadrust::tables::Table<TextStyle>,
|
||||||
dim_styles: acadrust::tables::Table<DimStyle>,
|
dim_styles: acadrust::tables::Table<DimStyle>,
|
||||||
|
|
@ -630,6 +640,64 @@ pub(super) struct StyleStateSnapshot {
|
||||||
tab_active_mleader: String,
|
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.
|
/// An in-progress style-manager transaction.
|
||||||
pub(super) struct StyleStage {
|
pub(super) struct StyleStage {
|
||||||
dirty_at_open: bool,
|
dirty_at_open: bool,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,28 @@ use acadrust::{EntityType as AcadEntityType, Handle};
|
||||||
use iced::time::Instant;
|
use iced::time::Instant;
|
||||||
use iced::{mouse, Point, Task};
|
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 {
|
impl OpenCADStudio {
|
||||||
/// Before a save, give every cached truck solid that still has no ACIS
|
/// 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
|
// progress, so mark one. The browser picker + parse happen
|
||||||
// inside `pick_and_load_web`; the real name is unknown until
|
// inside `pick_and_load_web`; the real name is unknown until
|
||||||
// then, so show a generic label meanwhile.
|
// 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 {
|
self.opening = Some(crate::app::OpenProgress {
|
||||||
name: "Opening…".into(),
|
name: "Opening…".into(),
|
||||||
size_bytes: 0,
|
size_bytes: 0,
|
||||||
phase: std::sync::Arc::new(std::sync::atomic::AtomicU8::new(
|
state: state.clone(),
|
||||||
crate::app::OPEN_PHASE_READING,
|
|
||||||
)),
|
|
||||||
started: Instant::now(),
|
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
|
// If the user clicked Cancel while the parser was running, the
|
||||||
// overlay state was cleared and we silently drop the result.
|
// overlay state was cleared and we silently drop the result.
|
||||||
if self.opening.is_none() {
|
if self.opening.is_none() {
|
||||||
return Task::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 timings = caches.timings;
|
||||||
let entity_count = doc.entities().count();
|
let entity_count = doc.entities().count();
|
||||||
self.command_line
|
self.command_line
|
||||||
|
|
@ -420,6 +445,30 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
caches.corrupt_dropped
|
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 thumbs_task = self.push_recent(path.clone());
|
||||||
|
|
||||||
let current_is_empty = {
|
let current_is_empty = {
|
||||||
|
|
@ -472,47 +521,6 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
1.0
|
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.
|
// Open-time breakdown so regressions are visible immediately.
|
||||||
// `total` is wall time from the Open click to here (post-xref,
|
// `total` is wall time from the Open click to here (post-xref,
|
||||||
// pre-first-frame); the phase figures are the background-thread
|
// 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);
|
.unwrap_or(0);
|
||||||
self.command_line.push_info(&format!(
|
self.command_line.push_info(&format!(
|
||||||
" parse {}ms · purge {}ms · caches {}ms · xref {}ms · total {}ms",
|
" 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().
|
// 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.images = caches.images;
|
||||||
self.tabs[i].scene.meshes = caches.meshes;
|
self.tabs[i].scene.meshes = caches.meshes;
|
||||||
self.tabs[i].scene.block_meshes = caches.block_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.
|
// Invalidate the wire cache so the new document is tessellated.
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.bump_geometry();
|
||||||
// XREFs are merged into the document AFTER the background worker
|
if let Some(prepared) = prepared_geometry {
|
||||||
// built the mesh caches above, so those caches contain none of
|
self.tabs[i].scene.install_prepared_open_geometry(prepared);
|
||||||
// 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();
|
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.selected = rustc_hash::FxHashSet::default();
|
self.tabs[i].scene.selected = rustc_hash::FxHashSet::default();
|
||||||
self.tabs[i].scene.preview_wires = vec![];
|
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")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
let interaction_task = Task::none();
|
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();
|
let pending_open_task = self.drain_pending_open();
|
||||||
Task::batch([thumbs_task, pending_open_task, interaction_task])
|
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 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 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(
|
crate::modules::insert::wblock::extract_entities_to_doc(
|
||||||
&self.tabs[i].scene.document,
|
&document,
|
||||||
&handles,
|
&handles,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
crate::modules::insert::wblock::extract_block_to_doc(
|
crate::modules::insert::wblock::extract_block_to_doc(
|
||||||
&self.tabs[i].scene.document,
|
&document,
|
||||||
&block_name,
|
&worker_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}\""
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Err(e) => self
|
|
||||||
.command_line
|
|
||||||
.push_error(&format!("WBLOCK save failed: {e}")),
|
|
||||||
},
|
|
||||||
Err(e) => self.command_line.push_error(&format!("WBLOCK: {e}")),
|
|
||||||
}
|
}
|
||||||
Task::none()
|
.map_err(|e| e.to_string())?; crate::io::save(&document, &worker_path).map_err(|e| e.to_string())
|
||||||
|
},
|
||||||
|
move |result| Message::WblockWriteFinished(block_name, path, result),)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn on_stl_export_path_some(&mut self, path: std::path::PathBuf) -> Task<Message> {
|
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()
|
.values()
|
||||||
.filter_map(|s| s.lods.first().cloned())
|
.filter_map(|s| s.lods.first().cloned())
|
||||||
.collect();
|
.collect();
|
||||||
let mesh_refs: Vec<&crate::scene::model::mesh_model::MeshModel> = meshes.iter().collect();
|
let worker_path = path.clone();
|
||||||
match crate::io::stl::build_stl(&mesh_refs) {
|
background_task(
|
||||||
Some(bytes) => match std::fs::write(&path, bytes) {
|
move || {
|
||||||
Ok(()) => self
|
let mesh_refs: Vec<_> = meshes.iter().collect();
|
||||||
.command_line
|
let bytes = crate::io::stl::build_stl(&mesh_refs)
|
||||||
.push_output(&format!("STLOUT: exported to \"{}\"", path.display())),
|
.ok_or_else(|| "no mesh data to export".to_string())?;
|
||||||
Err(e) => self
|
std::fs::write(&worker_path, bytes).map_err(|e| e.to_string())
|
||||||
.command_line
|
|
||||||
.push_error(&format!("STLOUT: write error: {e}")),
|
|
||||||
},
|
},
|
||||||
None => self
|
move |result| Message::StlExportFinished(path, result),)
|
||||||
.command_line
|
|
||||||
.push_error("STLOUT: no mesh data to export."),
|
|
||||||
}
|
|
||||||
Task::none()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn on_step_export_path_some(&mut self, path: std::path::PathBuf) -> Task<Message> {
|
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()
|
.values()
|
||||||
.filter_map(|s| s.lods.first().cloned())
|
.filter_map(|s| s.lods.first().cloned())
|
||||||
.collect();
|
.collect();
|
||||||
let mesh_refs: Vec<&crate::scene::model::mesh_model::MeshModel> = meshes.iter().collect();
|
let worker_path = path.clone();
|
||||||
match crate::io::step::build_step(&mesh_refs) {
|
background_task(
|
||||||
Some(text) => match std::fs::write(&path, text.as_bytes()) {
|
move || {
|
||||||
Ok(()) => self
|
let mesh_refs: Vec<_> = meshes.iter().collect();
|
||||||
.command_line
|
let text = crate::io::step::build_step(&mesh_refs)
|
||||||
.push_output(&format!("STEPOUT: exported to \"{}\"", path.display())),
|
.ok_or_else(|| "no mesh data to export".to_string())?;
|
||||||
Err(e) => self
|
std::fs::write(&worker_path, text.as_bytes()).map_err(|e| e.to_string())
|
||||||
.command_line
|
|
||||||
.push_error(&format!("STEPOUT: write error: {e}")),
|
|
||||||
},
|
},
|
||||||
None => self
|
move |result| Message::StepExportFinished(path, result),)
|
||||||
.command_line
|
|
||||||
.push_error("STEPOUT: no mesh data to export."),
|
|
||||||
}
|
|
||||||
Task::none()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn on_obj_import_path_some(&mut self, path: std::path::PathBuf) -> Task<Message> {
|
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) {
|
let tab_id = self.tabs[self.active_tab].id;
|
||||||
Ok(s) => s,
|
let worker_path = path.clone();
|
||||||
Err(e) => {
|
background_task(
|
||||||
self.command_line
|
move || {
|
||||||
.push_error(&format!("IMPORTOBJ: read error: {e}"));
|
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])
|
||||||
return Task::none();
|
.ok_or_else(|| "no usable geometry in file".to_string())
|
||||||
}
|
},
|
||||||
};
|
move |result| Message::ObjImportFinished(tab_id, path, result),)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[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);
|
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"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub(in crate::app) fn queue_native_save(
|
pub(in crate::app) fn queue_native_save(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|
@ -1484,7 +1451,11 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
_ => (paper_w, paper_h),
|
_ => (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,
|
&wires,
|
||||||
hatches.as_slice(),
|
hatches.as_slice(),
|
||||||
wipeouts.as_slice(),
|
wipeouts.as_slice(),
|
||||||
|
|
@ -1495,23 +1466,20 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
rotation_deg,
|
rotation_deg,
|
||||||
1.0,
|
1.0,
|
||||||
None,
|
None,
|
||||||
&path,
|
&worker_path,
|
||||||
self.active_plot_style.as_ref(),
|
plot_style.as_ref(),
|
||||||
) {
|
)
|
||||||
// Full path, not just the file name — when the export was
|
.map(|_| format!("Exported: {}", worker_path.display()))
|
||||||
// driven by EXPORTPDF <path> the user needs to see where
|
.map_err(|e| format!("Export failed: {e}"))
|
||||||
// the file actually landed. (#369)
|
},
|
||||||
Ok(()) => self
|
|result| Message::BackgroundIoFinished(result, false),
|
||||||
.command_line
|
)
|
||||||
.push_info(&format!("Exported: {}", path.display())),
|
|
||||||
Err(e) => self.command_line.push_error(&format!("Export failed: {e}")),
|
|
||||||
}
|
|
||||||
Task::none()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Export the pending model-space plot window (set by PLOTWINDOW while on
|
/// Export the pending model-space plot window (set by PLOTWINDOW while on
|
||||||
/// the Model tab) to PDF, using the chosen paper size/orientation/scale.
|
/// 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};
|
use crate::io::paper_sizes::{sheet_mm, window_to_sheet, PlotScale};
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
if self.tabs[i].scene.current_layout != "Model" {
|
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).
|
// 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 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,
|
&wires,
|
||||||
hatches.as_slice(),
|
hatches.as_slice(),
|
||||||
wipeouts.as_slice(),
|
wipeouts.as_slice(),
|
||||||
|
|
@ -1580,20 +1553,18 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
0,
|
0,
|
||||||
scale as f32,
|
scale as f32,
|
||||||
clip,
|
clip,
|
||||||
&path,
|
&worker_path,
|
||||||
self.active_plot_style.as_ref(),
|
plot_style.as_ref(),
|
||||||
);
|
)
|
||||||
match res {
|
.map(|_| {format!(
|
||||||
Ok(()) => {
|
|
||||||
self.command_line.push_info(&format!(
|
|
||||||
"Plotted window to {}",
|
"Plotted window to {}",
|
||||||
path.file_name().unwrap_or_default().to_string_lossy()
|
worker_path
|
||||||
));
|
.file_name().unwrap_or_default().to_string_lossy()
|
||||||
self.close_active_modal();
|
)
|
||||||
}
|
}).map_err(|e| format!("Plot failed: {e}"))
|
||||||
Err(e) => self.command_line.push_error(&format!("Plot failed: {e}")),
|
},
|
||||||
}
|
|result| Message::BackgroundIoFinished(result, false),
|
||||||
Task::none()
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the render inputs and page geometry for a full-layout plot: wires
|
/// 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();
|
self.layout_plot_params();
|
||||||
let plot_style = self.active_plot_style.clone();
|
let plot_style = self.active_plot_style.clone();
|
||||||
self.command_line.push_info("Sending to system printer…");
|
self.command_line.push_info("Sending to system printer…");
|
||||||
Task::perform(
|
background_task(
|
||||||
async move {
|
move || {
|
||||||
|
iced::futures::executor::block_on(
|
||||||
crate::io::print_to_printer::print_wires(
|
crate::io::print_to_printer::print_wires(
|
||||||
wires, hatches, wipeouts, eff_w, eff_h, ox, oy, rotation_deg, plot_style,
|
wires, hatches, wipeouts, eff_w, eff_h, ox, oy, rotation_deg, plot_style,
|
||||||
)
|
))
|
||||||
.await
|
|
||||||
},
|
},
|
||||||
Message::PrintResult,
|
Message::PrintResult,
|
||||||
)
|
)
|
||||||
|
|
@ -2220,16 +2191,16 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
};
|
};
|
||||||
let tmp = std::env::temp_dir().join("open_cad_studio_preview.pdf");
|
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,
|
&w_wires, &w_hatches, &w_wipeouts, sw, sh, wox, woy, 0, wscale, wclip, &tmp,
|
||||||
plot_style.as_ref(),
|
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 {
|
if d.to_file {
|
||||||
// Tested clipped export (opens a save dialog).
|
// Tested clipped export (opens a save dialog).
|
||||||
|
|
@ -2244,25 +2215,27 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
};
|
};
|
||||||
let tmp = std::env::temp_dir().join("open_cad_studio_print.pdf");
|
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,
|
&w_wires, &w_hatches, &w_wipeouts, sw, sh, wox, woy, 0, wscale, wclip, &tmp,
|
||||||
plot_style.as_ref(),
|
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();
|
let (wires, hatches, wipeouts, eff_w, eff_h, ox, oy, rot) = self.layout_plot_params();
|
||||||
|
|
||||||
if preview {
|
if preview {
|
||||||
let tmp = std::env::temp_dir().join("open_cad_studio_preview.pdf");
|
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,
|
&wires,
|
||||||
&hatches,
|
&hatches,
|
||||||
&wipeouts,
|
&wipeouts,
|
||||||
|
|
@ -2275,14 +2248,11 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
None,
|
None,
|
||||||
&tmp,
|
&tmp,
|
||||||
plot_style.as_ref(),
|
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 {
|
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);
|
let opts = self.plot_print_options(&d);
|
||||||
self.command_line.push_info("Sending to system printer…");
|
self.command_line.push_info("Sending to system printer…");
|
||||||
Task::perform(
|
background_task(
|
||||||
async move {
|
move || {
|
||||||
|
iced::futures::executor::block_on(
|
||||||
crate::io::print_to_printer::print_wires_with(
|
crate::io::print_to_printer::print_wires_with(
|
||||||
wires, hatches, wipeouts, eff_w, eff_h, ox, oy, rot, plot_style, opts,
|
wires, hatches, wipeouts, eff_w, eff_h, ox, oy, rot, plot_style, opts,
|
||||||
)
|
))
|
||||||
.await
|
|
||||||
},
|
},
|
||||||
Message::PrintResult,
|
Message::PrintResult,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -449,20 +449,26 @@ impl OpenCADStudio {
|
||||||
.file_name()
|
.file_name()
|
||||||
.map(|n| n.to_string_lossy().into_owned())
|
.map(|n| n.to_string_lossy().into_owned())
|
||||||
.unwrap_or_else(|| "unknown".into());
|
.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,
|
super::OPEN_PHASE_READING,
|
||||||
));
|
));
|
||||||
self.opening = Some(super::OpenProgress {
|
self.opening = Some(super::OpenProgress {
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
size_bytes,
|
size_bytes,
|
||||||
phase: phase.clone(),
|
state: progress.clone(),
|
||||||
started: Instant::now(),
|
started: Instant::now(),
|
||||||
});
|
});
|
||||||
let size_label = format_size(size_bytes);
|
let size_label = format_size(size_bytes);
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_info(&format!("Opening \"{name}\" ({size_label})…"));
|
.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(
|
Task::perform(
|
||||||
crate::io::open_path_with_phase(path, phase),
|
crate::io::open_path_with_phase(path, progress, model_bg),
|
||||||
Message::FileOpened,
|
Message::FileOpened,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -579,6 +585,19 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
Message::WblockSaveResult(_, None) => Task::none(),
|
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) => {
|
Message::DataExtractionSave(csv) => {
|
||||||
let csv_clone = csv.clone();
|
let csv_clone = csv.clone();
|
||||||
Task::perform(
|
Task::perform(
|
||||||
|
|
@ -643,6 +662,16 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
Message::StlExportPath(None) => Task::none(),
|
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 ─────────────────────────────────────────
|
// ── STEP AP203 export ─────────────────────────────────────────
|
||||||
Message::StepExport => {
|
Message::StepExport => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
|
@ -670,6 +699,16 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
Message::StepExportPath(None) => Task::none(),
|
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 ────────────────────────────────────────────────
|
// ── OBJ import ────────────────────────────────────────────────
|
||||||
Message::ObjImport => Task::perform(
|
Message::ObjImport => Task::perform(
|
||||||
async {
|
async {
|
||||||
|
|
@ -688,6 +727,38 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
Message::ObjImportPath(None) => Task::none(),
|
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::SaveFile => self.on_save_file(),
|
||||||
|
|
||||||
Message::SaveAs => {
|
Message::SaveAs => {
|
||||||
|
|
@ -1132,7 +1203,7 @@ impl OpenCADStudio {
|
||||||
let targets = self.layer_row_action_targets(i, idx);
|
let targets = self.layer_row_action_targets(i, idx);
|
||||||
if let Some(on) = on {
|
if let Some(on) = on {
|
||||||
if !targets.is_empty() {
|
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 {
|
for name in &targets {
|
||||||
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
||||||
dl.flags.off = !on;
|
dl.flags.off = !on;
|
||||||
|
|
@ -1143,8 +1214,9 @@ impl OpenCADStudio {
|
||||||
pl.visible = on;
|
pl.visible = on;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.invalidate_layer_dependencies(&targets);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
self.commit_layer_undo(i, undo);
|
||||||
self.command_line.push_output(&format!(
|
self.command_line.push_output(&format!(
|
||||||
"{} layer(s) turned {}",
|
"{} layer(s) turned {}",
|
||||||
targets.len(),
|
targets.len(),
|
||||||
|
|
@ -1171,7 +1243,7 @@ impl OpenCADStudio {
|
||||||
let targets = self.layer_row_action_targets(i, idx);
|
let targets = self.layer_row_action_targets(i, idx);
|
||||||
if let Some(locked) = locked {
|
if let Some(locked) = locked {
|
||||||
if !targets.is_empty() {
|
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 {
|
for name in &targets {
|
||||||
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
||||||
dl.flags.locked = locked;
|
dl.flags.locked = locked;
|
||||||
|
|
@ -1182,8 +1254,9 @@ impl OpenCADStudio {
|
||||||
pl.locked = locked;
|
pl.locked = locked;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.bump_geometry();
|
// Lock state affects editability, not rendered geometry.
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
self.commit_layer_undo(i, undo);
|
||||||
self.command_line.push_output(&format!(
|
self.command_line.push_output(&format!(
|
||||||
"{} layer(s) {}",
|
"{} layer(s) {}",
|
||||||
targets.len(),
|
targets.len(),
|
||||||
|
|
@ -1201,7 +1274,7 @@ impl OpenCADStudio {
|
||||||
let targets = self.layer_row_action_targets(i, idx);
|
let targets = self.layer_row_action_targets(i, idx);
|
||||||
if let Some(frozen) = frozen {
|
if let Some(frozen) = frozen {
|
||||||
if !targets.is_empty() {
|
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 {
|
for name in &targets {
|
||||||
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
||||||
if frozen {
|
if frozen {
|
||||||
|
|
@ -1216,8 +1289,9 @@ impl OpenCADStudio {
|
||||||
pl.frozen = frozen;
|
pl.frozen = frozen;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.invalidate_layer_dependencies(&targets);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
self.commit_layer_undo(i, undo);
|
||||||
self.command_line.push_output(&format!(
|
self.command_line.push_output(&format!(
|
||||||
"{} layer(s) {}",
|
"{} layer(s) {}",
|
||||||
targets.len(),
|
targets.len(),
|
||||||
|
|
@ -1321,6 +1395,7 @@ impl OpenCADStudio {
|
||||||
// Apply to every selected layer (multi-select), not just one.
|
// Apply to every selected layer (multi-select), not just one.
|
||||||
let names = self.selected_layer_names(i);
|
let names = self.selected_layer_names(i);
|
||||||
if !names.is_empty() {
|
if !names.is_empty() {
|
||||||
|
let undo = self.begin_layer_undo(i, "LAYER COLOR", &names);
|
||||||
use crate::ui::window::layers::iced_color_from_acad;
|
use crate::ui::window::layers::iced_color_from_acad;
|
||||||
let new_color = iced_color_from_acad(&AcadColor::Index(aci));
|
let new_color = iced_color_from_acad(&AcadColor::Index(aci));
|
||||||
for name in &names {
|
for name in &names {
|
||||||
|
|
@ -1334,10 +1409,11 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
self.commit_layer_undo(i, undo);
|
||||||
// ByLayer color is baked into the cached wires at
|
// ByLayer color is baked into the cached wires at
|
||||||
// tessellation time, so bump the geometry epoch to
|
// tessellation time, so bump the geometry epoch to
|
||||||
// invalidate the wire cache and repaint with the new color.
|
// 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_picker_row = None;
|
||||||
self.tabs[i].layers.color_full_palette = false;
|
self.tabs[i].layers.color_full_palette = false;
|
||||||
self.sync_ribbon_layers();
|
self.sync_ribbon_layers();
|
||||||
|
|
@ -1349,6 +1425,7 @@ impl OpenCADStudio {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let names = self.selected_layer_names(i);
|
let names = self.selected_layer_names(i);
|
||||||
if !names.is_empty() {
|
if !names.is_empty() {
|
||||||
|
let undo = self.begin_layer_undo(i, "LAYER LINETYPE", &names);
|
||||||
for name in &names {
|
for name in &names {
|
||||||
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
||||||
dl.line_type = lt.clone();
|
dl.line_type = lt.clone();
|
||||||
|
|
@ -1360,8 +1437,9 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
self.commit_layer_undo(i, undo);
|
||||||
// Linetype is baked into the cached wires; repaint.
|
// Linetype is baked into the cached wires; repaint.
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
@ -1370,6 +1448,7 @@ impl OpenCADStudio {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let names = self.selected_layer_names(i);
|
let names = self.selected_layer_names(i);
|
||||||
if !names.is_empty() {
|
if !names.is_empty() {
|
||||||
|
let undo = self.begin_layer_undo(i, "LAYER LINEWEIGHT", &names);
|
||||||
for name in &names {
|
for name in &names {
|
||||||
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
||||||
dl.line_weight = lw;
|
dl.line_weight = lw;
|
||||||
|
|
@ -1381,8 +1460,9 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
self.commit_layer_undo(i, undo);
|
||||||
// Lineweight is baked into the cached wires; repaint.
|
// Lineweight is baked into the cached wires; repaint.
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
@ -3984,6 +4064,17 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
Message::PlotWindowExportPath(Some(path)) => self.on_plot_window_export_path_some(path),
|
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 ───────────────────────────────────────
|
// ── Print to system printer ───────────────────────────────────────
|
||||||
Message::PrintToPrinter => self.on_print_to_printer(),
|
Message::PrintToPrinter => self.on_print_to_printer(),
|
||||||
Message::PrintResult(Ok(printer)) => {
|
Message::PrintResult(Ok(printer)) => {
|
||||||
|
|
|
||||||
169
src/io/mod.rs
169
src/io/mod.rs
|
|
@ -18,20 +18,58 @@ pub mod patterns;
|
||||||
pub mod update_check;
|
pub mod update_check;
|
||||||
pub mod paper_sizes;
|
pub mod paper_sizes;
|
||||||
pub mod thumbnail;
|
pub mod thumbnail;
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
mod web_worker;
|
||||||
|
|
||||||
use crate::scene::DerivedCaches;
|
use crate::scene::DerivedCaches;
|
||||||
use acadrust::entities::EntityType;
|
use acadrust::entities::EntityType;
|
||||||
use acadrust::io::dwg::DwgReader;
|
use acadrust::io::dwg::DwgReader;
|
||||||
use acadrust::{CadDocument, DwgWriter, DxfReader, DxfWriter};
|
use acadrust::{CadDocument, DwgWriter, DxfReader, DxfWriter};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::atomic::{AtomicU8, Ordering};
|
use std::sync::atomic::{AtomicU16, AtomicU32, AtomicU8, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
// Phase tags written into the shared atomic so the UI overlay can display a
|
/// Thread-safe state shared by the native loader and the open overlay.
|
||||||
// human-readable label. Kept in sync with the constants in `crate::app`.
|
///
|
||||||
const PHASE_PARSING: u8 = 1;
|
/// `basis_points` is monotonic in 0..=10000. `completed/total` describe the
|
||||||
const PHASE_CACHING: u8 = 2;
|
/// current sub-stage and let diagnostics distinguish real progress from a
|
||||||
const PHASE_FINALIZING: u8 = 3;
|
/// 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 ──────────────────────────────────────────────────────────────────
|
// ── Open ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -61,37 +99,103 @@ pub async fn pick_open_path() -> Option<(PathBuf, u64)> {
|
||||||
/// thread runs.
|
/// thread runs.
|
||||||
pub async fn open_path_with_phase(
|
pub async fn open_path_with_phase(
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
phase: Arc<AtomicU8>,
|
progress: Arc<OpenProgressState>,
|
||||||
|
model_bg: [f32; 4],
|
||||||
) -> Result<(String, PathBuf, CadDocument, DerivedCaches), String> {
|
) -> Result<(String, PathBuf, CadDocument, DerivedCaches), String> {
|
||||||
let name = path
|
let name = path
|
||||||
.file_name()
|
.file_name()
|
||||||
.map(|n| n.to_string_lossy().into_owned())
|
.map(|n| n.to_string_lossy().into_owned())
|
||||||
.unwrap_or_else(|| "unknown".into());
|
.unwrap_or_else(|| "unknown".into());
|
||||||
let path2 = path.clone();
|
let path2 = path.clone();
|
||||||
let phase2 = phase.clone();
|
let progress2 = progress.clone();
|
||||||
let (doc, caches) = std::thread::spawn(move || -> Result<_, String> {
|
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;
|
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 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;
|
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 t_purge = Instant::now();
|
||||||
let dropped = purge_corrupt_entities(&mut doc);
|
let dropped = purge_corrupt_entities(&mut doc);
|
||||||
let purge_ms = t_purge.elapsed().as_millis() as u32;
|
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 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 {
|
caches.timings = crate::scene::OpenTimings {
|
||||||
parse_ms,
|
parse_ms,
|
||||||
purge_ms,
|
purge_ms,
|
||||||
caches_ms: t_caches.elapsed().as_millis() as u32,
|
caches_ms: t_caches.elapsed().as_millis() as u32,
|
||||||
|
xref_ms,
|
||||||
};
|
};
|
||||||
caches.corrupt_dropped = dropped;
|
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))
|
Ok((doc, caches))
|
||||||
|
})();
|
||||||
|
let _ = sender.send(result);
|
||||||
})
|
})
|
||||||
.join()
|
.map_err(|error| format!("failed to start parser thread: {error}"))?;
|
||||||
.map_err(|_| "parser thread panicked".to_string())??;
|
let (doc, caches) = receiver
|
||||||
|
.await
|
||||||
|
.map_err(|_| "parser thread stopped without a result".to_string())??;
|
||||||
Ok((name, path, doc, caches))
|
Ok((name, path, doc, caches))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,6 +206,7 @@ pub async fn open_path_with_phase(
|
||||||
/// stands in for the document path.
|
/// stands in for the document path.
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub async fn pick_and_load_web(
|
pub async fn pick_and_load_web(
|
||||||
|
progress: Arc<OpenProgressState>,
|
||||||
) -> Result<(String, PathBuf, CadDocument, DerivedCaches), String> {
|
) -> Result<(String, PathBuf, CadDocument, DerivedCaches), String> {
|
||||||
let handle = crate::sys::file_dialog()
|
let handle = crate::sys::file_dialog()
|
||||||
.set_title("Open CAD file")
|
.set_title("Open CAD file")
|
||||||
|
|
@ -111,11 +216,23 @@ pub async fn pick_and_load_web(
|
||||||
.await
|
.await
|
||||||
.ok_or_else(|| "Cancelled".to_string())?;
|
.ok_or_else(|| "Cancelled".to_string())?;
|
||||||
let name = handle.file_name();
|
let name = handle.file_name();
|
||||||
|
progress.set(crate::app::OPEN_PHASE_READING, 500, 1, 2);
|
||||||
let bytes = handle.read().await;
|
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 dropped = purge_corrupt_entities(&mut doc);
|
||||||
let mut caches = crate::scene::build_derived_caches(&doc);
|
let mut caches = crate::scene::build_derived_caches(&doc);
|
||||||
caches.corrupt_dropped = dropped;
|
caches.corrupt_dropped = dropped;
|
||||||
|
progress.set(crate::app::OPEN_PHASE_FINALIZING, 9900, 1, 1);
|
||||||
let path = PathBuf::from(&name);
|
let path = PathBuf::from(&name);
|
||||||
Ok((name, path, doc, caches))
|
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> {
|
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
|
let ext = path
|
||||||
.extension()
|
.extension()
|
||||||
.map(|e| e.to_string_lossy().to_lowercase())
|
.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() {
|
match effective.as_str() {
|
||||||
"dwg" => {
|
"dwg" => {
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let mut doc = DwgReader::from_mmap(path)
|
let mut doc = {
|
||||||
.map_err(|e| e.to_string())?
|
let mut reader = DwgReader::from_mmap(path)
|
||||||
.read()
|
|
||||||
.map_err(|e| e.to_string())?;
|
.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")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
let mut doc = DwgReader::from_file(path)
|
let mut doc = DwgReader::from_file(path)
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
|
|
|
||||||
77
src/io/web_worker.rs
Normal file
77
src/io/web_worker.rs
Normal 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:?}"))
|
||||||
|
}
|
||||||
|
|
@ -41,6 +41,19 @@ pub struct XrefInfo {
|
||||||
/// just like the host doc, so it gets the same corrupt-entity guard. Folding
|
/// 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.
|
/// 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) {
|
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.
|
// Auto-resolve every xref — frustum + LOD culling keep GPU cost bounded.
|
||||||
let xref_entries: Vec<(String, String, Handle)> = doc
|
let xref_entries: Vec<(String, String, Handle)> = doc
|
||||||
.block_records
|
.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())
|
.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))
|
.map(|br| (br.name.clone(), br.xref_path.clone(), br.handle))
|
||||||
.collect();
|
.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`
|
// Phase 1 — parse every referenced file in parallel. Each `load_file`
|
||||||
// reads and decodes an independent DWG/DXF and touches nothing in the host
|
// 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
|
// 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.
|
// it stays serial.) `resolve_path` is pure and `base_dir` is shared &-ref.
|
||||||
use crate::par::prelude::*;
|
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
|
let parsed: Vec<(String, String, Handle, Option<PathBuf>, Option<CadDocument>)> = xref_entries
|
||||||
.into_par_iter()
|
.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 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)
|
(block_name, raw_path, br_handle, resolved, xref_doc)
|
||||||
})
|
})
|
||||||
.collect();
|
.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.
|
// block order (par_iter preserves it), so handle allocation is deterministic.
|
||||||
let mut result = Vec::with_capacity(parsed.len());
|
let mut result = Vec::with_capacity(parsed.len());
|
||||||
let mut dropped = 0usize;
|
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 {
|
let status = if let Some(xref_doc) = xref_doc {
|
||||||
ensure_block_entities(doc, &block_name);
|
ensure_block_entities(doc, &block_name);
|
||||||
dropped += merge_xref_into_block(doc, &block_name, br_handle, xref_doc);
|
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),
|
.unwrap_or(raw_path),
|
||||||
status,
|
status,
|
||||||
});
|
});
|
||||||
|
if let Some(progress) = &progress {
|
||||||
|
progress(
|
||||||
|
xref_count
|
||||||
|
.saturating_mul(1000)
|
||||||
|
.saturating_add((merge_index + 1).saturating_mul(1000)),
|
||||||
|
total_units,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(result, dropped)
|
(result, dropped)
|
||||||
|
|
|
||||||
99
src/modules/draw/modify/entity_index.rs
Normal file
99
src/modules/draw/modify/entity_index.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,8 @@ use crate::modules::draw::defaults;
|
||||||
use crate::modules::IconKind;
|
use crate::modules::IconKind;
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
|
|
||||||
|
use super::entity_index::ModifyEntityIndex;
|
||||||
|
|
||||||
// ── Dropdown constants ─────────────────────────────────────────────────────
|
// ── Dropdown constants ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub const DROPDOWN_ID: &str = "fillet_chamfer";
|
pub const DROPDOWN_ID: &str = "fillet_chamfer";
|
||||||
|
|
@ -1204,6 +1206,7 @@ pub struct FilletCommand {
|
||||||
radius: f64,
|
radius: f64,
|
||||||
step: FilletStep,
|
step: FilletStep,
|
||||||
all_entities: Vec<EntityType>,
|
all_entities: Vec<EntityType>,
|
||||||
|
entity_index: ModifyEntityIndex,
|
||||||
/// First-object pick to restore after a radius entry made mid-selection
|
/// 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
|
/// (i.e. "R" pressed after the first object was already picked), so the
|
||||||
/// command resumes at the second pick instead of restarting selection.
|
/// command resumes at the second pick instead of restarting selection.
|
||||||
|
|
@ -1212,10 +1215,12 @@ pub struct FilletCommand {
|
||||||
|
|
||||||
impl FilletCommand {
|
impl FilletCommand {
|
||||||
pub fn new(radius: f64, all_entities: Vec<EntityType>) -> Self {
|
pub fn new(radius: f64, all_entities: Vec<EntityType>) -> Self {
|
||||||
|
let entity_index = ModifyEntityIndex::build(&all_entities);
|
||||||
Self {
|
Self {
|
||||||
radius: radius as f64,
|
radius: radius as f64,
|
||||||
step: FilletStep::First,
|
step: FilletStep::First,
|
||||||
all_entities,
|
all_entities,
|
||||||
|
entity_index,
|
||||||
resume_second: None,
|
resume_second: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1348,9 +1353,8 @@ impl CadCommand for FilletCommand {
|
||||||
FilletStep::WaitingForRadius => return CmdResult::NeedPoint,
|
FilletStep::WaitingForRadius => return CmdResult::NeedPoint,
|
||||||
FilletStep::First => {
|
FilletStep::First => {
|
||||||
let e1 = self
|
let e1 = self
|
||||||
.all_entities
|
.entity_index
|
||||||
.iter()
|
.get(&self.all_entities, handle)
|
||||||
.find(|e| e.common().handle == handle)
|
|
||||||
.and_then(|entity| match entity {
|
.and_then(|entity| match entity {
|
||||||
EntityType::LwPolyline(p) => {
|
EntityType::LwPolyline(p) => {
|
||||||
Some(FilletEntity::from_lwpoly(p, handle, click))
|
Some(FilletEntity::from_lwpoly(p, handle, click))
|
||||||
|
|
@ -1380,9 +1384,8 @@ impl CadCommand for FilletCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
let e2 = self
|
let e2 = self
|
||||||
.all_entities
|
.entity_index
|
||||||
.iter()
|
.get(&self.all_entities, handle)
|
||||||
.find(|e| e.common().handle == handle)
|
|
||||||
.and_then(|entity| match entity {
|
.and_then(|entity| match entity {
|
||||||
EntityType::LwPolyline(p) => {
|
EntityType::LwPolyline(p) => {
|
||||||
Some(FilletEntity::from_lwpoly(p, handle, click))
|
Some(FilletEntity::from_lwpoly(p, handle, click))
|
||||||
|
|
@ -1426,9 +1429,8 @@ impl CadCommand for FilletCommand {
|
||||||
FilletStep::WaitingForRadius => vec![],
|
FilletStep::WaitingForRadius => vec![],
|
||||||
FilletStep::First => {
|
FilletStep::First => {
|
||||||
let pts = self
|
let pts = self
|
||||||
.all_entities
|
.entity_index
|
||||||
.iter()
|
.get(&self.all_entities, handle)
|
||||||
.find(|e| e.common().handle == handle)
|
|
||||||
.and_then(|e| match e {
|
.and_then(|e| match e {
|
||||||
EntityType::LwPolyline(p) => Some(lwpoly_seg_hover_pts(p, click)),
|
EntityType::LwPolyline(p) => Some(lwpoly_seg_hover_pts(p, click)),
|
||||||
_ => FilletEntity::from_entity(e).map(|fe| entity_pts(&fe.to_entity_type())),
|
_ => 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 e1 = e1.clone();
|
||||||
let click1 = *click1;
|
let click1 = *click1;
|
||||||
let e2 = self
|
let e2 = self
|
||||||
.all_entities
|
.entity_index
|
||||||
.iter()
|
.get(&self.all_entities, handle)
|
||||||
.find(|e| e.common().handle == handle)
|
|
||||||
.and_then(|entity| match entity {
|
.and_then(|entity| match entity {
|
||||||
EntityType::LwPolyline(p) => {
|
EntityType::LwPolyline(p) => {
|
||||||
Some(FilletEntity::from_lwpoly(p, handle, click))
|
Some(FilletEntity::from_lwpoly(p, handle, click))
|
||||||
|
|
@ -1583,6 +1584,7 @@ pub struct ChamferCommand {
|
||||||
dist2: f64,
|
dist2: f64,
|
||||||
step: ChamferStep,
|
step: ChamferStep,
|
||||||
all_entities: Vec<EntityType>,
|
all_entities: Vec<EntityType>,
|
||||||
|
entity_index: ModifyEntityIndex,
|
||||||
/// First-object pick (line or polyline segment) to restore after a
|
/// First-object pick (line or polyline segment) to restore after a
|
||||||
/// distance entry made mid-selection, so the command resumes at the
|
/// distance entry made mid-selection, so the command resumes at the
|
||||||
/// second pick instead of restarting selection.
|
/// second pick instead of restarting selection.
|
||||||
|
|
@ -1591,11 +1593,13 @@ pub struct ChamferCommand {
|
||||||
|
|
||||||
impl ChamferCommand {
|
impl ChamferCommand {
|
||||||
pub fn new(dist: f64, all_entities: Vec<EntityType>) -> Self {
|
pub fn new(dist: f64, all_entities: Vec<EntityType>) -> Self {
|
||||||
|
let entity_index = ModifyEntityIndex::build(&all_entities);
|
||||||
Self {
|
Self {
|
||||||
dist1: dist as f64,
|
dist1: dist as f64,
|
||||||
dist2: defaults::get_chamfer_dist2(),
|
dist2: defaults::get_chamfer_dist2(),
|
||||||
step: ChamferStep::First,
|
step: ChamferStep::First,
|
||||||
all_entities,
|
all_entities,
|
||||||
|
entity_index,
|
||||||
resume_pick: None,
|
resume_pick: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1777,9 +1781,7 @@ impl CadCommand for ChamferCommand {
|
||||||
}
|
}
|
||||||
ChamferStep::First => {
|
ChamferStep::First => {
|
||||||
match self
|
match self
|
||||||
.all_entities
|
.entity_index.get(&self.all_entities, handle)
|
||||||
.iter()
|
|
||||||
.find(|e| e.common().handle == handle)
|
|
||||||
{
|
{
|
||||||
Some(EntityType::Line(l)) => {
|
Some(EntityType::Line(l)) => {
|
||||||
self.step = ChamferStep::Second {
|
self.step = ChamferStep::Second {
|
||||||
|
|
@ -1821,9 +1823,8 @@ impl CadCommand for ChamferCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
let l2 = self
|
let l2 = self
|
||||||
.all_entities
|
.entity_index
|
||||||
.iter()
|
.get(&self.all_entities, handle)
|
||||||
.find(|e| e.common().handle == handle)
|
|
||||||
.and_then(|e| {
|
.and_then(|e| {
|
||||||
if let EntityType::Line(l) = e {
|
if let EntityType::Line(l) = e {
|
||||||
Some(l.clone())
|
Some(l.clone())
|
||||||
|
|
@ -1857,9 +1858,8 @@ impl CadCommand for ChamferCommand {
|
||||||
ChamferStep::WaitingForDist1 | ChamferStep::WaitingForDist2 => return vec![],
|
ChamferStep::WaitingForDist1 | ChamferStep::WaitingForDist2 => return vec![],
|
||||||
ChamferStep::First => {
|
ChamferStep::First => {
|
||||||
let pts = self
|
let pts = self
|
||||||
.all_entities
|
.entity_index
|
||||||
.iter()
|
.get(&self.all_entities, handle)
|
||||||
.find(|e| e.common().handle == handle)
|
|
||||||
.and_then(|e| match e {
|
.and_then(|e| match e {
|
||||||
EntityType::Line(l) => Some(line_pts(l)),
|
EntityType::Line(l) => Some(line_pts(l)),
|
||||||
EntityType::LwPolyline(p) => Some(lwpoly_seg_hover_pts(p, click)),
|
EntityType::LwPolyline(p) => Some(lwpoly_seg_hover_pts(p, click)),
|
||||||
|
|
@ -1880,9 +1880,8 @@ impl CadCommand for ChamferCommand {
|
||||||
let l1 = l1.clone();
|
let l1 = l1.clone();
|
||||||
let click1 = *click1;
|
let click1 = *click1;
|
||||||
let l2 = self
|
let l2 = self
|
||||||
.all_entities
|
.entity_index
|
||||||
.iter()
|
.get(&self.all_entities, handle)
|
||||||
.find(|e| e.common().handle == handle)
|
|
||||||
.and_then(|e| {
|
.and_then(|e| {
|
||||||
if let EntityType::Line(l) = e {
|
if let EntityType::Line(l) = e {
|
||||||
Some(l.clone())
|
Some(l.clone())
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ pub mod attedit;
|
||||||
pub mod break_cmd;
|
pub mod break_cmd;
|
||||||
pub mod copy;
|
pub mod copy;
|
||||||
pub mod delete;
|
pub mod delete;
|
||||||
|
mod entity_index;
|
||||||
pub mod explode;
|
pub mod explode;
|
||||||
pub mod fillet;
|
pub mod fillet;
|
||||||
pub mod join;
|
pub mod join;
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ use crate::modules::draw::defaults;
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
|
|
||||||
|
use super::entity_index::ModifyEntityIndex;
|
||||||
|
|
||||||
// ── Ribbon definition ──────────────────────────────────────────────────────
|
// ── Ribbon definition ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub fn tool() -> ToolDef {
|
pub fn tool() -> ToolDef {
|
||||||
|
|
@ -691,6 +693,7 @@ enum Step {
|
||||||
pub struct OffsetCommand {
|
pub struct OffsetCommand {
|
||||||
step: Step,
|
step: Step,
|
||||||
all_entities: Vec<EntityType>,
|
all_entities: Vec<EntityType>,
|
||||||
|
entity_index: ModifyEntityIndex,
|
||||||
/// Pre-selected offsettable objects (pick-first, #422); consumed when the
|
/// Pre-selected offsettable objects (pick-first, #422); consumed when the
|
||||||
/// distance step resolves.
|
/// distance step resolves.
|
||||||
preselected: Vec<EntityType>,
|
preselected: Vec<EntityType>,
|
||||||
|
|
@ -712,9 +715,11 @@ pub fn is_offsettable(e: &EntityType) -> bool {
|
||||||
|
|
||||||
impl OffsetCommand {
|
impl OffsetCommand {
|
||||||
pub fn new(all_entities: Vec<EntityType>) -> Self {
|
pub fn new(all_entities: Vec<EntityType>) -> Self {
|
||||||
|
let entity_index = ModifyEntityIndex::build(&all_entities);
|
||||||
Self {
|
Self {
|
||||||
step: Step::Distance,
|
step: Step::Distance,
|
||||||
all_entities,
|
all_entities,
|
||||||
|
entity_index,
|
||||||
preselected: Vec::new(),
|
preselected: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -722,9 +727,11 @@ impl OffsetCommand {
|
||||||
/// Pick-first flow (#422): the distance step still comes first, then the
|
/// Pick-first flow (#422): the distance step still comes first, then the
|
||||||
/// pre-selected objects go straight to the side step.
|
/// pre-selected objects go straight to the side step.
|
||||||
pub fn with_selection(all_entities: Vec<EntityType>, targets: Vec<EntityType>) -> Self {
|
pub fn with_selection(all_entities: Vec<EntityType>, targets: Vec<EntityType>) -> Self {
|
||||||
|
let entity_index = ModifyEntityIndex::build(&all_entities);
|
||||||
Self {
|
Self {
|
||||||
step: Step::Distance,
|
step: Step::Distance,
|
||||||
all_entities,
|
all_entities,
|
||||||
|
entity_index,
|
||||||
preselected: targets,
|
preselected: targets,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -801,9 +808,7 @@ impl CadCommand for OffsetCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
let entity = self
|
let entity = self
|
||||||
.all_entities
|
.entity_index.get(&self.all_entities, handle)
|
||||||
.iter()
|
|
||||||
.find(|e| e.common().handle == handle)
|
|
||||||
.cloned();
|
.cloned();
|
||||||
|
|
||||||
// Accept every type compute_offset can offset — including XLine (#296),
|
// Accept every type compute_offset can offset — including XLine (#296),
|
||||||
|
|
@ -881,9 +886,7 @@ impl CadCommand for OffsetCommand {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
if let Some(entity) = self
|
if let Some(entity) = self
|
||||||
.all_entities
|
.entity_index.get(&self.all_entities, handle)
|
||||||
.iter()
|
|
||||||
.find(|e| e.common().handle == handle)
|
|
||||||
{
|
{
|
||||||
let pts = entity_wire_pts(entity);
|
let pts = entity_wire_pts(entity);
|
||||||
if !pts.is_empty() {
|
if !pts.is_empty() {
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ use crate::modules::draw::modify::spline_ops::{
|
||||||
use crate::modules::IconKind;
|
use crate::modules::IconKind;
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
|
|
||||||
|
use super::entity_index::ModifyEntityIndex;
|
||||||
|
|
||||||
// ── Dropdown constants ─────────────────────────────────────────────────────
|
// ── Dropdown constants ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub const DROPDOWN_ID: &str = "trim_extend";
|
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> {
|
fn build_geos(entities: &[EntityType]) -> Vec<Geo> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for e in entities {
|
for e in entities {
|
||||||
|
|
@ -2797,6 +2813,7 @@ fn crossing_preview_wire(p1: [f64; 2], cursor: [f64; 2], name: &str) -> WireMode
|
||||||
|
|
||||||
pub struct TrimCommand {
|
pub struct TrimCommand {
|
||||||
all_entities: Vec<EntityType>,
|
all_entities: Vec<EntityType>,
|
||||||
|
entity_index: ModifyEntityIndex,
|
||||||
geos: Vec<Geo>,
|
geos: Vec<Geo>,
|
||||||
mode: TrimMode,
|
mode: TrimMode,
|
||||||
/// Cutting-edge selection; empty = every object cuts (quick mode).
|
/// Cutting-edge selection; empty = every object cuts (quick mode).
|
||||||
|
|
@ -2809,9 +2826,11 @@ pub struct TrimCommand {
|
||||||
|
|
||||||
impl TrimCommand {
|
impl TrimCommand {
|
||||||
pub fn new(all_entities: Vec<EntityType>) -> Self {
|
pub fn new(all_entities: Vec<EntityType>) -> Self {
|
||||||
|
let entity_index = ModifyEntityIndex::build(&all_entities);
|
||||||
let geos = build_geos(&all_entities);
|
let geos = build_geos(&all_entities);
|
||||||
Self {
|
Self {
|
||||||
all_entities,
|
all_entities,
|
||||||
|
entity_index,
|
||||||
geos,
|
geos,
|
||||||
mode: TrimMode::Pick,
|
mode: TrimMode::Pick,
|
||||||
edge_set: Vec::new(),
|
edge_set: Vec::new(),
|
||||||
|
|
@ -2823,6 +2842,7 @@ impl TrimCommand {
|
||||||
/// Boundary geometry from the edge selection (or everything when none),
|
/// Boundary geometry from the edge selection (or everything when none),
|
||||||
/// with the Edge option's implied extrapolation applied on top.
|
/// with the Edge option's implied extrapolation applied on top.
|
||||||
fn rebuild_geos(&mut self) {
|
fn rebuild_geos(&mut self) {
|
||||||
|
self.entity_index = ModifyEntityIndex::build(&self.all_entities);
|
||||||
self.geos = if self.edge_set.is_empty() {
|
self.geos = if self.edge_set.is_empty() {
|
||||||
build_geos(&self.all_entities)
|
build_geos(&self.all_entities)
|
||||||
} else {
|
} 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(
|
fn fence_run(
|
||||||
&mut self,
|
&mut self,
|
||||||
fence: &[[f64; 2]],
|
fence: &[[f64; 2]],
|
||||||
|
|
@ -2974,7 +3011,8 @@ impl CadCommand for TrimCommand {
|
||||||
pick_extend_at(&self.all_entities, &self.geos, handle, px, py)
|
pick_extend_at(&self.all_entities, &self.geos, handle, px, py)
|
||||||
.map(|e| vec![e])
|
.map(|e| vec![e])
|
||||||
} else {
|
} 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 {
|
if let Some(new_entities) = new_entities {
|
||||||
// Snapshot is updated in on_entity_replaced once we know
|
// Snapshot is updated in on_entity_replaced once we know
|
||||||
|
|
@ -3108,10 +3146,10 @@ impl CadCommand for TrimCommand {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let nearby_geos = self.nearby_geos(handle);
|
||||||
|
let geos = nearby_geos.as_slice();
|
||||||
let entity = self
|
let entity = self
|
||||||
.all_entities
|
.entity_index.get(&self.all_entities, handle);
|
||||||
.iter()
|
|
||||||
.find(|e| e.common().handle == handle);
|
|
||||||
|
|
||||||
let mut hover_wires = match entity {
|
let mut hover_wires = match entity {
|
||||||
Some(EntityType::Line(l)) => {
|
Some(EntityType::Line(l)) => {
|
||||||
|
|
@ -3119,7 +3157,7 @@ impl CadCommand for TrimCommand {
|
||||||
let ay = l.start.y;
|
let ay = l.start.y;
|
||||||
let bx = l.end.x;
|
let bx = l.end.x;
|
||||||
let by = l.end.y;
|
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() {
|
if ts.is_empty() {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
|
|
@ -3152,7 +3190,7 @@ impl CadCommand for TrimCommand {
|
||||||
let cy = a.center.y;
|
let cy = a.center.y;
|
||||||
let a0 = a.start_angle;
|
let a0 = a.start_angle;
|
||||||
let a1 = a.end_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() {
|
if ts.is_empty() {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
|
|
@ -3176,7 +3214,7 @@ impl CadCommand for TrimCommand {
|
||||||
Some(EntityType::Circle(c)) => {
|
Some(EntityType::Circle(c)) => {
|
||||||
let cx = c.center.x;
|
let cx = c.center.x;
|
||||||
let cy = c.center.y;
|
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 {
|
if ts.len() < 2 {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
|
|
@ -3205,7 +3243,7 @@ impl CadCommand for TrimCommand {
|
||||||
let by = r.base_point.y;
|
let by = r.base_point.y;
|
||||||
let ex = bx + r.direction.x * TRIM_EXTENT;
|
let ex = bx + r.direction.x * TRIM_EXTENT;
|
||||||
let ey = by + r.direction.y * 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() {
|
if ts.is_empty() {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
|
|
@ -3245,7 +3283,7 @@ impl CadCommand for TrimCommand {
|
||||||
let ey_start = by - x.direction.y * TRIM_EXTENT;
|
let ey_start = by - x.direction.y * TRIM_EXTENT;
|
||||||
let ex_end = bx + x.direction.x * TRIM_EXTENT;
|
let ex_end = bx + x.direction.x * TRIM_EXTENT;
|
||||||
let ey_end = by + x.direction.y * 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() {
|
if ts.is_empty() {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
|
|
@ -3294,7 +3332,7 @@ impl CadCommand for TrimCommand {
|
||||||
t1 += TAU;
|
t1 += TAU;
|
||||||
}
|
}
|
||||||
let ts = ellipse_seg_ts(
|
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() {
|
if ts.is_empty() {
|
||||||
return vec![];
|
return vec![];
|
||||||
|
|
@ -3321,7 +3359,7 @@ impl CadCommand for TrimCommand {
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
Some(EntityType::Spline(s)) => {
|
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() {
|
if ts.is_empty() {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
|
|
@ -3348,7 +3386,7 @@ impl CadCommand for TrimCommand {
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
Some(EntityType::LwPolyline(p)) => {
|
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 {
|
else {
|
||||||
return vec![];
|
return vec![];
|
||||||
};
|
};
|
||||||
|
|
@ -3371,7 +3409,7 @@ impl CadCommand for TrimCommand {
|
||||||
if self.implied_edges && !hover_wires.is_empty() {
|
if self.implied_edges && !hover_wires.is_empty() {
|
||||||
if let (Some(orig), Some(pieces)) = (
|
if let (Some(orig), Some(pieces)) = (
|
||||||
entity,
|
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);
|
let cuts = piece_cut_points(orig, &pieces);
|
||||||
hover_wires.extend(implied_cut_guides(&self.all_entities, handle, &cuts));
|
hover_wires.extend(implied_cut_guides(&self.all_entities, handle, &cuts));
|
||||||
|
|
@ -3477,6 +3515,7 @@ impl CadCommand for TrimCommand {
|
||||||
|
|
||||||
pub struct ExtendCommand {
|
pub struct ExtendCommand {
|
||||||
all_entities: Vec<EntityType>,
|
all_entities: Vec<EntityType>,
|
||||||
|
entity_index: ModifyEntityIndex,
|
||||||
geos: Vec<Geo>,
|
geos: Vec<Geo>,
|
||||||
mode: TrimMode,
|
mode: TrimMode,
|
||||||
/// Boundary-edge selection; empty = every object is a boundary.
|
/// Boundary-edge selection; empty = every object is a boundary.
|
||||||
|
|
@ -3489,9 +3528,11 @@ pub struct ExtendCommand {
|
||||||
|
|
||||||
impl ExtendCommand {
|
impl ExtendCommand {
|
||||||
pub fn new(all_entities: Vec<EntityType>) -> Self {
|
pub fn new(all_entities: Vec<EntityType>) -> Self {
|
||||||
|
let entity_index = ModifyEntityIndex::build(&all_entities);
|
||||||
let geos = build_geos(&all_entities);
|
let geos = build_geos(&all_entities);
|
||||||
Self {
|
Self {
|
||||||
all_entities,
|
all_entities,
|
||||||
|
entity_index,
|
||||||
geos,
|
geos,
|
||||||
mode: TrimMode::Pick,
|
mode: TrimMode::Pick,
|
||||||
edge_set: Vec::new(),
|
edge_set: Vec::new(),
|
||||||
|
|
@ -3501,6 +3542,7 @@ impl ExtendCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rebuild_geos(&mut self) {
|
fn rebuild_geos(&mut self) {
|
||||||
|
self.entity_index = ModifyEntityIndex::build(&self.all_entities);
|
||||||
self.geos = if self.edge_set.is_empty() {
|
self.geos = if self.edge_set.is_empty() {
|
||||||
build_geos(&self.all_entities)
|
build_geos(&self.all_entities)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -3681,18 +3723,14 @@ impl CadCommand for ExtendCommand {
|
||||||
let mut out: Vec<WireModel> = self
|
let mut out: Vec<WireModel> = self
|
||||||
.edge_set
|
.edge_set
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|h| {
|
.filter_map(|h| self.entity_index.get(&self.all_entities, *h))
|
||||||
self.all_entities.iter().find(|e| e.common().handle == *h)
|
|
||||||
})
|
|
||||||
.map(|e| {
|
.map(|e| {
|
||||||
WireModel::solid("edge_sel".into(), entity_pts(e), OPT_YELLOW, false)
|
WireModel::solid("edge_sel".into(), entity_pts(e), OPT_YELLOW, false)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
if !handle.is_null() && !self.edge_set.contains(&handle) {
|
if !handle.is_null() && !self.edge_set.contains(&handle) {
|
||||||
if let Some(e) = self
|
if let Some(e) = self
|
||||||
.all_entities
|
.entity_index.get(&self.all_entities, handle)
|
||||||
.iter()
|
|
||||||
.find(|e| e.common().handle == handle)
|
|
||||||
{
|
{
|
||||||
let mut c = OPT_YELLOW;
|
let mut c = OPT_YELLOW;
|
||||||
c[3] = 0.45;
|
c[3] = 0.45;
|
||||||
|
|
@ -3732,9 +3770,7 @@ impl CadCommand for ExtendCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
let entity = self
|
let entity = self
|
||||||
.all_entities
|
.entity_index.get(&self.all_entities, handle);
|
||||||
.iter()
|
|
||||||
.find(|e| e.common().handle == handle);
|
|
||||||
match entity {
|
match entity {
|
||||||
Some(EntityType::Line(l)) => {
|
Some(EntityType::Line(l)) => {
|
||||||
let ax = l.start.x;
|
let ax = l.start.x;
|
||||||
|
|
|
||||||
223
src/scene/cache/block_cache.rs
vendored
223
src/scene/cache/block_cache.rs
vendored
|
|
@ -15,7 +15,7 @@
|
||||||
// a visited set so a self-referential block produces a marker rather than
|
// a visited set so a self-referential block produces a marker rather than
|
||||||
// recursing forever.
|
// recursing forever.
|
||||||
|
|
||||||
use rustc_hash::FxHashMap as HashMap;
|
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use acadrust::types::{Color as AcadColor, LineWeight, Transform, Vector3};
|
use acadrust::types::{Color as AcadColor, LineWeight, Transform, Vector3};
|
||||||
|
|
@ -153,6 +153,29 @@ pub struct BlockDefn {
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
pub struct BlockCache {
|
pub struct BlockCache {
|
||||||
defns: HashMap<String, Arc<BlockDefn>>,
|
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 {
|
impl BlockCache {
|
||||||
|
|
@ -181,6 +204,18 @@ impl BlockCache {
|
||||||
use crate::par::prelude::*;
|
use crate::par::prelude::*;
|
||||||
let mut cache = Self::new();
|
let mut cache = Self::new();
|
||||||
let referenced = collect_referenced_blocks(doc);
|
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
|
// Each defn is built independently: nested INSERTs are stored as
|
||||||
// by-name references (`LocalSub::Nested`), never expanded here, so a
|
// by-name references (`LocalSub::Nested`), never expanded here, so a
|
||||||
// block's build never depends on another block's defn. That makes the
|
// 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
|
/// Walk all entities + all block_record contents collecting every distinct
|
||||||
/// `block_name` that appears in an Insert (transitively).
|
/// `block_name` that appears in an Insert (transitively).
|
||||||
fn collect_referenced_blocks(doc: &CadDocument) -> Vec<String> {
|
fn collect_referenced_blocks(doc: &CadDocument) -> Vec<String> {
|
||||||
use rustc_hash::FxHashSet as HashSet;
|
|
||||||
let mut seen: HashSet<String> = HashSet::default();
|
let mut seen: HashSet<String> = HashSet::default();
|
||||||
let mut queue: Vec<String> = Vec::new();
|
let mut queue: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
|
@ -795,6 +829,61 @@ pub fn expand_insert(
|
||||||
xform = xform.then(&scale_about_p);
|
xform = xform.then(&scale_about_p);
|
||||||
}
|
}
|
||||||
let name = ins_handle.value().to_string();
|
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 batches = Batches::default();
|
||||||
let mut visited: Vec<String> = Vec::with_capacity(8);
|
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));
|
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 {
|
fn aabb_pixel_size(local_aabb: [f32; 4], world_per_pixel: f32) -> f32 {
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,7 @@ impl Scene {
|
||||||
};
|
};
|
||||||
|
|
||||||
if !handle.is_null() {
|
if !handle.is_null() {
|
||||||
|
self.invalidate_dependency_index();
|
||||||
if let Some(model) = hatch_seed {
|
if let Some(model) = hatch_seed {
|
||||||
self.hatches.insert(handle, model);
|
self.hatches.insert(handle, model);
|
||||||
}
|
}
|
||||||
|
|
@ -275,6 +276,7 @@ impl Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
self.invalidate_dependency_index();
|
||||||
self.bump_geometry();
|
self.bump_geometry();
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
@ -353,6 +355,7 @@ impl Scene {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
*slot = entity;
|
*slot = entity;
|
||||||
|
self.invalidate_dependency_index();
|
||||||
|
|
||||||
// Drop stale derived caches for this handle, then reseed for the new
|
// Drop stale derived caches for this handle, then reseed for the new
|
||||||
// entity's type (which may differ from the old one).
|
// 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
|
/// Rebuild hatch / image / mesh caches after the document is modified
|
||||||
/// outside the normal `add_entity` path (e.g. REFCLOSE SAVE).
|
/// outside the normal `add_entity` path (e.g. REFCLOSE SAVE).
|
||||||
pub fn rebuild_derived_caches(&mut self) {
|
pub fn rebuild_derived_caches(&mut self) {
|
||||||
|
self.invalidate_dependency_index();
|
||||||
self.populate_hatches_from_document_unbumped();
|
self.populate_hatches_from_document_unbumped();
|
||||||
self.populate_images_from_document_unbumped();
|
self.populate_images_from_document_unbumped();
|
||||||
self.populate_meshes_impl(false, false);
|
self.populate_meshes_impl(false, false);
|
||||||
|
|
|
||||||
509
src/scene/mod.rs
509
src/scene/mod.rs
|
|
@ -45,6 +45,22 @@ pub(super) struct EntityIndex {
|
||||||
pub unbounded_handles: Vec<Handle>,
|
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]> {
|
fn hatch_interaction_aabb(hatch: &model::hatch_model::HatchModel) -> Option<[f64; 4]> {
|
||||||
let mut aabb = [
|
let mut aabb = [
|
||||||
f64::INFINITY,
|
f64::INFINITY,
|
||||||
|
|
@ -238,6 +254,21 @@ struct ResidentWireLayout {
|
||||||
marker_start: usize,
|
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)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct WireGpuPatch {
|
pub(crate) struct WireGpuPatch {
|
||||||
pub(crate) changes: Arc<Vec<(Handle, ChangeKind)>>,
|
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
|
/// Reported back to the UI so the user knows when a file had parser-junk
|
||||||
/// entities silently dropped.
|
/// entities silently dropped.
|
||||||
pub corrupt_dropped: usize,
|
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,
|
/// Background-thread open-phase timings in milliseconds (parse, purge,
|
||||||
/// derived-cache build). Filled in by `open_path_with_phase`; surfaced in
|
/// derived-cache build). Filled in by `open_path_with_phase`; surfaced in
|
||||||
/// the open-complete breakdown log so open-time regressions are visible.
|
/// the open-complete breakdown log so open-time regressions are visible.
|
||||||
|
|
@ -369,11 +409,30 @@ pub struct OpenTimings {
|
||||||
pub parse_ms: u32,
|
pub parse_ms: u32,
|
||||||
pub purge_ms: u32,
|
pub purge_ms: u32,
|
||||||
pub caches_ms: u32,
|
pub caches_ms: u32,
|
||||||
|
pub xref_ms: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build hatch / image / mesh caches from a document without needing `&mut Scene`.
|
/// Build hatch / image / mesh caches from a document without needing `&mut Scene`.
|
||||||
/// Intended to run on a background thread during file load.
|
/// Intended to run on a background thread during file load.
|
||||||
pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
|
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
|
// 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
|
// the memoised set so each reference re-reads / re-fetches once here (and
|
||||||
// stays cached across this document's later cache rebuilds).
|
// 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 image_handles: Vec<Handle> = Vec::new();
|
||||||
let mut mesh_handles: Vec<Handle> = Vec::new();
|
let mut mesh_handles: Vec<Handle> = Vec::new();
|
||||||
let mut centers: Vec<[f64; 3]> = 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;
|
let h = e.common().handle;
|
||||||
match e {
|
match e {
|
||||||
EntityType::Hatch(_) | EntityType::Solid(_) => hatch_handles.push(h),
|
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) {
|
if let Some(c) = offset_centroid(e, model_block, &prep) {
|
||||||
centers.push(c);
|
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);
|
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).
|
// 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];
|
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
|
// hatches
|
||||||
let hatches: HashMap<Handle, HatchModel> = hatch_handles
|
let hatches: HashMap<Handle, HatchModel> = hatch_handles
|
||||||
.par_iter()
|
.par_iter()
|
||||||
|
|
@ -465,15 +546,23 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
|
||||||
EntityType::Solid(solid) => Some(Scene::solid_hatch_model(solid, color)),
|
EntityType::Solid(solid) => Some(Scene::solid_hatch_model(solid, color)),
|
||||||
_ => None,
|
_ => 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();
|
.collect();
|
||||||
|
|
||||||
// images
|
// images
|
||||||
let images: HashMap<Handle, ImageModel> = image_handles
|
let images: HashMap<Handle, ImageModel> = image_handles
|
||||||
.par_iter()
|
.par_iter()
|
||||||
.filter_map(|&handle| match doc.get_entity(handle)? {
|
.filter_map(|&handle| {
|
||||||
EntityType::RasterImage(img) => ImageModel::from_raster_image(img).map(|m| (handle, m)),
|
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::Ole2Frame(ole) => ImageModel::from_ole2frame(ole).map(|m| (handle, m)),
|
||||||
EntityType::Underlay(u) => match doc.objects.get(&u.definition_handle) {
|
EntityType::Underlay(u) => match doc.objects.get(&u.definition_handle) {
|
||||||
Some(acadrust::objects::ObjectType::UnderlayDefinition(def)) => {
|
Some(acadrust::objects::ObjectType::UnderlayDefinition(def)) => {
|
||||||
|
|
@ -482,6 +571,12 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
|
||||||
_ => None,
|
_ => None,
|
||||||
},
|
},
|
||||||
_ => None,
|
_ => None,
|
||||||
|
};
|
||||||
|
let done = detail_done.fetch_add(1, Ordering::Relaxed) + 1;
|
||||||
|
if done & 0xff == 0 || done == detail_total {
|
||||||
|
report_detail(done);
|
||||||
|
}
|
||||||
|
result
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|
@ -511,10 +606,16 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
|
||||||
let (raw, ..) = view::render::render_style_for(doc, e);
|
let (raw, ..) = view::render::render_style_for(doc, e);
|
||||||
let color = view::render::adapt_to_bg(raw, LOAD_BG);
|
let color = view::render::adapt_to_bg(raw, LOAD_BG);
|
||||||
let top_level = layout_blocks.contains(&e.common().owner_handle);
|
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 };
|
let m = if top_level { offset_mesh_lod_set(m) } else { m };
|
||||||
(handle, m, top_level)
|
(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();
|
.collect();
|
||||||
let mut meshes: HashMap<Handle, MeshLodSet> = HashMap::default();
|
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 {
|
DerivedCaches {
|
||||||
local_extent_max,
|
local_extent_max,
|
||||||
local_center,
|
local_center,
|
||||||
|
|
@ -535,10 +640,54 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
|
||||||
meshes,
|
meshes,
|
||||||
block_meshes,
|
block_meshes,
|
||||||
corrupt_dropped: 0,
|
corrupt_dropped: 0,
|
||||||
|
xref_dropped: 0,
|
||||||
|
xrefs: Vec::new(),
|
||||||
|
prepared_geometry: None,
|
||||||
timings: OpenTimings::default(),
|
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
|
/// Mirrors `cache::block_cache::SANE_EXTENT` — wire coords past this magnitude
|
||||||
/// are treated as corruption rather than precision-relevant geometry.
|
/// are treated as corruption rather than precision-relevant geometry.
|
||||||
const CLUSTER_SANE_EXTENT: f64 = 1.0e8;
|
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
|
/// filtered variants. Viewports sharing a frozen set share one entry (like
|
||||||
/// the resident wire set). Empty for a viewport with no frozen layers (it
|
/// the resident wire set). Empty for a viewport with no frozen layers (it
|
||||||
/// reuses the unfiltered `*_arc` sets directly).
|
/// reuses the unfiltered `*_arc` sets directly).
|
||||||
frozen_hatch_cache:
|
frozen_hatch_cache: RefCell<HashMap<(String, u64), (u64, u64, Arc<Vec<HatchModel>>)>>,
|
||||||
RefCell<HashMap<(String, u64), (u64, u64, Arc<Vec<HatchModel>>)>>,
|
frozen_wipeout_cache: RefCell<HashMap<(String, 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_image_cache: RefCell<HashMap<u64, (u64, Arc<Vec<ImageModel>>)>>,
|
||||||
frozen_mesh_cache:
|
frozen_mesh_cache: RefCell<HashMap<(String, u64), (u64, Arc<Vec<MeshLodSet>>)>>,
|
||||||
RefCell<HashMap<(String, u64), (u64, Arc<Vec<MeshLodSet>>)>>,
|
|
||||||
/// Cached block-INSERT hatches for hit-testing, keyed by geometry_epoch.
|
/// Cached block-INSERT hatches for hit-testing, keyed by geometry_epoch.
|
||||||
/// Building this explodes every model-space INSERT, so without the cache a
|
/// Building this explodes every model-space INSERT, so without the cache a
|
||||||
/// heavy block-instanced drawing re-explodes thousands of inserts on every
|
/// 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.
|
/// 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.
|
/// 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>)>>,
|
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
|
/// Tessellated block definitions in block-local coords, keyed by render
|
||||||
/// background and block epoch. Model and Paper adapt black/white colours
|
/// background and block epoch. Model and Paper adapt black/white colours
|
||||||
/// differently; retaining both variants prevents a full block rebuild on
|
/// differently; retaining both variants prevents a full block rebuild on
|
||||||
/// every layout-tab switch.
|
/// every layout-tab switch.
|
||||||
block_defn_cache:
|
block_defn_cache: RefCell<HashMap<[u32; 4], (u64, Arc<cache::block_cache::BlockCache>)>>,
|
||||||
RefCell<HashMap<[u32; 4], (u64, Arc<cache::block_cache::BlockCache>)>>,
|
|
||||||
/// Spatial index + always-emit list for top-level entities
|
/// Spatial index + always-emit list for top-level entities
|
||||||
/// (Phase 2.1). Lazily rebuilt by `entity_index()` on
|
/// (Phase 2.1). Lazily rebuilt by `entity_index()` on
|
||||||
/// `geometry_epoch` change. See `EntityIndex` for what each side
|
/// `geometry_epoch` change. See `EntityIndex` for what each side
|
||||||
|
|
@ -1455,6 +1605,7 @@ impl Scene {
|
||||||
annotation_affects_wires: std::cell::Cell::new(None),
|
annotation_affects_wires: std::cell::Cell::new(None),
|
||||||
model_extents_cache: RefCell::new(None),
|
model_extents_cache: RefCell::new(None),
|
||||||
entity_block_map_cache: RefCell::new(None),
|
entity_block_map_cache: RefCell::new(None),
|
||||||
|
dependency_index_cache: RefCell::new(None),
|
||||||
block_defn_cache: RefCell::new(HashMap::default()),
|
block_defn_cache: RefCell::new(HashMap::default()),
|
||||||
entity_index_cache: RefCell::new(None),
|
entity_index_cache: RefCell::new(None),
|
||||||
last_render_aspect: std::cell::Cell::new(16.0 / 9.0),
|
last_render_aspect: std::cell::Cell::new(16.0 / 9.0),
|
||||||
|
|
@ -1555,6 +1706,30 @@ impl Scene {
|
||||||
arc
|
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
|
/// 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
|
/// 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
|
/// 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
|
changes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3245,30 +3424,7 @@ impl Scene {
|
||||||
} else {
|
} else {
|
||||||
self.paper_bg_color
|
self.paper_bg_color
|
||||||
};
|
};
|
||||||
let key = {
|
let key = Self::resident_wire_key(block, bg, anno_scale_override, frozen_layers);
|
||||||
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 sets = self.resident_wire_sets.borrow();
|
let sets = self.resident_wire_sets.borrow();
|
||||||
if let Some(set) = sets.get(&key) {
|
if let Some(set) = sets.get(&key) {
|
||||||
|
|
@ -3331,6 +3487,36 @@ impl Scene {
|
||||||
arc
|
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`:
|
/// 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
|
/// `(prev_gen, changed handles)` when the Model set reached `gen` via an
|
||||||
/// incremental resident patch, else `None`. Read (not consumed) so every
|
/// 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
|
/// Spatial index + always-emit list for top-level entities. Lazily
|
||||||
/// rebuilt on `geometry_epoch` change.
|
/// rebuilt on `geometry_epoch` change.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ pub struct Pipeline {
|
||||||
/// Used to draw ghost copies of selected wires through occluding geometry.
|
/// Used to draw ghost copies of selected wires through occluding geometry.
|
||||||
wire_xray_pipeline: wgpu::RenderPipeline,
|
wire_xray_pipeline: wgpu::RenderPipeline,
|
||||||
/// Layout for the per-wire `WireConst` storage buffer (group 1 of the wire /
|
/// 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`.
|
/// compatibility mode. Passed to `WireGpu::from_run` / `from_batch`.
|
||||||
pub(crate) wire_const_bgl: Option<wgpu::BindGroupLayout>,
|
pub(crate) wire_const_bgl: Option<wgpu::BindGroupLayout>,
|
||||||
wipeout_pipeline: wgpu::RenderPipeline,
|
wipeout_pipeline: wgpu::RenderPipeline,
|
||||||
|
|
@ -338,8 +338,8 @@ impl Pipeline {
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Wire pipeline ──────────────────────────────────────────────────
|
// ── Wire pipeline ──────────────────────────────────────────────────
|
||||||
// Select once per device. The fast native path hoists shared constants
|
// Select once per device. Native and WebGPU hoist shared constants into
|
||||||
// into storage; compatibility mode keeps them in 10 packed attributes.
|
// storage when limits allow; WebGL2/compat keeps packed attributes.
|
||||||
let wire_mode = wire_gpu::WirePipelineMode::select(device);
|
let wire_mode = wire_gpu::WirePipelineMode::select(device);
|
||||||
let renderer_mode_name =
|
let renderer_mode_name =
|
||||||
if wire_mode.uses_storage() { "fast-storage" } else { "packed-compat" };
|
if wire_mode.uses_storage() { "fast-storage" } else { "packed-compat" };
|
||||||
|
|
@ -357,12 +357,9 @@ impl Pipeline {
|
||||||
renderer_mode_name,
|
renderer_mode_name,
|
||||||
device.limits().max_storage_buffers_per_shader_stage
|
device.limits().max_storage_buffers_per_shader_stage
|
||||||
);
|
);
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
let wire_const_bgl = wire_mode
|
let wire_const_bgl = wire_mode
|
||||||
.uses_storage()
|
.uses_storage()
|
||||||
.then(|| wire_gpu::WireConst::bind_group_layout(device));
|
.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];
|
let mut wire_bgls: Vec<&wgpu::BindGroupLayout> = vec![&frame_bgl];
|
||||||
if let Some(bgl) = &wire_const_bgl {
|
if let Some(bgl) = &wire_const_bgl {
|
||||||
wire_bgls.push(bgl);
|
wire_bgls.push(bgl);
|
||||||
|
|
@ -379,7 +376,6 @@ impl Pipeline {
|
||||||
let wire_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
let wire_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
label: Some("wire.shader"),
|
label: Some("wire.shader"),
|
||||||
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(match wire_mode {
|
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(match wire_mode {
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
wire_gpu::WirePipelineMode::IndexedStorage => {
|
wire_gpu::WirePipelineMode::IndexedStorage => {
|
||||||
include_str!("../../shaders/wire_indexed.wgsl")
|
include_str!("../../shaders/wire_indexed.wgsl")
|
||||||
}
|
}
|
||||||
|
|
@ -3157,6 +3153,14 @@ fn create_msaa_texture(
|
||||||
pub struct MultiPipeline {
|
pub struct MultiPipeline {
|
||||||
pub(crate) inners: Vec<Pipeline>,
|
pub(crate) inners: Vec<Pipeline>,
|
||||||
format: wgpu::TextureFormat,
|
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
|
/// The resident wire batches, keyed by `wire_content_id` and shared across
|
||||||
/// every slot (and every pane — one `MultiPipeline` backs all of them) that
|
/// 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
|
/// renders the same content. `prepare` builds an entry once on a cache miss
|
||||||
|
|
@ -3189,8 +3193,70 @@ impl MultiPipeline {
|
||||||
let n = n.max(1);
|
let n = n.max(1);
|
||||||
while self.inners.len() < n {
|
while self.inners.len() < n {
|
||||||
self.inners.push(Pipeline::new(device, queue, self.format));
|
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
|
/// Send wgpu's uncaptured validation errors to stderr instead of the default
|
||||||
|
|
@ -3227,6 +3293,9 @@ impl iced::widget::shader::Pipeline for MultiPipeline {
|
||||||
Self {
|
Self {
|
||||||
inners: vec![Pipeline::new(device, queue, format)],
|
inners: vec![Pipeline::new(device, queue, format)],
|
||||||
format,
|
format,
|
||||||
|
slot_by_instance: rustc_hash::FxHashMap::default(),
|
||||||
|
slot_last_used: vec![0],
|
||||||
|
slot_clock: 0,
|
||||||
wire_buffer_cache: rustc_hash::FxHashMap::default(),
|
wire_buffer_cache: rustc_hash::FxHashMap::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,18 +58,17 @@ fn instance_buffer_mapped<T: bytemuck::Pod>(
|
||||||
|
|
||||||
// ── Instance layout ───────────────────────────────────────────────────────
|
// ── 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
|
// 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
|
// 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
|
// per-wire `WireConst` storage buffer indexed by `wire_id`, so the instance
|
||||||
// keeps only the per-segment data (endpoints + arc-length distances). Cuts the
|
// 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
|
// 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
|
// 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
|
// storage buffers, so the wasm build below keeps the original self-contained
|
||||||
// fat instance.
|
// fat instance.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
pub struct WireInstance {
|
pub struct WireInstance {
|
||||||
|
|
@ -87,8 +86,6 @@ pub struct WireInstance {
|
||||||
/// exactly one 64-byte cache line.
|
/// exactly one 64-byte cache line.
|
||||||
pub taper_ratio: [u16; 2],
|
pub taper_ratio: [u16; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
impl WireInstance {
|
impl WireInstance {
|
||||||
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
|
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
|
||||||
// Must match `InstanceIn` in wire_indexed.wgsl.
|
// 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
|
/// layout: three vec4 then eight scalars = 80 B, matching `WireConst` in
|
||||||
/// wire_indexed.wgsl. `align_end` / `align_total` carry the "A"-type endpoint
|
/// wire_indexed.wgsl. `align_end` / `align_total` carry the "A"-type endpoint
|
||||||
/// alignment (see `wire_distances`); 0.0 total = no alignment.
|
/// alignment (see `wire_distances`); 0.0 total = no alignment.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
pub struct WireConst {
|
pub struct WireConst {
|
||||||
|
|
@ -134,8 +130,6 @@ pub struct WireConst {
|
||||||
pub _pad1: f32,
|
pub _pad1: f32,
|
||||||
pub _pad2: f32,
|
pub _pad2: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
impl WireConst {
|
impl WireConst {
|
||||||
/// Bind-group layout for the per-wire storage buffer (group 1 of the wire /
|
/// Bind-group layout for the per-wire storage buffer (group 1 of the wire /
|
||||||
/// xray pipelines). Read-only storage, visible to the vertex stage.
|
/// xray pipelines). Read-only storage, visible to the vertex stage.
|
||||||
|
|
@ -158,8 +152,8 @@ impl WireConst {
|
||||||
|
|
||||||
// ── Packed compatibility instance (no vertex-stage storage) ────────────────
|
// ── Packed compatibility instance (no vertex-stage storage) ────────────────
|
||||||
//
|
//
|
||||||
// Web always uses this layout. Native selects it at runtime for adapters whose
|
// WebGL2 and limited native adapters use this layout. WebGPU selects the
|
||||||
// storage-buffer limits are insufficient, or when --compat-renderer is set.
|
// indexed storage path when its reported limits satisfy the same requirement.
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
pub struct PackedWireInstance {
|
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
|
/// Wire and hatch pipelines switch together: the fast path uses storage
|
||||||
/// buffers; the compatibility path carries wire constants in packed vertex
|
/// buffers; the compatibility path carries wire constants in packed vertex
|
||||||
/// attributes and hatch data in a texture.
|
/// attributes and hatch data in a texture.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum WirePipelineMode {
|
pub enum WirePipelineMode {
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
IndexedStorage,
|
IndexedStorage,
|
||||||
Packed,
|
Packed,
|
||||||
}
|
}
|
||||||
|
fn select_pipeline(max_storage_buffers_per_stage: u32, forced: bool) -> WirePipelineMode {
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
fn select_native_pipeline(max_storage_buffers_per_stage: u32, forced: bool) -> WirePipelineMode {
|
|
||||||
const REQUIRED_STORAGE_BUFFERS_PER_STAGE: u32 = 5;
|
const REQUIRED_STORAGE_BUFFERS_PER_STAGE: u32 = 5;
|
||||||
if forced || max_storage_buffers_per_stage < REQUIRED_STORAGE_BUFFERS_PER_STAGE {
|
if forced || max_storage_buffers_per_stage < REQUIRED_STORAGE_BUFFERS_PER_STAGE {
|
||||||
WirePipelineMode::Packed
|
WirePipelineMode::Packed
|
||||||
|
|
@ -249,23 +237,15 @@ fn select_native_pipeline(max_storage_buffers_per_stage: u32, forced: bool) -> W
|
||||||
|
|
||||||
impl WirePipelineMode {
|
impl WirePipelineMode {
|
||||||
pub fn select(device: &wgpu::Device) -> Self {
|
pub fn select(device: &wgpu::Device) -> Self {
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
{
|
|
||||||
let _ = device;
|
|
||||||
Self::Packed
|
|
||||||
}
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
{
|
let forced = crate::cli::gui_config().compat_renderer;
|
||||||
select_native_pipeline(
|
#[cfg(target_arch = "wasm32")]
|
||||||
device.limits().max_storage_buffers_per_shader_stage,
|
let forced = false;
|
||||||
crate::cli::gui_config().compat_renderer,
|
select_pipeline(device.limits().max_storage_buffers_per_shader_stage, forced)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uses_storage(self) -> bool {
|
pub fn uses_storage(self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
Self::IndexedStorage => true,
|
Self::IndexedStorage => true,
|
||||||
Self::Packed => false,
|
Self::Packed => false,
|
||||||
}
|
}
|
||||||
|
|
@ -273,7 +253,6 @@ impl WirePipelineMode {
|
||||||
|
|
||||||
pub fn layout<'a>(self) -> wgpu::VertexBufferLayout<'a> {
|
pub fn layout<'a>(self) -> wgpu::VertexBufferLayout<'a> {
|
||||||
match self {
|
match self {
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
Self::IndexedStorage => WireInstance::layout(),
|
Self::IndexedStorage => WireInstance::layout(),
|
||||||
Self::Packed => PackedWireInstance::layout(),
|
Self::Packed => PackedWireInstance::layout(),
|
||||||
}
|
}
|
||||||
|
|
@ -494,9 +473,8 @@ fn emit_wire_packed(
|
||||||
instances
|
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.
|
/// plus the one `WireConst` record every segment of this wire shares.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
pub(crate) fn emit_wire_native(
|
pub(crate) fn emit_wire_native(
|
||||||
wire: &WireModel,
|
wire: &WireModel,
|
||||||
wire_id: u32,
|
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
|
/// Build the shared per-wire `WireConst` storage buffer and its bind group.
|
||||||
/// (native only). All instance-buffer chunks from one build reference the same
|
/// All instance-buffer chunks from one build reference the same
|
||||||
/// buffer via their global `wire_id`, so a single bind group is cloned into
|
/// buffer via their global `wire_id`, so a single bind group is cloned into
|
||||||
/// each chunk.
|
/// each chunk.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
fn build_const_bind_group(
|
fn build_const_bind_group(
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
bgl: &wgpu::BindGroupLayout,
|
bgl: &wgpu::BindGroupLayout,
|
||||||
|
|
@ -677,7 +654,6 @@ impl WireGpu {
|
||||||
mesh_edge: bool,
|
mesh_edge: bool,
|
||||||
const_bgl: Option<&wgpu::BindGroupLayout>,
|
const_bgl: Option<&wgpu::BindGroupLayout>,
|
||||||
) -> Vec<Self> {
|
) -> Vec<Self> {
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
if let Some(const_bgl) = const_bgl {
|
if let Some(const_bgl) = const_bgl {
|
||||||
const MAX_INSTANCES: usize =
|
const MAX_INSTANCES: usize =
|
||||||
268_435_456 / std::mem::size_of::<WireInstance>();
|
268_435_456 / std::mem::size_of::<WireInstance>();
|
||||||
|
|
@ -769,7 +745,6 @@ impl WireGpu {
|
||||||
if total_segs == 0 {
|
if total_segs == 0 {
|
||||||
return vec![];
|
return vec![];
|
||||||
}
|
}
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
if let Some(const_bgl) = const_bgl {
|
if let Some(const_bgl) = const_bgl {
|
||||||
// GPU max buffer size is 256 MB; chunk to stay within the limit.
|
// GPU max buffer size is 256 MB; chunk to stay within the limit.
|
||||||
const MAX_INSTANCES: usize =
|
const MAX_INSTANCES: usize =
|
||||||
|
|
|
||||||
|
|
@ -391,7 +391,10 @@ impl Scene {
|
||||||
// Report the exact erased handles so derived caches drop just those and
|
// Report the exact erased handles so derived caches drop just those and
|
||||||
// the resident set removes only their wires (bump_entities drops them
|
// the resident set removes only their wires (bump_entities drops them
|
||||||
// from the tessellation memos too).
|
// from the tessellation memos too).
|
||||||
self.bump_entities(&erased);
|
if !erased.is_empty() {
|
||||||
|
self.invalidate_dependency_index();
|
||||||
|
self.bump_entities(&erased);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Restore erased Arc-backed entities without re-linking their still-present
|
/// Restore erased Arc-backed entities without re-linking their still-present
|
||||||
|
|
@ -414,6 +417,7 @@ impl Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !changes.is_empty() {
|
if !changes.is_empty() {
|
||||||
|
self.invalidate_dependency_index();
|
||||||
self.bump_entities(&changes);
|
self.bump_entities(&changes);
|
||||||
}
|
}
|
||||||
restored
|
restored
|
||||||
|
|
|
||||||
|
|
@ -152,13 +152,6 @@ pub struct Primitive {
|
||||||
pub(in crate::scene) viewports: Vec<ViewportData>,
|
pub(in crate::scene) viewports: Vec<ViewportData>,
|
||||||
/// Background color used to clear each viewport's MSAA buffer.
|
/// Background color used to clear each viewport's MSAA buffer.
|
||||||
pub(in crate::scene) bg_color: [f32; 4],
|
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.
|
/// One input-to-render sample, carried only when PERF tracing is enabled.
|
||||||
pub(in crate::scene) nav_perf: Option<NavPerfSample>,
|
pub(in crate::scene) nav_perf: Option<NavPerfSample>,
|
||||||
}
|
}
|
||||||
|
|
@ -250,10 +243,11 @@ impl shader::Primitive for Primitive {
|
||||||
let phys = viewport.physical_size();
|
let phys = viewport.physical_size();
|
||||||
let full_size = Size::new(phys.width, phys.height);
|
let full_size = Size::new(phys.width, phys.height);
|
||||||
let scale = viewport.scale_factor() as f32;
|
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() {
|
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
|
// Pipeline slots are addressed by list index, but off-canvas
|
||||||
// viewports are dropped from the list — so a slot can be reused by a
|
// viewports are dropped from the list — so a slot can be reused by a
|
||||||
// DIFFERENT viewport across frames (e.g. the first viewport scrolls
|
// 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
|
// the whole wire buffer. Only for the scissor-free, mesh-free
|
||||||
// (single-batch) Model set; scissored paper viewports and mixed
|
// (single-batch) Model set; scissored paper viewports and mixed
|
||||||
// 2D/3D sets fall through to the shared batched path below.
|
// 2D/3D sets fall through to the shared batched path below.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let mut arena_served = false;
|
let mut arena_served = false;
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
let arena_served = false;
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let _perf = crate::perf::enabled();
|
let _perf = crate::perf::enabled();
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
|
@ -895,9 +892,12 @@ impl shader::Primitive for Primitive {
|
||||||
let ch = clip.height as f32;
|
let ch = clip.height as f32;
|
||||||
let clip_right = clip.x + clip.width;
|
let clip_right = clip.x + clip.width;
|
||||||
let clip_bottom = clip.y + clip.height;
|
let clip_bottom = clip.y + clip.height;
|
||||||
for (i, vp) in self.viewports.iter().enumerate() {
|
for vp in &self.viewports {
|
||||||
let Some(inner) = pipeline.inners.get(self.base_slot + i) else {
|
let Some(slot) = pipeline.slot_by_instance.get(&vp.instance_id) else {
|
||||||
break;
|
continue;
|
||||||
|
};
|
||||||
|
let Some(inner) = pipeline.inners.get(*slot) else {
|
||||||
|
continue;
|
||||||
};
|
};
|
||||||
// Where the viewport would land on the surface in absolute
|
// Where the viewport would land on the surface in absolute
|
||||||
// pixels (i32 because either edge may stick off the canvas).
|
// 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.build_ms = nav_build_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
sample
|
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 {
|
Primitive {
|
||||||
viewports,
|
viewports,
|
||||||
bg_color,
|
bg_color,
|
||||||
base_slot,
|
|
||||||
nav_perf: perf_nav,
|
nav_perf: perf_nav,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1466,7 +1461,6 @@ impl Scene {
|
||||||
return Primitive {
|
return Primitive {
|
||||||
viewports: vec![],
|
viewports: vec![],
|
||||||
bg_color,
|
bg_color,
|
||||||
base_slot: tile_idx,
|
|
||||||
nav_perf: None,
|
nav_perf: None,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -1514,7 +1508,6 @@ impl Scene {
|
||||||
Primitive {
|
Primitive {
|
||||||
viewports,
|
viewports,
|
||||||
bg_color,
|
bg_color,
|
||||||
base_slot: tile_idx,
|
|
||||||
nav_perf: perf_nav,
|
nav_perf: perf_nav,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! Modal overlay shown while a CAD file is being loaded.
|
//! Modal overlay shown while a CAD file is being loaded.
|
||||||
//!
|
//!
|
||||||
//! Displays the file name, size, current phase, an indeterminate animated
|
//! Displays the file name, size, current phase, measured progress, and a
|
||||||
//! progress bar, and a Cancel button.
|
//! Cancel button.
|
||||||
|
|
||||||
use iced::time::Instant;
|
use iced::time::Instant;
|
||||||
use iced::widget::{button, column, container, row, stack, text, Space};
|
use iced::widget::{button, column, container, row, stack, text, Space};
|
||||||
|
|
@ -10,20 +10,18 @@ use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
use crate::app::{
|
use crate::app::{
|
||||||
Message, OpenProgress, OPEN_PHASE_CACHING, OPEN_PHASE_FINALIZING, OPEN_PHASE_PARSING,
|
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 CARD_WIDTH: f32 = 420.0;
|
||||||
const BAR_TRACK_WIDTH: f32 = 380.0;
|
const BAR_TRACK_WIDTH: f32 = 380.0;
|
||||||
const BAR_TRACK_HEIGHT: f32 = 6.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 {
|
fn phase_label(phase: u8) -> &'static str {
|
||||||
match phase {
|
match phase {
|
||||||
OPEN_PHASE_READING => "Reading file…",
|
OPEN_PHASE_READING => "Reading file…",
|
||||||
OPEN_PHASE_PARSING => "Parsing entities…",
|
OPEN_PHASE_PARSING => "Parsing entities…",
|
||||||
|
OPEN_PHASE_XREF => "Loading references…",
|
||||||
OPEN_PHASE_CACHING => "Building scene caches…",
|
OPEN_PHASE_CACHING => "Building scene caches…",
|
||||||
OPEN_PHASE_FINALIZING => "Finalizing…",
|
OPEN_PHASE_FINALIZING => "Finalizing…",
|
||||||
_ => "Working…",
|
_ => "Working…",
|
||||||
|
|
@ -46,29 +44,22 @@ fn format_size(bytes: u64) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the left-offset of the moving highlight inside the track.
|
pub fn view<'a>(progress: &'a OpenProgress, _now: Instant) -> Element<'a, Message> {
|
||||||
/// Bounces left↔right using a triangle wave so the user sees motion even when
|
let phase = progress.state.phase.load(Ordering::Acquire);
|
||||||
/// the actual phase atomic stays put for a while.
|
let basis_points = progress
|
||||||
fn bar_offset(elapsed_ms: f32) -> f32 {
|
.state
|
||||||
let travel = (BAR_TRACK_WIDTH - BAR_WINDOW_WIDTH).max(0.0);
|
.basis_points
|
||||||
let cycle = (elapsed_ms / BAR_PERIOD_MS).fract();
|
.load(Ordering::Relaxed)
|
||||||
let tri = if cycle < 0.5 {
|
.min(10000);
|
||||||
cycle * 2.0
|
let fraction = basis_points as f32 / 10000.0;
|
||||||
} else {
|
let fill_width = BAR_TRACK_WIDTH * fraction;
|
||||||
(1.0 - cycle) * 2.0
|
let trailing = (BAR_TRACK_WIDTH - fill_width).max(0.0);
|
||||||
};
|
|
||||||
tri * travel
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn view<'a>(progress: &'a OpenProgress, now: Instant) -> Element<'a, Message> {
|
let bar_fill: Element<'_, Message> = container(
|
||||||
let phase = progress.phase.load(Ordering::Relaxed);
|
Space::new()
|
||||||
let elapsed_ms = now.saturating_duration_since(progress.started).as_millis() as f32;
|
.width(Length::Fixed(fill_width))
|
||||||
|
.height(Length::Fixed(BAR_TRACK_HEIGHT)),
|
||||||
// ── 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)))
|
|
||||||
.style(|_: &Theme| container::Style {
|
.style(|_: &Theme| container::Style {
|
||||||
background: Some(Background::Color(Color {
|
background: Some(Background::Color(Color {
|
||||||
r: 0.30,
|
r: 0.30,
|
||||||
|
|
@ -84,11 +75,8 @@ pub fn view<'a>(progress: &'a OpenProgress, now: Instant) -> Element<'a, Message
|
||||||
})
|
})
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
let bar_moving: Element<'_, Message> = row![
|
let bar_value: Element<'_, Message> = row![
|
||||||
Space::new()
|
bar_fill,
|
||||||
.width(Length::Fixed(offset))
|
|
||||||
.height(Length::Fixed(BAR_TRACK_HEIGHT)),
|
|
||||||
bar_window,
|
|
||||||
Space::new()
|
Space::new()
|
||||||
.width(Length::Fixed(trailing))
|
.width(Length::Fixed(trailing))
|
||||||
.height(Length::Fixed(BAR_TRACK_HEIGHT)),
|
.height(Length::Fixed(BAR_TRACK_HEIGHT)),
|
||||||
|
|
@ -111,7 +99,7 @@ pub fn view<'a>(progress: &'a OpenProgress, now: Instant) -> Element<'a, Message
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
bar_moving,
|
bar_value,
|
||||||
]
|
]
|
||||||
.width(Length::Fixed(BAR_TRACK_WIDTH))
|
.width(Length::Fixed(BAR_TRACK_WIDTH))
|
||||||
.height(Length::Fixed(BAR_TRACK_HEIGHT)),
|
.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,
|
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)
|
.size(12)
|
||||||
.color(Color {
|
.color(Color {
|
||||||
r: 0.70,
|
r: 0.70,
|
||||||
|
|
|
||||||
20
web/ocs-parse-worker.js
Normal file
20
web/ocs-parse-worker.js
Normal 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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue