forked from 1b-agent/opencadstudio-upstream
web: XREFs, data links & extraction, ARCHIVE transmittals, worker-side saves
The last four big web-vs-desktop gaps.
XREFs: XATTACH reads the picked drawing's bytes and registers them
name-keyed (the image/PDF-underlay pattern), pre-parses on the web
worker, and the commit merge produces the same xref block flow as
native. Opened drawings resolve xrefs from the registry (trailing-name
match) before cache build; unresolved references produce ONE warning
listing the names and the fix — drop the referenced files on the page
(drops and picker opens now register their bytes) and reopen or
XRELOAD. XRELOAD works off the registry on unsaved drawings too.
Data Link & Extraction: spreadsheet links read picked xlsx/csv bytes
(calamine from slices / CSV from string) through a session registry,
so preview, refresh, and table-insert work on web; extraction takes
external drawings via a multi-file picker parsed on the worker, and
outputs CSV/TXT/.dxex via download_bytes. The disabled 'Add folder'
button explains why.
ARCHIVE/ETRANSMIT: assembles the drawing plus registered images, PDF
underlays, and session plot tables into a ZIP delivered as
{stem}_transmit.zip; skipped dependencies are reported by name.
Save/autosave off the UI thread: the parse worker gains a serialize
action (protocol v5) — the document is prepared once on the main
thread (clone, styles, dimension bake), then the DWG/DXF writer pass,
the dominant cost, runs on the worker; any worker failure falls back
to inline serialization with the identical shared implementation.
The save path also clears the dirty flag only when the tab didn't
change mid-write, and the runtime-fields sidecar now carries
graphic_data, fixing a pre-existing loss of raw DWG round-trip bytes
on web-opened documents.
(cherry picked from commit 2e100634)
Ported onto main (#1717 review): the new ocs_save crate builds on the
pinned codec; the hand-written ZIP writer is replaced by the zip crate
already in the tree; ARCHIVE packs registered xref bytes too and
serializes document_for_save() like a web Save; the web save prepares
document_for_save() (prepare_document_for_save now takes the copy by
value, so no second clone); the wasm resolve_path consults the xref
registry, so every reference lookup — not just resolve — finds dropped
files; XRELOAD keeps main's undo, unload and stale-baseline handling;
native data extraction streams the source drawings one at a time.
This commit is contained in:
1 parent
9cd898a17a
commit
61c32c7f74
43 files changed
+2187
-336
No files matched your search
+95
-44
@@ -7,24 +7,10 @@ use wasm_bindgen::{JsCast, JsValue};
|
||||
use web_sys::{ErrorEvent, MessageEvent, Worker, WorkerOptions, WorkerType};
|
||||
|
||||
const HASH_MARKER: &str = "\nreport-source-sha256:";
|
||||
const PROTOCOL_VERSION: u16 = 4;
|
||||
const WORKER_URL: &str = "ocs-parse-worker.js?v=4";
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct EntityRuntimeFields {
|
||||
handle: u64,
|
||||
linetype_handle: Option<u64>,
|
||||
color_book_handle: Option<u64>,
|
||||
face_visual_style_handle: Option<u64>,
|
||||
edge_visual_style_handle: Option<u64>,
|
||||
material_flags: u8,
|
||||
material_handle: Option<u64>,
|
||||
shadow_flags: u8,
|
||||
plotstyle_flags: u8,
|
||||
plotstyle_handle: Option<u64>,
|
||||
entity_mode: Option<u8>,
|
||||
has_ds_data: bool,
|
||||
}
|
||||
/// Version of the shared worker wire format (`ocs_save`); the `?v=` on the
|
||||
/// worker URL cache-busts to match. v5 adds the `serialize` action.
|
||||
const PROTOCOL_VERSION: u16 = ocs_save::PROTOCOL_VERSION;
|
||||
const WORKER_URL: &str = "ocs-parse-worker.js?v=5";
|
||||
|
||||
pub(super) async fn parse_document(
|
||||
name: &str,
|
||||
@@ -58,7 +44,7 @@ pub(super) async fn parse_document(
|
||||
Result<codec::ReadOutcome, (String, Option<codec::ReadStats>)>,
|
||||
Option<String>,
|
||||
bool,
|
||||
Vec<EntityRuntimeFields>,
|
||||
Vec<ocs_save::EntityRuntimeFields>,
|
||||
) = bincode::deserialize(&bytes)
|
||||
.map_err(|error| super::OpenLoadError::from(error.to_string()))?;
|
||||
if payload.0 != PROTOCOL_VERSION {
|
||||
@@ -69,7 +55,10 @@ pub(super) async fn parse_document(
|
||||
}
|
||||
match payload.1 {
|
||||
Ok(mut outcome) => {
|
||||
restore_entity_runtime_fields(&mut outcome.document, payload.4);
|
||||
ocs_save::restore_entity_runtime_fields(
|
||||
&mut outcome.document,
|
||||
payload.4,
|
||||
);
|
||||
Ok((outcome, payload.2))
|
||||
}
|
||||
Err((message, read_stats)) => Err(super::OpenLoadError {
|
||||
@@ -136,30 +125,6 @@ pub(super) async fn parse_document(
|
||||
result
|
||||
}
|
||||
|
||||
fn restore_entity_runtime_fields(
|
||||
document: &mut codec::CadDocument,
|
||||
fields: Vec<EntityRuntimeFields>,
|
||||
) {
|
||||
let handle = |value: Option<u64>| value.map(codec::Handle::new);
|
||||
for fields in fields {
|
||||
let Some(entity) = document.get_entity_mut(codec::Handle::new(fields.handle)) else {
|
||||
continue;
|
||||
};
|
||||
let common = entity.common_mut();
|
||||
common.linetype_handle = handle(fields.linetype_handle);
|
||||
common.color_book_handle = handle(fields.color_book_handle);
|
||||
common.face_visual_style_handle = handle(fields.face_visual_style_handle);
|
||||
common.edge_visual_style_handle = handle(fields.edge_visual_style_handle);
|
||||
common.material_flags = fields.material_flags;
|
||||
common.material_handle = handle(fields.material_handle);
|
||||
common.shadow_flags = fields.shadow_flags;
|
||||
common.plotstyle_flags = fields.plotstyle_flags;
|
||||
common.plotstyle_handle = handle(fields.plotstyle_handle);
|
||||
common.entity_mode = fields.entity_mode;
|
||||
common.has_ds_data = fields.has_ds_data;
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn sha256_document(bytes: &[u8]) -> Result<String, super::OpenLoadError> {
|
||||
let options = WorkerOptions::new();
|
||||
options.set_type(WorkerType::Module);
|
||||
@@ -226,6 +191,92 @@ fn decode_worker_error(message: String) -> super::OpenLoadError {
|
||||
super::OpenLoadError::new(message.to_string(), source_sha256)
|
||||
}
|
||||
|
||||
/// Run the DWG/DXF writer pass for SAVE/AUTOSAVE on the browser worker.
|
||||
/// `encoded_request` is the shared `ocs_save` payload (prepared document +
|
||||
/// runtime-field sidecar + target extension); the worker replies with the
|
||||
/// encoded drawing bytes. Any worker failure — cannot spawn, script error,
|
||||
/// stale protocol — falls back to decoding the document here and serializing
|
||||
/// inline on this thread, so a save never hard-fails because the worker is
|
||||
/// unavailable (strict CSP, blocked worker script, out-of-memory spawn...).
|
||||
pub(super) async fn serialize_document(encoded_request: Vec<u8>) -> Result<Vec<u8>, String> {
|
||||
match serialize_document_via_worker(&encoded_request).await {
|
||||
Ok(bytes) => Ok(bytes),
|
||||
Err(worker_error) => {
|
||||
log::warn!("save worker unavailable, serializing inline: {worker_error}");
|
||||
let (document, ext) = ocs_save::decode_request(&encoded_request)?;
|
||||
ocs_save::serialize_document(&document, &ext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn serialize_document_via_worker(encoded_request: &[u8]) -> Result<Vec<u8>, String> {
|
||||
let options = WorkerOptions::new();
|
||||
options.set_type(WorkerType::Module);
|
||||
let worker =
|
||||
Worker::new_with_options(WORKER_URL, &options).map_err(|error| js_error(error))?;
|
||||
|
||||
let (sender, receiver) =
|
||||
iced::futures::channel::oneshot::channel::<Result<Vec<u8>, String>>();
|
||||
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(|error| js_error(error))
|
||||
.and_then(|value| {
|
||||
let bytes = Uint8Array::new(&value).to_vec();
|
||||
ocs_save::decode_response(&bytes)
|
||||
})
|
||||
.and_then(|result| result)
|
||||
} else {
|
||||
let message = Reflect::get(&data, &JsValue::from_str("error"))
|
||||
.ok()
|
||||
.and_then(|value| value.as_string())
|
||||
.unwrap_or_else(|| "CAD serializer worker failed".to_string());
|
||||
Err(message)
|
||||
};
|
||||
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("action"),
|
||||
&JsValue::from_str("serialize"),
|
||||
)
|
||||
.map_err(|error| js_error(error))?;
|
||||
let input = Uint8Array::from(encoded_request);
|
||||
Reflect::set(&payload, &JsValue::from_str("document"), &input.buffer())
|
||||
.map_err(|error| js_error(error))?;
|
||||
let transfer = Array::new();
|
||||
transfer.push(&input.buffer());
|
||||
worker
|
||||
.post_message_with_transfer(&payload, &transfer)
|
||||
.map_err(|error| js_error(error))?;
|
||||
|
||||
let result = receiver
|
||||
.await
|
||||
.map_err(|_| "CAD serializer worker closed without a result".to_string())?;
|
||||
worker.terminate();
|
||||
result
|
||||
}
|
||||
|
||||
fn js_error(value: JsValue) -> String {
|
||||
value
|
||||
.as_string()
|
||||
|
||||
Reference in new issue
Block a user