forked from 1b-agent/opencadstudio-upstream
feat: web automation wrapper (#1349), DATALINK table on web, System theme; honest decode accounting
Ported from2c38de53(#1717). Main already carries the hatch PDF draw order fix (#1345), the numeric set_properties compare (#1348) and the web open from bytes, so those parts stay out; what remains: #1349 — window.ocsControl.ready/request promise wrapper with an ocs-ready event (web/ocs-control.js, loaded by the web page), and the startup donation prompt is suppressed once a control request is seen (opt-in pre-boot via localStorage["opencadstudio.automation"] = "1"). docs/automation/web.md documents both. The control protocol stays at 1. DATALINK CSV-to-table works on web (picker + bytes through the session registry — was std::fs, silently dead); COLORSCHEME accepts System (prefers-color-scheme on web, AppsUseLightTheme on Windows). Decode accounting, made honest per the fix-of-fix review: into_rgba8 allocates the RGBA destination while the native buffer is alive for every non-Rgba8 layout (L8 5, La8 6, Rgb8 7, L16 6, La16 8 B/px peaks — the formula now says so), GIF bills the screen+frame coexistence at 8 B/px, and TIFF drops the 3x multipliers both ways — tiff 0.11 splits the given budget between frame and scratch (a total ceiling), so the old double-billing refused honest 16-bit GeoTIFFs ~2.25x early while admitting hostile chunk transients at 3x. (cherry picked from commit2c38de53)
This commit is contained in:
1 parent
f015c3b5fa
commit
db8db14006
37 files changed
+436
-52
No files matched your search
@@ -1298,6 +1298,34 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// Web DATALINK: the picked .csv's bytes arrived. They are registered in
|
||||
/// the picked-source registry — so `DATALINKUPDATE` re-reads them
|
||||
/// in-session instead of failing on the absent filesystem — and fed to
|
||||
/// the same table-insert path the native typed-path command uses.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(super) fn on_datalink_csv_picked(
|
||||
&mut self,
|
||||
picked: Option<(String, Vec<u8>)>,
|
||||
) -> Task<Message> {
|
||||
let Some((name, bytes)) = picked else {
|
||||
return Task::none();
|
||||
};
|
||||
let text = String::from_utf8_lossy(&bytes).into_owned();
|
||||
if !register_picked_source(&name, Arc::new(bytes)) {
|
||||
if let Some(name) = take_last_collision().take() {
|
||||
self.command_line.push_warning(
|
||||
crate::tf!(
|
||||
"\"{name}\" is already loaded with different contents; the existing file was kept."
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
self.place_data_link_table(&text, &name);
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub(super) fn on_data_link_browse(&mut self) -> Task<Message> {
|
||||
Task::perform(
|
||||
async {
|
||||
|
||||
+59
-40
@@ -1799,51 +1799,25 @@ impl OpenCADStudio {
|
||||
.push_info(crate::t!("Usage: DATALINK <path-to-.csv>").as_ref());
|
||||
return Some(Task::none());
|
||||
}
|
||||
// Web: nothing is behind a typed path — pick the .csv through
|
||||
// the browser, read its bytes at pick time, and register them
|
||||
// in the picked-source registry; the same table-insert path
|
||||
// then runs from `DataLinkCsvPicked`.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let _ = path;
|
||||
return Some(Task::perform(
|
||||
crate::io::pick_datalink_csv_web(),
|
||||
Message::DataLinkCsvPicked,
|
||||
));
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(text) => {
|
||||
let rows_data = parse_csv_table(&text);
|
||||
let nrows = rows_data.len();
|
||||
let ncols = rows_data.iter().map(|r| r.len()).max().unwrap_or(0);
|
||||
if nrows == 0 || ncols == 0 {
|
||||
self.command_line
|
||||
.push_error(crate::t!("DATALINK: the CSV file is empty.").as_ref());
|
||||
return Some(Task::none());
|
||||
}
|
||||
use codec::entities::TableBuilder;
|
||||
use codec::types::Vector3;
|
||||
let mut table = TableBuilder::new(nrows, ncols)
|
||||
.at(Vector3::new(0.0, 0.0, 0.0))
|
||||
.row_height(0.5)
|
||||
.column_width(2.0)
|
||||
.build();
|
||||
for (r, row) in rows_data.iter().enumerate() {
|
||||
for (c, cell) in row.iter().enumerate() {
|
||||
table.set_cell_text(r, c, cell);
|
||||
}
|
||||
}
|
||||
let doc = &self.tabs[i].scene.document;
|
||||
let current_style = doc.header.current_table_style_name.clone();
|
||||
table.table_style_handle =
|
||||
doc.objects
|
||||
.iter()
|
||||
.find_map(|(handle, object)| match object {
|
||||
codec::objects::ObjectType::TableStyle(style)
|
||||
if style.name.eq_ignore_ascii_case(¤t_style) =>
|
||||
{
|
||||
Some(*handle)
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
let link_path = std::fs::canonicalize(path)
|
||||
.map(|value| value.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|_| path.to_string());
|
||||
let command =
|
||||
crate::modules::annotate::data_link::DataLinkPlaceCommand::new(
|
||||
table, &link_path,
|
||||
);
|
||||
self.command_line
|
||||
.push_info(&crate::command::CadCommand::prompt(&command));
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
self.place_data_link_table(&text, &link_path);
|
||||
}
|
||||
Err(e) => {
|
||||
self.command_line.push_error(
|
||||
@@ -2076,6 +2050,51 @@ impl OpenCADStudio {
|
||||
}
|
||||
Some(self.finish_dispatch(cmd))
|
||||
}
|
||||
|
||||
/// Build the linked table from CSV text and start the placement command —
|
||||
/// the shared tail of the native typed-path `DATALINK` read and the web
|
||||
/// picker's `DataLinkCsvPicked` (which registers the picked bytes in the
|
||||
/// picked-source registry first so the link re-reads in-session).
|
||||
pub(in crate::app) fn place_data_link_table(&mut self, csv_text: &str, link_path: &str) {
|
||||
let i = self.active_tab;
|
||||
let rows_data = parse_csv_table(csv_text);
|
||||
let nrows = rows_data.len();
|
||||
let ncols = rows_data.iter().map(|r| r.len()).max().unwrap_or(0);
|
||||
if nrows == 0 || ncols == 0 {
|
||||
self.command_line
|
||||
.push_error(crate::t!("DATALINK: the CSV file is empty.").as_ref());
|
||||
return;
|
||||
}
|
||||
use codec::entities::TableBuilder;
|
||||
use codec::types::Vector3;
|
||||
let mut table = TableBuilder::new(nrows, ncols)
|
||||
.at(Vector3::new(0.0, 0.0, 0.0))
|
||||
.row_height(0.5)
|
||||
.column_width(2.0)
|
||||
.build();
|
||||
for (r, row) in rows_data.iter().enumerate() {
|
||||
for (c, cell) in row.iter().enumerate() {
|
||||
table.set_cell_text(r, c, cell);
|
||||
}
|
||||
}
|
||||
let doc = &self.tabs[i].scene.document;
|
||||
let current_style = doc.header.current_table_style_name.clone();
|
||||
table.table_style_handle = doc.objects.iter().find_map(|(handle, object)| {
|
||||
match object {
|
||||
codec::objects::ObjectType::TableStyle(style)
|
||||
if style.name.eq_ignore_ascii_case(¤t_style) =>
|
||||
{
|
||||
Some(*handle)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
let command =
|
||||
crate::modules::annotate::data_link::DataLinkPlaceCommand::new(table, link_path);
|
||||
self.command_line
|
||||
.push_info(&crate::command::CadCommand::prompt(&command));
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
}
|
||||
|
||||
// Scan LandXML text for <CgPoint> survey points. Each element's text content is
|
||||
|
||||
@@ -535,7 +535,8 @@ impl OpenCADStudio {
|
||||
"Available themes: {}",
|
||||
"DARK LIGHT DRACULA NORD SOLARIZED_LIGHT SOLARIZED_DARK \
|
||||
GRUVBOX_LIGHT GRUVBOX_DARK TOKYONIGHT TOKYONIGHTSTORM TOKYONIGHTLIGHT \
|
||||
KANAGAWAWAVE KANAGAWADRAGON KANAGAWALOTUS MOONFLY NIGHTFLY OXOCARBON FERRA",
|
||||
KANAGAWAWAVE KANAGAWADRAGON KANAGAWALOTUS MOONFLY NIGHTFLY OXOCARBON FERRA \
|
||||
SYSTEM (follows the OS or browser color scheme)",
|
||||
));
|
||||
self.command_line.push_info(&crate::tf!(
|
||||
"Current color scheme: <{}>",
|
||||
@@ -559,7 +560,8 @@ impl OpenCADStudio {
|
||||
self.command_line.push_output(&crate::tf!("Available themes: {}",
|
||||
"DARK LIGHT DRACULA NORD SOLARIZED_LIGHT SOLARIZED_DARK \
|
||||
GRUVBOX_LIGHT GRUVBOX_DARK TOKYONIGHT TOKYONIGHTSTORM TOKYONIGHTLIGHT \
|
||||
KANAGAWAWAVE KANAGAWADRAGON KANAGAWALOTUS MOONFLY NIGHTFLY OXOCARBON FERRA",
|
||||
KANAGAWAWAVE KANAGAWADRAGON KANAGAWALOTUS MOONFLY NIGHTFLY OXOCARBON FERRA \
|
||||
SYSTEM (follows the OS or browser color scheme)",
|
||||
));
|
||||
return Some(Task::none());
|
||||
}
|
||||
@@ -569,6 +571,29 @@ impl OpenCADStudio {
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
// SYSTEM keeps its own name in the saved config and resolves
|
||||
// to a concrete Light/Dark theme at run time — the browser's
|
||||
// prefers-color-scheme on the web, the Windows personalization
|
||||
// setting natively. Re-running the command re-resolves; there
|
||||
// is no live preference listener.
|
||||
if upper_sub == "SYSTEM" {
|
||||
if self.ui_theme.name == "Custom" {
|
||||
self.saved_custom_palette = Some(self.ui_theme.palette);
|
||||
}
|
||||
let resolved = crate::app::config::resolve_system_theme();
|
||||
let name = format!("{resolved:?}");
|
||||
self.ui_theme.name = "System".to_string();
|
||||
self.ui_theme.palette =
|
||||
crate::app::config::UiThemePalette::from_iced(resolved.seed());
|
||||
self.theme_color_inputs = self.ui_theme.palette.hex_values();
|
||||
self.active_theme = resolved;
|
||||
self.sync_model_space_theme(true);
|
||||
self.persist_settings_if_changed();
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"Color scheme set to 'System' (currently {name})."
|
||||
).as_ref());
|
||||
return Some(Task::none());
|
||||
}
|
||||
let theme = crate::app::config::parse_theme_name(sub);
|
||||
if let Some(t) = theme {
|
||||
if self.ui_theme.name == "Custom" {
|
||||
|
||||
@@ -109,12 +109,77 @@ impl UiThemeConfig {
|
||||
pub fn to_iced(&self) -> iced::Theme {
|
||||
if self.name == "Custom" {
|
||||
iced::Theme::custom("Custom", self.palette.to_iced())
|
||||
} else if self.name.eq_ignore_ascii_case("System") {
|
||||
resolve_system_theme()
|
||||
} else {
|
||||
builtin_theme(&self.name).unwrap_or(iced::Theme::Oxocarbon)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the `System` color scheme (COLORSCHEME SYSTEM) to a concrete
|
||||
/// theme: the browser `prefers-color-scheme` media query on the web, the
|
||||
/// Windows `AppsUseLightTheme` personalization value natively, and the dark
|
||||
/// default on the remaining native platforms (no cheap OS query there).
|
||||
/// Read at boot (`apply_config`) and re-read whenever COLORSCHEME SYSTEM
|
||||
/// runs again — there is no live preference listener by design.
|
||||
pub fn resolve_system_theme() -> iced::Theme {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let light = web_sys::window().is_some_and(|window| {
|
||||
window
|
||||
.match_media("(prefers-color-scheme: light)")
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_some_and(|query| query.matches())
|
||||
});
|
||||
if light {
|
||||
iced::Theme::Light
|
||||
} else {
|
||||
iced::Theme::Dark
|
||||
}
|
||||
}
|
||||
#[cfg(all(not(target_arch = "wasm32"), target_os = "windows"))]
|
||||
{
|
||||
use windows_sys::Win32::System::Registry::{
|
||||
RegGetValueW, HKEY_CURRENT_USER, RRF_RT_REG_DWORD,
|
||||
};
|
||||
let sub_key: Vec<u16> = "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"
|
||||
.encode_utf16()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
let value: Vec<u16> = "AppsUseLightTheme"
|
||||
.encode_utf16()
|
||||
.chain(std::iter::once(0))
|
||||
.collect();
|
||||
let mut data: u32 = 0;
|
||||
let mut size = std::mem::size_of::<u32>() as u32;
|
||||
// ERROR_SUCCESS (0) with data 1 means the apps theme is Light; a
|
||||
// missing value (older Windows, wiped personalization) is Dark.
|
||||
let light = unsafe {
|
||||
RegGetValueW(
|
||||
HKEY_CURRENT_USER,
|
||||
sub_key.as_ptr(),
|
||||
value.as_ptr(),
|
||||
RRF_RT_REG_DWORD,
|
||||
std::ptr::null_mut(),
|
||||
&mut data as *mut u32 as *mut core::ffi::c_void,
|
||||
&mut size,
|
||||
) == 0
|
||||
&& data == 1
|
||||
};
|
||||
if light {
|
||||
iced::Theme::Light
|
||||
} else {
|
||||
iced::Theme::Dark
|
||||
}
|
||||
}
|
||||
#[cfg(not(any(target_arch = "wasm32", target_os = "windows")))]
|
||||
{
|
||||
iced::Theme::Dark
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct UiThemePalette {
|
||||
@@ -538,6 +603,25 @@ fn config_path() -> Option<std::path::PathBuf> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn system_scheme_resolves_to_a_concrete_theme_and_keeps_its_name() {
|
||||
// Fresh configs keep the existing default; SYSTEM is opt-in.
|
||||
assert_ne!(UiThemeConfig::default().name, "System");
|
||||
let mut cfg = UiThemeConfig::default();
|
||||
cfg.name = "System".to_string();
|
||||
let resolved = cfg.to_iced();
|
||||
// Case-insensitive like the other stored names.
|
||||
cfg.name = "system".to_string();
|
||||
assert_eq!(cfg.to_iced(), resolved);
|
||||
assert!(
|
||||
matches!(resolved, iced::Theme::Light | iced::Theme::Dark),
|
||||
"SYSTEM must resolve through the platform preference"
|
||||
);
|
||||
// The stored name survives resolution; re-runs re-resolve.
|
||||
assert_eq!(cfg.name, "system");
|
||||
assert_eq!(resolve_system_theme(), resolved);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_model_space_defaults() {
|
||||
let cfg = ModelSpaceThemeConfig::default();
|
||||
|
||||
@@ -106,6 +106,10 @@ pub(super) struct State {
|
||||
completed: VecDeque<(String, Value, Value)>,
|
||||
owner: Option<(u64, String)>,
|
||||
pub enabled: bool,
|
||||
/// Set by the first control request of the session (or by a web host's
|
||||
/// boot-time opt-in): while control is enabled, startup prompts that
|
||||
/// would block automation — the donation prompt — are suppressed (#1349).
|
||||
pub automation_seen: bool,
|
||||
serial: u64,
|
||||
events: VecDeque<Value>,
|
||||
/// Monotonic counter bumped on every real selection change, so clients
|
||||
@@ -489,6 +493,18 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
pub(super) fn control_request(&mut self, req: Value) -> (Value, Task<Message>) {
|
||||
// First control contact marks the session automated (#1349): a
|
||||
// DonationPrompt that is still queued (not yet shown) is dropped so
|
||||
// automation never waits behind it, and `queue_startup_prompts`
|
||||
// stops queueing one. A prompt already on screen stays — a human
|
||||
// may be reading it; clients close it with `close_modal`.
|
||||
if !self.control.automation_seen && self.control.enabled {
|
||||
self.control.automation_seen = true;
|
||||
if self.active_modal != Some(super::ModalKind::DonationPrompt) {
|
||||
self.pending_startup_modals
|
||||
.retain(|kind| kind != &super::ModalKind::DonationPrompt);
|
||||
}
|
||||
}
|
||||
let op = req["op"].as_str().unwrap_or("");
|
||||
let query = matches!(
|
||||
op,
|
||||
|
||||
@@ -2754,6 +2754,11 @@ pub enum Message {
|
||||
/// `None` = cancelled.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
CenterlineLinetypeFilePicked(Option<(String, String)>),
|
||||
/// Web DATALINK: the picked .csv file's name and raw bytes — registered
|
||||
/// in the picked-source registry, then fed to the same table-insert path
|
||||
/// the native typed-path command uses. `None` = cancelled.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
DataLinkCsvPicked(Option<(String, Vec<u8>)>),
|
||||
/// Append one typed character to the command-line input from the
|
||||
/// global key-press subscription. Used when the text-input widget
|
||||
/// itself isn't focused (focus parked on viewport / button / etc.)
|
||||
@@ -5079,6 +5084,16 @@ impl OpenCADStudio {
|
||||
fn boot_web() -> (Self, Task<Message>) {
|
||||
#[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
|
||||
let mut s = Self::new();
|
||||
// A web host that intends to automate can opt in before the first
|
||||
// control request by setting `opencadstudio.automation` to "1" in
|
||||
// localStorage (the same pre-load mechanism the settings use): the
|
||||
// startup donation prompt then never queues (#1349).
|
||||
if web_sys::window().and_then(|window| window.local_storage().ok().flatten()).and_then(
|
||||
|storage| storage.get_item("opencadstudio.automation").ok().flatten(),
|
||||
) == Some("1".to_string())
|
||||
{
|
||||
s.control.automation_seen = true;
|
||||
}
|
||||
s.queue_startup_prompts();
|
||||
let focus = s.focus_cmd_input();
|
||||
let primary_font =
|
||||
|
||||
+40
-1
@@ -8,7 +8,13 @@ impl OpenCADStudio {
|
||||
self.pending_startup_modals
|
||||
.push_back(ModalKind::AssocPrompt);
|
||||
}
|
||||
if self.donation_prompt_version != env!("OCS_APP_VERSION") {
|
||||
// Automation sessions (#1349): once control traffic has arrived (or a
|
||||
// web host opted in at boot) while control is enabled, the donation
|
||||
// prompt is suppressed — a modal would block every automated
|
||||
// operation until a client closed it by hand.
|
||||
if self.donation_prompt_version != env!("OCS_APP_VERSION")
|
||||
&& !(self.control.enabled && self.control.automation_seen)
|
||||
{
|
||||
self.pending_startup_modals
|
||||
.push_back(ModalKind::DonationPrompt);
|
||||
}
|
||||
@@ -226,6 +232,39 @@ mod tests {
|
||||
assert!(restarted.active_modal.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automation_sessions_skip_the_donation_prompt() {
|
||||
// First control contact drops a still-queued donation prompt and
|
||||
// stops later queueing while control stays enabled (#1349).
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.default_assoc_prompted = true;
|
||||
app.queue_startup_prompts();
|
||||
assert_eq!(app.active_modal, Some(ModalKind::DonationPrompt));
|
||||
let _ = app.update(Message::CloseModal);
|
||||
app.donation_prompt_version = "previous-release".to_string();
|
||||
app.pending_startup_modals.push_back(ModalKind::DonationPrompt);
|
||||
let _ = app.control_request(serde_json::json!({"op":"state"}));
|
||||
assert!(app.pending_startup_modals.is_empty());
|
||||
app.queue_startup_prompts();
|
||||
assert_ne!(app.active_modal, Some(ModalKind::DonationPrompt));
|
||||
|
||||
// A still-queued (not yet shown) prompt is dropped on first contact.
|
||||
let mut queued = OpenCADStudio::new_for_test();
|
||||
queued.default_assoc_prompted = true;
|
||||
queued.donation_prompt_version = "previous-release".to_string();
|
||||
queued.pending_startup_modals.push_back(ModalKind::DonationPrompt);
|
||||
let _ = queued.control_request(serde_json::json!({"op":"state"}));
|
||||
queued.queue_startup_prompts();
|
||||
assert_ne!(queued.active_modal, Some(ModalKind::DonationPrompt));
|
||||
|
||||
// A prompt already on screen is left for its reader; clients close
|
||||
// it themselves, and a session with no control traffic still prompts.
|
||||
let mut human = OpenCADStudio::new_for_test();
|
||||
human.default_assoc_prompted = true;
|
||||
human.queue_startup_prompts();
|
||||
assert_eq!(human.active_modal, Some(ModalKind::DonationPrompt));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_dialogs_wait_their_turn_and_preserve_unseen_prompts() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
|
||||
@@ -1348,6 +1348,14 @@ impl OpenCADStudio {
|
||||
pub(in crate::app) fn apply_config(&mut self, cfg: crate::app::config::AppConfig) {
|
||||
self.apply_settings(&cfg.settings);
|
||||
self.ui_theme = cfg.theme.clone();
|
||||
// A saved `System` scheme re-resolves at boot: the stored palette is
|
||||
// whatever the last resolution produced, so refresh it alongside the
|
||||
// live theme (COLORSCHEME SYSTEM documents this boot-time read).
|
||||
if self.ui_theme.name.eq_ignore_ascii_case("System") {
|
||||
self.ui_theme.palette = crate::app::config::UiThemePalette::from_iced(
|
||||
crate::app::config::resolve_system_theme().seed(),
|
||||
);
|
||||
}
|
||||
self.active_theme = self.ui_theme.to_iced();
|
||||
self.theme_color_inputs = self.ui_theme.palette.hex_values();
|
||||
self.model_space = cfg.model_space;
|
||||
|
||||
@@ -2964,6 +2964,12 @@ impl OpenCADStudio {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Message::ScriptFilePicked(None) => Task::none(),
|
||||
|
||||
// Web DATALINK: the picked .csv's bytes arrived — register them
|
||||
// as the link's in-session source, then run the same table-insert
|
||||
// path the native typed-path command feeds.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Message::DataLinkCsvPicked(picked) => self.on_datalink_csv_picked(picked),
|
||||
|
||||
// Web CENTERLTYPEFILE: the picked .lin/.dnl file's text arrived —
|
||||
// load its linetypes, then record the setting under the file's
|
||||
// name (the native command reads the typed path inside
|
||||
|
||||
@@ -273,6 +273,27 @@ pub async fn pick_linetype_file_web() -> Option<(String, String)> {
|
||||
Some((name, String::from_utf8_lossy(&bytes).into_owned()))
|
||||
}
|
||||
|
||||
/// Web DATALINK: pick the .csv through the browser and read its raw bytes —
|
||||
/// the stand-in for the typed path the native command reads with
|
||||
/// `std::fs::read_to_string`. The caller registers the bytes in the
|
||||
/// picked-source registry so the link re-reads in-session. `None` = the user
|
||||
/// cancelled.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn pick_datalink_csv_web() -> Option<(String, Vec<u8>)> {
|
||||
let Some(handle) = crate::sys::file_dialog()
|
||||
.set_title(crate::t!("Link a CSV data source").as_ref())
|
||||
.add_filter(crate::t!("CSV Files").as_ref(), &["csv", "txt", "CSV", "TXT"])
|
||||
.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, bytes))
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -1013,6 +1013,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
||||
"Color Temperature" => Some(("properties", "color-temperature")),
|
||||
"Color override (#RRGGBB):" => Some(("styles", "color-override-rrggbb")),
|
||||
"Color scheme set to '{name}'." => Some(("view", "color-scheme-set-to-name")),
|
||||
"Color scheme set to 'System' (currently {name})." => Some(("view", "color-scheme-set-to-system-currently-name")),
|
||||
"Color {}" => Some(("ui", "color")),
|
||||
"Column count" => Some(("properties", "column-count")),
|
||||
"Column gutter" => Some(("properties", "column-gutter")),
|
||||
@@ -4187,6 +4188,8 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
||||
"Data Extraction Settings" => Some(("common", "data-extraction-settings")),
|
||||
"Add Drawings" => Some(("common", "add-drawings")),
|
||||
"CSV" => Some(("common", "csv")),
|
||||
"CSV Files" => Some(("common", "csv-files")),
|
||||
"Link a CSV data source" => Some(("common", "link-a-csv-data-source")),
|
||||
"Pick the file again to load its contents in the browser." => Some(("file", "pick-the-file-again-to-load-its-contents")),
|
||||
"Adding folders is not available in the browser. Add drawing files instead." => Some(("file", "adding-folders-is-not-available-in-the-browser")),
|
||||
"Drawing not loaded in the browser: {name}." => Some(("file", "drawing-not-loaded-in-the-browser")),
|
||||
|
||||
@@ -772,19 +772,27 @@ fn native_footprint(format: Option<image::ImageFormat>, color: Option<image::Col
|
||||
image::ColorType::Rgba32F => 16,
|
||||
_ => 4,
|
||||
});
|
||||
// A 16-bit frame and its RGBA conversion can coexist; an 8-bit one is
|
||||
// converted with at most a small row buffer in flight.
|
||||
let peak_bpp = if native_bpp > 4 { native_bpp + 4 } else { 4 };
|
||||
let codec_factor = match format {
|
||||
Some(image::ImageFormat::Tiff) => 3,
|
||||
_ => 1,
|
||||
// `DynamicImage::into_rgba8` only MOVES for Rgba8; every other layout
|
||||
// allocates the RGBA destination while the native buffer is still
|
||||
// alive, so both coexist (image 0.25 buffer.rs cast_in_color_space).
|
||||
let peak_bpp = if native_bpp == 4 { 4 } else { native_bpp + 4 };
|
||||
// GIF decodes the frame into its own buffer while the screen-sized
|
||||
// output buffer is live — two RGBA-sized allocations coexist.
|
||||
let peak_bpp = match format {
|
||||
Some(image::ImageFormat::Gif) => peak_bpp.max(8),
|
||||
_ => peak_bpp,
|
||||
};
|
||||
// tiff 0.11 splits the given budget between the frame buffer and the
|
||||
// chunk scratch (a TOTAL cap), so no multiplier is needed: the honest
|
||||
// peak is the coexisting native+RGBA pair, already in peak_bpp.
|
||||
let codec_factor = 1;
|
||||
NativeFootprint { peak_bpp, codec_factor }
|
||||
}
|
||||
|
||||
/// Budget decision for probed pixel dimensions. Pure and cfg-independent
|
||||
/// when the budgets are passed explicitly, so tests can probe arbitrary
|
||||
/// budgets; the decode path always passes this target's constants.
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
fn tier_for(
|
||||
width: u32,
|
||||
height: u32,
|
||||
@@ -915,6 +923,10 @@ fn decode_with_budgets<R: std::io::BufRead + std::io::Seek>(
|
||||
// 0.25); the tier already folded that factor in, and the allowance
|
||||
// matches it so an in-tier TIFF never codec-refuses.
|
||||
let mut limits = image::Limits::default();
|
||||
// tiff 0.11 SPLITS this cap between the frame buffer and chunk scratch
|
||||
// (a total ceiling), so the plain budget is the right allowance; the
|
||||
// ×3 multiplier that image's older tiff codec needed would instead
|
||||
// admit hostile chunk transients up to 3× the budget.
|
||||
limits.max_alloc = Some(match reader.format() {
|
||||
Some(image::ImageFormat::Gif) => probed
|
||||
.map(|(width, height, _, _)| {
|
||||
@@ -922,7 +934,6 @@ fn decode_with_budgets<R: std::io::BufRead + std::io::Seek>(
|
||||
})
|
||||
.unwrap_or(max_decode_bytes)
|
||||
.min(max_decode_bytes),
|
||||
Some(image::ImageFormat::Tiff) => max_decode_bytes.saturating_mul(3),
|
||||
_ => max_decode_bytes,
|
||||
});
|
||||
reader.limits(limits);
|
||||
|
||||
Reference in new issue
Block a user