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:
Afonsop authored and Hakan Seven committed 2026-10-12 12:02:04 +03:00
1 parent 69fa7cae4f
commit 9cd898a17a
42 files changed
+1315 -155

No files matched your search

+72 -25
View File
@@ -80,9 +80,9 @@ impl OpenCADStudio {
}
#[cfg(target_arch = "wasm32")]
{
self.command_line.push_info(
crate::t!("SAVEALL: save each tab individually in the web build.").as_ref(),
);
// Web: every dirty saved tab is serialized and downloaded
// in one pass — see `saveall_web` for the flow.
self.saveall_web();
}
return Some(Task::none());
}
@@ -420,30 +420,30 @@ impl OpenCADStudio {
let path = cmd.split_once(' ').map(|(_, r)| r.trim().to_string());
match path {
Some(p) if !p.is_empty() => {
let clean_path = p.trim().trim_matches(|c| c == '"' || c == '\'');
match std::fs::read_to_string(clean_path) {
Ok(text) => {
let cmds: Vec<Task<Message>> = text
.lines()
.map(str::trim)
.filter(|l| !l.starts_with('#') && !l.starts_with(';'))
.map(|l| Task::done(Message::ScriptLine(l.to_string())))
.collect();
self.command_line.push_output(
crate::tf!(
"SCRIPT: running {} command(s) from {clean_path}.",
cmds.len()
)
.as_ref(),
);
return Some(Task::batch(cmds));
}
Err(e) => {
self.command_line.push_error(
crate::tf!("SCRIPT: cannot read {clean_path}: {e}").as_ref(),
);
#[cfg(not(target_arch = "wasm32"))]
{
let clean_path = p.trim().trim_matches(|c| c == '"' || c == '\'');
match std::fs::read_to_string(clean_path) {
Ok(text) => return Some(self.run_script(clean_path, &text)),
Err(e) => {
self.command_line.push_error(
crate::tf!("SCRIPT: cannot read {clean_path}: {e}").as_ref(),
);
}
}
}
// Web: nothing is behind a typed path — pick the
// script through the browser and read its bytes at
// pick time (the `pick_and_load_web` pattern); the
// picked text is fed to the same runner.
#[cfg(target_arch = "wasm32")]
{
let _ = &p;
return Some(Task::perform(
crate::io::pick_script_web(),
Message::ScriptFilePicked,
));
}
}
_ => {
self.command_line
@@ -456,6 +456,53 @@ impl OpenCADStudio {
}
Some(self.finish_dispatch(cmd))
}
/// Announce a script and feed its rows to the same runner the `--script`
/// startup flag uses — shared by the native `SCRIPT <path>` read and the
/// web picker's `ScriptFilePicked`.
pub(in crate::app) fn run_script(&mut self, path: &str, text: &str) -> Task<Message> {
let p = path;
let cmds: Vec<Task<Message>> = script_command_lines(text)
.into_iter()
.map(|line| Task::done(Message::ScriptLine(line)))
.collect();
self.command_line.push_output(
crate::tf!("SCRIPT: running {} command(s) from {p}.", cmds.len()).as_ref(),
);
Task::batch(cmds)
}
}
/// One runnable row of a `.scr` script: trimmed, comment rows (`#`, `;`)
/// dropped, blank rows kept so they submit Enter to the active command.
fn script_command_lines(text: &str) -> Vec<String> {
text.lines()
.map(str::trim)
.filter(|line| !line.starts_with('#') && !line.starts_with(';'))
.map(str::to_string)
.collect()
}
#[cfg(test)]
mod script_tests {
use super::script_command_lines;
/// Comments drop, every other row survives trimmed, and blank or
/// whitespace-only rows stay as empty strings (Enter presses).
#[test]
fn comments_and_blanks_follow_the_scr_rules() {
let lines =
script_command_lines("# comment\n; also a comment\nLINE 0,0 10,0\n\n \n TRIM \n");
assert_eq!(
lines,
vec![
"LINE 0,0 10,0".to_string(),
String::new(),
String::new(),
"TRIM".to_string(),
]
);
}
}
/// Parse the argument list of the `BACKGROUND` command into an `[r,g,b,a]`
+12
View File
@@ -1757,6 +1757,18 @@ impl OpenCADStudio {
_ => unreachable!(),
};
if let Some(value) = &value {
// Web CENTERLTYPEFILE: no path to read — pick the
// .lin/.dnl file through the browser, read its
// bytes at pick time and populate the document
// from the text (native reads the typed path
// inside `set_centerline_setting`).
#[cfg(target_arch = "wasm32")]
if name == "CENTERLTYPEFILE" && !value.trim().is_empty() {
return Some(Task::perform(
crate::io::pick_linetype_file_web(),
Message::CenterlineLinetypeFilePicked,
));
}
match self.tabs[i].scene.set_centerline_setting(&name, value) {
Ok(message) => {
self.tabs[i].dirty = true;