forked from 1b-agent/opencadstudio-upstream
web: raster images, WBLOCK/SAVEALL/SCRIPT/CTB/LAYTRANS/INSERT, filled TTF text
A fresh web-vs-desktop audit found the remaining gaps; this closes the
top ten.
Raster images: IMAGE attach read the picked file via image::open on a
name-only path (dead on wasm), and raster references inside opened
drawings decoded the same way. Picked bytes are now registered in a
name-keyed store (the PDF-underlay pattern), decoding runs from memory,
and dropped .png/.jpg/.bmp/.tif files register by name so unresolved
references in opened drawings re-resolve and render (and export to PDF).
Command gaps fixed with the bytes-at-pick / download_bytes pattern:
- WBLOCK now downloads the extracted block instead of failing on fs.
- SAVEALL saves every dirty tab instead of printing a no-op message.
- SCRIPT picks and reads the .scr file through the browser picker.
- CENTERLTYPEFILE picks the .lin/.dnl file and populates from text.
- Custom .ctb tables picked on web now parse, register for the
session, appear in the plot dialog list, and apply (load_named
previously returned builtin-only on wasm).
- LAYTRANS 'Load standards file' parses the picked drawing on the
existing web worker.
- INSERT 'from file' in the block palette parses picked bytes and
feeds the shared import path.
Text fidelity: TTF-named styles rendered as thin stroke glyphs on web
because the font database was empty. Fetched Noto faces are now
registered into it with a family alias table (Arial, Times New Roman,
Cambria, ... -> Noto Sans/Serif), so those styles render filled, shape,
export to PDF as vector fills, and appear in the style picker.
ViewCube labels request semibold again on web.
docs/native-vs-web.md: the hatch and font rows were stale (both work on
web now), the GPU row claimed WebGPU is used where available (the
backend is pinned to OpenGL), and the parallelism row missed the parse
worker — all corrected.
(cherry picked from commit c643a10e)
Ported onto main (#1717 review): conflicts resolved toward main — the
font-file lookup, PDF searchable text, purge service, block palette and
plot-style copy stay as they are on main; the web pieces (font registry
and aliases, session plot styles, SCRIPT/SAVEALL/CENTERLTYPEFILE picks,
block-from-file on the web) are layered on top.
This commit is contained in:
1 parent
69fa7cae4f
commit
9cd898a17a
42 files changed
+1315
-155
No files matched your search
+126
-17
@@ -232,6 +232,42 @@ pub async fn pick_layer_mapping_path(save: bool) -> Option<PathBuf> {
|
||||
Some(crate::sys::handle_path(&handle))
|
||||
}
|
||||
|
||||
/// Web SCRIPT: pick the .scr file through the browser and read its text —
|
||||
/// the byte-level stand-in for the typed path the native command reads with
|
||||
/// `std::fs::read_to_string`. `None` means the user cancelled.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn pick_script_web() -> Option<(String, String)> {
|
||||
let Some(handle) = crate::sys::file_dialog()
|
||||
.set_title(crate::t!("SCRIPT path to the .scr file:").as_ref())
|
||||
.add_filter(crate::t!("All Files").as_ref(), &["*"])
|
||||
.pick_file()
|
||||
.await
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
let name = handle.file_name();
|
||||
let bytes = handle.read().await;
|
||||
Some((name, String::from_utf8_lossy(&bytes).into_owned()))
|
||||
}
|
||||
|
||||
/// Web CENTERLTYPEFILE: pick the linetype (.lin/.dnl) file through the
|
||||
/// browser and read its text — the stand-in for the typed path the native
|
||||
/// command reads inside `set_centerline_setting`. `None` = cancelled.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn pick_linetype_file_web() -> Option<(String, String)> {
|
||||
let Some(handle) = crate::sys::file_dialog()
|
||||
.set_title(crate::t!("Load linetype file").as_ref())
|
||||
.add_filter(crate::t!("All Files").as_ref(), &["*"])
|
||||
.pick_file()
|
||||
.await
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
let name = handle.file_name();
|
||||
let bytes = handle.read().await;
|
||||
Some((name, String::from_utf8_lossy(&bytes).into_owned()))
|
||||
}
|
||||
|
||||
/// Load a CAD file from a known path. Parsing and cache building run on a
|
||||
/// dedicated OS thread so the async executor stays free for rendering during
|
||||
/// the load. Writes phase markers into `phase` so the UI can show
|
||||
@@ -734,6 +770,50 @@ async fn load_web_bytes(
|
||||
Ok((name.to_string(), path, doc, caches))
|
||||
}
|
||||
|
||||
/// Web: parse a *second* drawing (a block to insert, a layer-standards
|
||||
/// file) from bytes on the parse worker, with the same post-parse fixups
|
||||
/// `load_web_bytes` applies — the byte-level counterpart of `load_file`.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
async fn parse_web_document(name: &str, bytes: &[u8]) -> Result<CadDocument, String> {
|
||||
let (outcome, _) = web_worker::parse_document(name, bytes, false, "")
|
||||
.await
|
||||
.map_err(|error| error.message)?;
|
||||
let mut doc = outcome.document;
|
||||
normalize_block_origins(&mut doc);
|
||||
if name.to_ascii_lowercase().ends_with(".dxf") {
|
||||
fix_dxf_dimension_rotations(&mut doc);
|
||||
fix_dxf_layout_plot_settings(&mut doc);
|
||||
}
|
||||
fix_viewport_status_flags(&mut doc);
|
||||
fix_current_style_names(&mut doc);
|
||||
Ok(doc)
|
||||
}
|
||||
|
||||
/// Web: show the browser picker, read the chosen file's bytes, and parse it
|
||||
/// as a drawing on the worker — `pick_and_load_web`'s pick-read-parse core
|
||||
/// for features that consume a second drawing instead of opening a tab.
|
||||
/// `Ok(None)` means the user cancelled.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn pick_and_parse_web(
|
||||
title: &str,
|
||||
filter_label: &str,
|
||||
extensions: &[&str],
|
||||
) -> Result<Option<(String, CadDocument)>, String> {
|
||||
let Some(handle) = crate::sys::file_dialog()
|
||||
.set_title(title)
|
||||
.add_filter(filter_label, extensions)
|
||||
.add_filter(crate::t!("All Files").as_ref(), &["*"])
|
||||
.pick_file()
|
||||
.await
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let name = handle.file_name();
|
||||
let bytes = handle.read().await;
|
||||
let document = parse_web_document(&name, &bytes).await?;
|
||||
Ok(Some((name, document)))
|
||||
}
|
||||
|
||||
/// Parse a drawing from bytes using its name or recovery-file signature.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn load_bytes(name: &str, bytes: Vec<u8>) -> Result<CadDocument, String> {
|
||||
@@ -1766,29 +1846,51 @@ pub async fn pick_plot_style() -> Result<Option<plot_style::PlotStyleTable>, Str
|
||||
let Some(handle) = dialog.pick_file().await else {
|
||||
return Ok(None);
|
||||
};
|
||||
let path = crate::sys::handle_path(&handle);
|
||||
let table = plot_style::PlotStyleTable::load(&path)?;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let (Ok(dir), Some(file_name)) = (plot_style::ensure_plot_styles_dir(), path.file_name()) {
|
||||
let destination = dir.join(file_name);
|
||||
let same_place = path.parent().is_some_and(|parent| {
|
||||
parent
|
||||
.canonicalize()
|
||||
.ok()
|
||||
.zip(dir.canonicalize().ok())
|
||||
.is_some_and(|(a, b)| a == b)
|
||||
});
|
||||
if !same_place && !destination.exists() {
|
||||
let _ = std::fs::copy(&path, &destination);
|
||||
{
|
||||
let path = crate::sys::handle_path(&handle);
|
||||
let table = plot_style::PlotStyleTable::load(&path)?;
|
||||
if let (Ok(dir), Some(file_name)) =
|
||||
(plot_style::ensure_plot_styles_dir(), path.file_name())
|
||||
{
|
||||
let destination = dir.join(file_name);
|
||||
let same_place = path.parent().is_some_and(|parent| {
|
||||
parent
|
||||
.canonicalize()
|
||||
.ok()
|
||||
.zip(dir.canonicalize().ok())
|
||||
.is_some_and(|(a, b)| a == b)
|
||||
});
|
||||
if !same_place && !destination.exists() {
|
||||
let _ = std::fs::copy(&path, &destination);
|
||||
}
|
||||
}
|
||||
Ok(Some(table))
|
||||
}
|
||||
// Web: read the picked file's bytes now — the name-only path has nothing
|
||||
// behind it — parse them, and remember them in the session registry so
|
||||
// `load_named` resolves the name afterwards. There is no plot-styles
|
||||
// folder on the web, so registration lasts for the session.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let name = handle.file_name();
|
||||
let bytes: std::sync::Arc<Vec<u8>> = std::sync::Arc::new(handle.read().await);
|
||||
let table = plot_style::PlotStyleTable::from_bytes(&name, &bytes)?;
|
||||
plot_style::register_session_table(&name, bytes);
|
||||
Ok(Some(table))
|
||||
}
|
||||
Ok(Some(table))
|
||||
}
|
||||
|
||||
// ── Image file picker ─────────────────────────────────────────────────────
|
||||
|
||||
/// Show a file-open dialog for raster images and decode the selected file.
|
||||
/// Returns `(path, pixel_width, pixel_height)` or an error string.
|
||||
///
|
||||
/// The bytes are read through the dialog handle and registered against the
|
||||
/// returned path (name-only on the web — there is no filesystem behind it),
|
||||
/// mirroring the PDF-underlay pick. Decoding then comes from those bytes, and
|
||||
/// the entity the IMAGE command commits resolves through the same registry
|
||||
/// instead of a `image::open` on a path that only exists natively.
|
||||
pub async fn pick_image_file() -> Result<(PathBuf, u32, u32), String> {
|
||||
let handle = crate::sys::file_dialog()
|
||||
.set_title(crate::t!("Select Image File").as_ref())
|
||||
@@ -1803,9 +1905,16 @@ pub async fn pick_image_file() -> Result<(PathBuf, u32, u32), String> {
|
||||
.await
|
||||
.ok_or_else(|| "Cancelled".to_string())?;
|
||||
let path = crate::sys::handle_path(&handle);
|
||||
let img = image::open(&path).map_err(|e| e.to_string())?;
|
||||
let (w, h) = image::GenericImageView::dimensions(&img);
|
||||
Ok((path, w, h))
|
||||
let bytes = std::sync::Arc::new(handle.read().await);
|
||||
let reference = path.to_string_lossy().into_owned();
|
||||
let short = std::path::Path::new(&reference)
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| reference.clone());
|
||||
crate::scene::model::image_model::register_source(&reference, bytes);
|
||||
let decoded = crate::scene::model::image_model::resolve_image(&reference)
|
||||
.ok_or_else(|| format!("Unsupported or corrupt image \"{short}\""))?;
|
||||
Ok((path, decoded.width, decoded.height))
|
||||
}
|
||||
|
||||
/// Pick an image file and prepare it for embedding (IMAGEEMBED). The bytes
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
use rustc_hash::FxHashMap as HashMap;
|
||||
use std::io::Read;
|
||||
use std::path::{Component, Path};
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::path::PathBuf;
|
||||
@@ -87,6 +88,9 @@ pub fn available_ctb_names() -> Vec<String> {
|
||||
})
|
||||
.collect();
|
||||
names.extend(STANDARD_PLOT_STYLES.iter().map(|name| (*name).to_string()));
|
||||
// Empty on native (nothing registers there); on the web this is
|
||||
// where the tables picked this session surface.
|
||||
names.extend(session_ctb_names());
|
||||
names.sort_by_key(|name| name.to_ascii_lowercase());
|
||||
names.dedup_by(|left, right| left.eq_ignore_ascii_case(right));
|
||||
names
|
||||
@@ -95,10 +99,62 @@ pub fn available_ctb_names() -> Vec<String> {
|
||||
{
|
||||
let mut names = vec![DEFAULT_PLOT_STYLE.into(), MONOCHROME_PLOT_STYLE.into()];
|
||||
names.extend(STANDARD_PLOT_STYLES.iter().map(|name| (*name).to_string()));
|
||||
// No folder to scan — the tables picked this session stand in.
|
||||
names.extend(session_ctb_names());
|
||||
names.sort_by_key(|name| name.to_ascii_lowercase());
|
||||
names.dedup_by(|left, right| left.eq_ignore_ascii_case(right));
|
||||
names
|
||||
}
|
||||
}
|
||||
|
||||
// ── Web session registry ────────────────────────────────────────────────────
|
||||
|
||||
/// Plot tables picked this session, keyed by file name. The browser keeps
|
||||
/// no folder behind a picked name, so the web build remembers the bytes
|
||||
/// instead and `load_named` resolves through here before falling back to
|
||||
/// the built-ins — the plot-style counterpart of `pdf_raster::register_source`.
|
||||
/// Native never writes it, so the lookup is always empty there and the
|
||||
/// plot-styles folder stays authoritative.
|
||||
fn session_tables() -> &'static Mutex<HashMap<String, Arc<Vec<u8>>>> {
|
||||
static TABLES: OnceLock<Mutex<HashMap<String, Arc<Vec<u8>>>>> = OnceLock::new();
|
||||
TABLES.get_or_init(|| Mutex::new(HashMap::default()))
|
||||
}
|
||||
|
||||
/// Remember a picked table's bytes so later `load_named` calls — the Plot
|
||||
/// dialog's table list and APPLY, plot settings, display plot style —
|
||||
/// resolve its name. Web only; on native, permanently registering a picked
|
||||
/// table copies it into the plot-styles folder instead.
|
||||
pub fn register_session_table(name: &str, bytes: Arc<Vec<u8>>) {
|
||||
session_tables()
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
.insert(name.to_string(), bytes);
|
||||
}
|
||||
|
||||
/// The bytes registered under `name` this session, matched
|
||||
/// case-insensitively the way the folder scan matches file names.
|
||||
fn session_table(name: &str) -> Option<Arc<Vec<u8>>> {
|
||||
session_tables()
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
.iter()
|
||||
.find(|(registered, _)| registered.eq_ignore_ascii_case(name))
|
||||
.map(|(_, bytes)| Arc::clone(bytes))
|
||||
}
|
||||
|
||||
/// The colour-dependent (.ctb) names picked this session, for the Plot
|
||||
/// dialog's table list. Named (.stb) tables are left out: the plotter is
|
||||
/// color-dependent, so they cannot be applied anyway.
|
||||
fn session_ctb_names() -> Vec<String> {
|
||||
session_tables()
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
.keys()
|
||||
.filter(|name| name.to_ascii_lowercase().ends_with(".ctb"))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ── Standard lineweight table (index → mm) ───────────────────────────────────
|
||||
|
||||
/// Lineweight table: index value → mm, matching the stored LWEIGHT codes.
|
||||
@@ -289,6 +345,13 @@ impl PlotStyleTable {
|
||||
return Err(format!("Invalid plot style name: {name}"));
|
||||
}
|
||||
|
||||
// A table picked this session resolves through the registry first.
|
||||
// Empty on native, so the folder search below stays authoritative
|
||||
// there; on the web this is the only place a picked table lives.
|
||||
if let Some(bytes) = session_table(name) {
|
||||
return Self::from_bytes(name, &bytes);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let dir = ensure_plot_styles_dir()?;
|
||||
@@ -910,6 +973,80 @@ mod round_trip_tests {
|
||||
PlotStyleTable::from_bytes(table.name.clone(), &bytes).expect("reload")
|
||||
}
|
||||
|
||||
/// Real .stb files use the same container with an `STBVER1` tag;
|
||||
/// they must decompress instead of dying in the deflate fallback.
|
||||
#[test]
|
||||
fn stb_container_headers_parse() {
|
||||
let mut bytes =
|
||||
compress_ctb(PlotStyleTable::identity("x").to_text().as_bytes()).expect("compress");
|
||||
// "PIAFILEVERSION_2.0," is 19 bytes, so 19..22 holds the "CTB" tag.
|
||||
bytes[19..22].copy_from_slice(b"STB");
|
||||
let table = PlotStyleTable::from_bytes("mystyles.stb", &bytes).expect("stb parses");
|
||||
assert!(table.is_stb);
|
||||
}
|
||||
|
||||
/// Named tables are refused with the actual limitation, not a generic
|
||||
/// invalid-name error (and without touching the file system).
|
||||
#[test]
|
||||
fn load_named_rejects_stb_with_reason() {
|
||||
let error = PlotStyleTable::load_named("mystyles.stb").unwrap_err();
|
||||
assert!(error.contains("stb"), "{error}");
|
||||
assert!(error.contains("color-dependent"), "{error}");
|
||||
}
|
||||
|
||||
/// A table registered this session (the web stand-in for the plot-styles
|
||||
/// folder) is what `load_named` returns for its name — matched
|
||||
/// case-insensitively, the way the folder scan matches file names.
|
||||
#[test]
|
||||
fn load_named_resolves_a_session_registered_table() {
|
||||
let mut table = PlotStyleTable::identity("SessionPens.ctb");
|
||||
table.aci_entries[1].lineweight = 13; // 0.50 mm
|
||||
let bytes = compress_ctb(table.to_text().as_bytes()).expect("compress");
|
||||
super::register_session_table("SessionPens.ctb", std::sync::Arc::new(bytes));
|
||||
|
||||
let loaded = PlotStyleTable::load_named("sessionpens.CTB").expect("registry hit");
|
||||
assert_eq!(loaded.name, "sessionpens.CTB");
|
||||
assert_eq!(loaded.resolve_lineweight(1), Some(0.50));
|
||||
|
||||
// A name nothing was registered under still falls back to the built-ins.
|
||||
assert!(PlotStyleTable::load_named(super::MONOCHROME_PLOT_STYLE).is_ok());
|
||||
}
|
||||
|
||||
/// The STB refusal comes before the registry lookup: even a registered
|
||||
/// named table cannot be applied to a color-dependent plot.
|
||||
#[test]
|
||||
fn stb_refusal_precedes_the_session_registry() {
|
||||
let bytes =
|
||||
compress_ctb(PlotStyleTable::identity("x").to_text().as_bytes()).expect("compress");
|
||||
super::register_session_table("Refused.stb", std::sync::Arc::new(bytes));
|
||||
let error = PlotStyleTable::load_named("Refused.stb").unwrap_err();
|
||||
assert!(error.contains("color-dependent"), "{error}");
|
||||
}
|
||||
|
||||
/// The Plot dialog's table list includes the session-registered colour
|
||||
/// tables, but never a named (.stb) one.
|
||||
#[test]
|
||||
fn available_names_include_session_registered_tables() {
|
||||
let bytes =
|
||||
compress_ctb(PlotStyleTable::identity("x").to_text().as_bytes()).expect("compress");
|
||||
let bytes = std::sync::Arc::new(bytes);
|
||||
super::register_session_table("DialogPens.ctb", std::sync::Arc::clone(&bytes));
|
||||
super::register_session_table("Named.stb", bytes);
|
||||
let names = super::available_ctb_names();
|
||||
assert!(
|
||||
names
|
||||
.iter()
|
||||
.any(|name| name.eq_ignore_ascii_case("DialogPens.ctb")),
|
||||
"{names:?}"
|
||||
);
|
||||
assert!(
|
||||
!names
|
||||
.iter()
|
||||
.any(|name| name.eq_ignore_ascii_case("Named.stb")),
|
||||
"{names:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Saving a table keeps its global scale factor, which scales every
|
||||
/// lineweight while apply_factor is on. It used to be cut to one decimal.
|
||||
#[test]
|
||||
|
||||
+72
-9
@@ -25,6 +25,9 @@ pub struct QueuedOpen {
|
||||
}
|
||||
|
||||
static DROPPED: Mutex<VecDeque<QueuedOpen>> = Mutex::new(VecDeque::new());
|
||||
// Dropped raster images never open as drawings: they are registered by name
|
||||
// so RasterImage entities the open drawing references can resolve them.
|
||||
static DROPPED_IMAGES: Mutex<VecDeque<(String, Vec<u8>)>> = Mutex::new(VecDeque::new());
|
||||
// `OnceLock::take` needs `&mut self`, which an immutable static cannot
|
||||
// provide — an AtomicBool swap gives the same set-once marker and can still
|
||||
// be reverted when no document exists.
|
||||
@@ -69,6 +72,32 @@ pub fn enqueue_open(name: String, bytes: Vec<u8>) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
/// Queue a dropped raster image for the poll to register as decodable bytes
|
||||
/// (name-keyed) — the web analogue of the drawing folder a drawing's images
|
||||
/// live beside natively. Returns `false` when the queue is full.
|
||||
pub fn enqueue_image(name: String, bytes: Vec<u8>) -> bool {
|
||||
let mut queue = DROPPED_IMAGES
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
if queue.len() >= QUEUE_CAP {
|
||||
drop(queue);
|
||||
warn(&format!(
|
||||
"ocs: dropped-image queue full; ignoring \"{name}\""
|
||||
));
|
||||
return false;
|
||||
}
|
||||
queue.push_back((name, bytes));
|
||||
true
|
||||
}
|
||||
|
||||
/// Pop the oldest dropped image the poll has not registered yet.
|
||||
pub fn take_dropped_image() -> Option<(String, Vec<u8>)> {
|
||||
DROPPED_IMAGES
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
.pop_front()
|
||||
}
|
||||
|
||||
/// Queue an autosave-restore file tagged with its snapshot key. Returns
|
||||
/// `false` when the queue is full and the bytes were dropped (the OPFS
|
||||
/// snapshot survives; the caller must not wait for an open that will
|
||||
@@ -162,22 +191,26 @@ pub fn install() {
|
||||
// reads that begin after the event returns, and parallel reads
|
||||
// would enqueue (and open) in completion order, not drop order.
|
||||
let mut reads: Vec<(String, js_sys::Promise)> = Vec::new();
|
||||
let mut image_reads: Vec<(String, js_sys::Promise)> = Vec::new();
|
||||
for index in 0..files.length() {
|
||||
let Some(file) = files.get(index) else {
|
||||
continue;
|
||||
};
|
||||
let name = file.name();
|
||||
// A drop has no picker filter: accept the formats the web Open
|
||||
// dialog accepts; anything else is logged, never parsed.
|
||||
if !is_drawing_name(&name) {
|
||||
// dialog accepts, plus the raster formats the IMAGE command
|
||||
// accepts; anything else is logged, never parsed.
|
||||
if is_drawing_name(&name) {
|
||||
reads.push((name, file.array_buffer()));
|
||||
} else if is_image_name(&name) {
|
||||
image_reads.push((name, file.array_buffer()));
|
||||
} else {
|
||||
warn(&format!(
|
||||
"ocs: ignored dropped file \"{name}\" (not .dwg/.dxf)"
|
||||
"ocs: ignored dropped file \"{name}\" (not .dwg/.dxf/image)"
|
||||
));
|
||||
continue;
|
||||
}
|
||||
reads.push((name, file.array_buffer()));
|
||||
}
|
||||
if reads.is_empty() {
|
||||
if reads.is_empty() && image_reads.is_empty() {
|
||||
return;
|
||||
}
|
||||
// The page is the only intended drop target; hide drawing drops
|
||||
@@ -189,8 +222,7 @@ pub fn install() {
|
||||
Ok(buffer) => {
|
||||
// A full queue warns on the console; the drop
|
||||
// handler has no command line to talk to.
|
||||
let _ =
|
||||
enqueue_open(name, js_sys::Uint8Array::new(&buffer).to_vec());
|
||||
let _ = enqueue_open(name, js_sys::Uint8Array::new(&buffer).to_vec());
|
||||
}
|
||||
Err(error) => {
|
||||
web_sys::console::error_1(&wasm_bindgen::JsValue::from_str(&format!(
|
||||
@@ -200,6 +232,19 @@ pub fn install() {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (name, read) in image_reads {
|
||||
match JsFuture::from(read).await {
|
||||
Ok(buffer) => {
|
||||
let _ = enqueue_image(name, js_sys::Uint8Array::new(&buffer).to_vec());
|
||||
}
|
||||
Err(error) => {
|
||||
web_sys::console::error_1(&wasm_bindgen::JsValue::from_str(&format!(
|
||||
"ocs: failed to read dropped image \"{name}\": {}",
|
||||
error.as_string().unwrap_or_else(|| format!("{error:?}"))
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -218,7 +263,25 @@ fn is_drawing_name(name: &str) -> bool {
|
||||
// a dot-less name is its own extension, so a file literally named
|
||||
// "DWG"/"dxf" parses and must not be filtered out here.
|
||||
matches!(
|
||||
name.rsplit('.').next().unwrap_or_default().to_ascii_lowercase().as_str(),
|
||||
name.rsplit('.')
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase()
|
||||
.as_str(),
|
||||
"dwg" | "dxf"
|
||||
)
|
||||
}
|
||||
|
||||
/// The decodable raster set matches the IMAGE picker's filter (and the `image`
|
||||
/// crate features the app builds with) — anything else would queue bytes that
|
||||
/// can only ever fail to decode.
|
||||
fn is_image_name(name: &str) -> bool {
|
||||
matches!(
|
||||
name.rsplit('.')
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase()
|
||||
.as_str(),
|
||||
"png" | "jpg" | "jpeg" | "bmp" | "tiff" | "tif"
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user