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 deterministic ZIP (minimal
writer over flate2 — no new dependency) 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.
This commit is contained in:
1 parent
c643a10ed9
commit
2e1006348b
44 files changed
+2564
-330
No files matched your search
Generated
+11
@@ -42,6 +42,7 @@ dependencies = [
|
|||||||
"objc2-app-kit 0.2.2",
|
"objc2-app-kit 0.2.2",
|
||||||
"objc2-foundation 0.2.2",
|
"objc2-foundation 0.2.2",
|
||||||
"ocs_plugin_api",
|
"ocs_plugin_api",
|
||||||
|
"ocs_save",
|
||||||
"open",
|
"open",
|
||||||
"printpdf",
|
"printpdf",
|
||||||
"rayon",
|
"rayon",
|
||||||
@@ -4174,6 +4175,15 @@ dependencies = [
|
|||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ocs_save"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"acadrust",
|
||||||
|
"bincode",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ocs_web_worker"
|
name = "ocs_web_worker"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -4183,6 +4193,7 @@ dependencies = [
|
|||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
"ocs_save",
|
||||||
"serde",
|
"serde",
|
||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ default-run = "OpenCADStudio"
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"crates/ocs_plugin_api",
|
"crates/ocs_plugin_api",
|
||||||
|
"crates/ocs_save",
|
||||||
"crates/ocs_web_worker",
|
"crates/ocs_web_worker",
|
||||||
"crates/dwg-thumbnailer",
|
"crates/dwg-thumbnailer",
|
||||||
"crates/dwg-thumbnailer-win",
|
"crates/dwg-thumbnailer-win",
|
||||||
@@ -36,6 +37,9 @@ acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "568a12
|
|||||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "dcf0840", features = ["acis", "offset"] }
|
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "dcf0840", features = ["acis", "offset"] }
|
||||||
cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "dcf0840" }
|
cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "dcf0840" }
|
||||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||||
|
# Shared SAVE serialization dispatch + web-worker wire format: one
|
||||||
|
# implementation for the inline (fallback/native) path and the worker path.
|
||||||
|
ocs_save = { path = "crates/ocs_save" }
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||||
hayro = "0.7.1"
|
hayro = "0.7.1"
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "ocs_save"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
# Pure-Rust SAVE pipeline pieces shared by the app crate (native host and the
|
||||||
|
# main wasm instance) and the browser parse/serialize worker (`ocs_web_worker`),
|
||||||
|
# so the DWG/DXF byte serialization and the worker wire format each have
|
||||||
|
# exactly one implementation. No UI/app dependencies — only acadrust + serde.
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "568a12c", features = ["serde"] }
|
||||||
|
bincode = "1.3"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
//! Shared SAVE/serialize pipeline pieces.
|
||||||
|
//!
|
||||||
|
//! Both sides of the browser worker boundary use this crate so the DWG/DXF
|
||||||
|
//! byte serialization and the worker wire format have exactly one
|
||||||
|
//! implementation:
|
||||||
|
//!
|
||||||
|
//! - the app crate's inline `io::save_to_bytes` (native host fallback and the
|
||||||
|
//! web fallback path when the worker is unavailable), and
|
||||||
|
//! - `ocs_web_worker`'s `serialize_document` action, which runs the same
|
||||||
|
//! serialization on the dedicated worker thread.
|
||||||
|
//!
|
||||||
|
//! The document-crossing helpers (bincode encode/decode plus the runtime-field
|
||||||
|
//! sidecar) are also shared with the existing `parse_document` action, which
|
||||||
|
//! returns documents over the same boundary in the opposite direction.
|
||||||
|
|
||||||
|
use acadrust::{CadDocument, DwgWriter, DxfWriter};
|
||||||
|
|
||||||
|
/// Worker protocol version. Bumped whenever a request/response payload shape
|
||||||
|
/// changes; the `?v=` cache-buster on the worker URL must match. v5 adds the
|
||||||
|
/// `serialize` action and extends the runtime-field sidecar with
|
||||||
|
/// `graphic_data`.
|
||||||
|
pub const PROTOCOL_VERSION: u16 = 5;
|
||||||
|
|
||||||
|
/// Serialize a document to an in-memory byte buffer, format chosen by `ext`
|
||||||
|
/// (`dxf` → DXF, anything else → DWG). The document is expected to be fully
|
||||||
|
/// prepared for saving (version stamped, current styles synced, dimension
|
||||||
|
/// blocks baked) — see the app crate's `io::prepare_document_for_save`, which
|
||||||
|
/// owns that app-side half of the pipeline.
|
||||||
|
pub fn serialize_document(doc: &CadDocument, ext: &str) -> Result<Vec<u8>, String> {
|
||||||
|
match ext.to_lowercase().as_str() {
|
||||||
|
"dxf" => DxfWriter::new(doc).write_to_vec().map_err(|e| e.to_string()),
|
||||||
|
_ => {
|
||||||
|
let mut buf = std::io::Cursor::new(Vec::new());
|
||||||
|
DwgWriter::write_to_writer(&mut buf, doc).map_err(|e| e.to_string())?;
|
||||||
|
Ok(buf.into_inner())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Runtime-field sidecar ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Per-entity fields `acadrust` marks `serde(skip)` on `EntityCommon` (raw
|
||||||
|
/// DWG storage/round-trip data the logical model does not own). They do not
|
||||||
|
/// survive a bincode crossing on their own, so every document that crosses
|
||||||
|
/// the worker boundary carries them beside the payload and re-installs them
|
||||||
|
/// after decode — otherwise the saved file would silently lose per-entity
|
||||||
|
/// linetype/material/plotstyle handles and raw graphic bytes.
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct EntityRuntimeFields {
|
||||||
|
pub handle: u64,
|
||||||
|
pub linetype_handle: Option<u64>,
|
||||||
|
pub color_book_handle: Option<u64>,
|
||||||
|
pub face_visual_style_handle: Option<u64>,
|
||||||
|
pub edge_visual_style_handle: Option<u64>,
|
||||||
|
pub material_flags: u8,
|
||||||
|
pub material_handle: Option<u64>,
|
||||||
|
pub shadow_flags: u8,
|
||||||
|
pub plotstyle_flags: u8,
|
||||||
|
pub plotstyle_handle: Option<u64>,
|
||||||
|
pub entity_mode: Option<u8>,
|
||||||
|
pub has_ds_data: bool,
|
||||||
|
pub graphic_data: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collect the serde-skipped runtime fields of every entity in `doc`.
|
||||||
|
pub fn extract_entity_runtime_fields(doc: &CadDocument) -> Vec<EntityRuntimeFields> {
|
||||||
|
doc.entities()
|
||||||
|
.map(|entity| {
|
||||||
|
let common = entity.common();
|
||||||
|
let handle = |value: Option<acadrust::Handle>| value.map(|handle| handle.value());
|
||||||
|
EntityRuntimeFields {
|
||||||
|
handle: common.handle.value(),
|
||||||
|
linetype_handle: handle(common.linetype_handle),
|
||||||
|
color_book_handle: handle(common.color_book_handle),
|
||||||
|
face_visual_style_handle: handle(common.face_visual_style_handle),
|
||||||
|
edge_visual_style_handle: handle(common.edge_visual_style_handle),
|
||||||
|
material_flags: common.material_flags,
|
||||||
|
material_handle: handle(common.material_handle),
|
||||||
|
shadow_flags: common.shadow_flags,
|
||||||
|
plotstyle_flags: common.plotstyle_flags,
|
||||||
|
plotstyle_handle: handle(common.plotstyle_handle),
|
||||||
|
entity_mode: common.entity_mode,
|
||||||
|
has_ds_data: common.has_ds_data,
|
||||||
|
graphic_data: common.graphic_data.clone(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Re-install runtime fields collected by [`extract_entity_runtime_fields`]
|
||||||
|
/// into a document that crossed a serde boundary. Entities whose handle no
|
||||||
|
/// longer resolves are skipped silently (mirrors the parse direction).
|
||||||
|
pub fn restore_entity_runtime_fields(document: &mut CadDocument, fields: Vec<EntityRuntimeFields>) {
|
||||||
|
let handle = |value: Option<u64>| value.map(acadrust::Handle::new);
|
||||||
|
for fields in fields {
|
||||||
|
let Some(entity) = document.get_entity_mut(acadrust::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;
|
||||||
|
common.graphic_data = fields.graphic_data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Worker wire format (`serialize` action) ────────────────────────────────
|
||||||
|
//
|
||||||
|
// Request (main → worker): bincode `(PROTOCOL_VERSION, ext, &CadDocument,
|
||||||
|
// Vec<EntityRuntimeFields>)` transferred as an ArrayBuffer.
|
||||||
|
// Response (worker → main): bincode `(PROTOCOL_VERSION, Result<Vec<u8>,
|
||||||
|
// String>)` — the encoded DWG/DXF bytes on success, the writer's
|
||||||
|
// error text on failure.
|
||||||
|
|
||||||
|
/// Encode a prepared document for the trip main → worker. Costs one bincode
|
||||||
|
/// pass over the document plus the runtime-field sidecar; see the crate docs
|
||||||
|
/// for why the sidecar exists.
|
||||||
|
pub fn encode_request(doc: &CadDocument, ext: &str) -> Result<Vec<u8>, String> {
|
||||||
|
let runtime_fields = extract_entity_runtime_fields(doc);
|
||||||
|
bincode::serialize(&(PROTOCOL_VERSION, ext, doc, runtime_fields))
|
||||||
|
.map_err(|error| format!("encode save document for worker: {error}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a `serialize` request on the worker: returns the document (with
|
||||||
|
/// runtime fields restored) and the target extension.
|
||||||
|
pub fn decode_request(bytes: &[u8]) -> Result<(CadDocument, String), String> {
|
||||||
|
let (version, ext, mut document, runtime_fields): (
|
||||||
|
u16,
|
||||||
|
String,
|
||||||
|
CadDocument,
|
||||||
|
Vec<EntityRuntimeFields>,
|
||||||
|
) = bincode::deserialize(bytes).map_err(|error| format!("decode save document: {error}"))?;
|
||||||
|
if version != PROTOCOL_VERSION {
|
||||||
|
return Err(format!(
|
||||||
|
"serializer worker protocol mismatch: expected {PROTOCOL_VERSION}, received {version}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
restore_entity_runtime_fields(&mut document, runtime_fields);
|
||||||
|
Ok((document, ext))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode the worker's reply: the serialized drawing bytes, or the error text.
|
||||||
|
pub fn encode_response(result: Result<Vec<u8>, String>) -> Result<Vec<u8>, String> {
|
||||||
|
bincode::serialize(&(PROTOCOL_VERSION, result))
|
||||||
|
.map_err(|error| format!("encode save response: {error}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode the worker's reply on the main thread, rejecting stale-protocol
|
||||||
|
/// payloads.
|
||||||
|
pub fn decode_response(bytes: &[u8]) -> Result<Result<Vec<u8>, String>, String> {
|
||||||
|
let (version, result): (u16, Result<Vec<u8>, String>) =
|
||||||
|
bincode::deserialize(bytes).map_err(|error| format!("decode save response: {error}"))?;
|
||||||
|
if version != PROTOCOL_VERSION {
|
||||||
|
return Err(format!(
|
||||||
|
"serializer worker protocol mismatch: expected {PROTOCOL_VERSION}, received {version}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use acadrust::entities::{EntityType, Line, Point};
|
||||||
|
use acadrust::types::Vector3;
|
||||||
|
|
||||||
|
fn sample_document() -> CadDocument {
|
||||||
|
let mut doc = CadDocument::new();
|
||||||
|
let mut pt = Point::new();
|
||||||
|
pt.common.handle = doc.allocate_handle();
|
||||||
|
pt.common.layer = "RUNTIME-FIELDS".to_string();
|
||||||
|
// Fill every serde-skipped field with a recognizable value.
|
||||||
|
pt.common.linetype_handle = Some(acadrust::Handle::new(0xABCD));
|
||||||
|
pt.common.color_book_handle = Some(acadrust::Handle::new(0x101));
|
||||||
|
pt.common.material_flags = 0b11;
|
||||||
|
pt.common.material_handle = Some(acadrust::Handle::new(0x202));
|
||||||
|
pt.common.plotstyle_flags = 0b11;
|
||||||
|
pt.common.plotstyle_handle = Some(acadrust::Handle::new(0x303));
|
||||||
|
pt.common.shadow_flags = 7;
|
||||||
|
pt.common.entity_mode = Some(2);
|
||||||
|
pt.common.has_ds_data = true;
|
||||||
|
pt.common.graphic_data = Some(vec![0xDE, 0xAD, 0xBE, 0xEF]);
|
||||||
|
let _ = doc.add_entity(EntityType::Point(pt));
|
||||||
|
let mut line = Line::new();
|
||||||
|
line.start = Vector3::new(0.0, 0.0, 0.0);
|
||||||
|
line.end = Vector3::new(10.0, 0.0, 0.0);
|
||||||
|
line.common.handle = doc.allocate_handle();
|
||||||
|
let _ = doc.add_entity(EntityType::Line(line));
|
||||||
|
doc
|
||||||
|
}
|
||||||
|
|
||||||
|
// The sidecar is the whole reason documents don't lose raw DWG storage
|
||||||
|
// fields when they cross the worker boundary: serde skips them, so
|
||||||
|
// encode → decode must restore every one.
|
||||||
|
#[test]
|
||||||
|
fn request_round_trip_restores_runtime_fields() {
|
||||||
|
let doc = sample_document();
|
||||||
|
let encoded = encode_request(&doc, "dwg").expect("encode");
|
||||||
|
let (decoded, ext) = decode_request(&encoded).expect("decode");
|
||||||
|
assert_eq!(ext, "dwg");
|
||||||
|
assert_eq!(decoded.entities().count(), 2);
|
||||||
|
let point = decoded
|
||||||
|
.entities()
|
||||||
|
.find(|e| matches!(e, EntityType::Point(_)))
|
||||||
|
.expect("point survived");
|
||||||
|
let common = point.common();
|
||||||
|
assert_eq!(common.layer, "RUNTIME-FIELDS");
|
||||||
|
assert_eq!(common.linetype_handle.map(|h| h.value()), Some(0xABCD));
|
||||||
|
assert_eq!(
|
||||||
|
common.color_book_handle.map(|h| h.value()),
|
||||||
|
Some(0x101)
|
||||||
|
);
|
||||||
|
assert_eq!(common.material_flags, 0b11);
|
||||||
|
assert_eq!(common.material_handle.map(|h| h.value()), Some(0x202));
|
||||||
|
assert_eq!(common.plotstyle_flags, 0b11);
|
||||||
|
assert_eq!(
|
||||||
|
common.plotstyle_handle.map(|h| h.value()),
|
||||||
|
Some(0x303)
|
||||||
|
);
|
||||||
|
assert_eq!(common.shadow_flags, 7);
|
||||||
|
assert_eq!(common.entity_mode, Some(2));
|
||||||
|
assert!(common.has_ds_data);
|
||||||
|
assert_eq!(common.graphic_data.as_deref(), Some(&[0xDE, 0xAD, 0xBE, 0xEF][..]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn response_round_trip_ok_and_err() {
|
||||||
|
let ok = decode_response(&encode_response(Ok(vec![1, 2, 3])).expect("encode"))
|
||||||
|
.expect("decode")
|
||||||
|
.expect("inner ok");
|
||||||
|
assert_eq!(ok, vec![1, 2, 3]);
|
||||||
|
let err = decode_response(&encode_response(Err("writer failed".to_string())).expect("encode"))
|
||||||
|
.expect("decode")
|
||||||
|
.expect_err("inner err");
|
||||||
|
assert_eq!(err, "writer failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn response_rejects_foreign_protocol_version() {
|
||||||
|
let stale =
|
||||||
|
bincode::serialize(&(PROTOCOL_VERSION - 1, Ok::<Vec<u8>, String>(vec![]))).unwrap();
|
||||||
|
assert!(decode_response(&stale).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_rejects_foreign_protocol_version() {
|
||||||
|
let stale = bincode::serialize(&(
|
||||||
|
PROTOCOL_VERSION - 1,
|
||||||
|
"dwg",
|
||||||
|
CadDocument::new(),
|
||||||
|
Vec::<EntityRuntimeFields>::new(),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
assert!(decode_request(&stale).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ext dispatch both save paths (inline fallback and worker) share:
|
||||||
|
// `dxf` must produce an ASCII DXF, anything else the DWG sentinel.
|
||||||
|
#[test]
|
||||||
|
fn serialize_document_picks_format_by_extension() {
|
||||||
|
let mut doc = CadDocument::new();
|
||||||
|
doc.version = acadrust::DxfVersion::AC1032;
|
||||||
|
let dxf = serialize_document(&doc, "dxf").expect("dxf bytes");
|
||||||
|
assert!(!dxf.is_empty());
|
||||||
|
assert!(dxf.starts_with(b" 0") || dxf.starts_with(b"0\n"), "not a DXF header");
|
||||||
|
let dwg = serialize_document(&doc, "dwg").expect("dwg bytes");
|
||||||
|
assert!(dwg.starts_with(b"AC10"), "not a DWG sentinel");
|
||||||
|
// Case-insensitive: the tab state and save dialog both pass through.
|
||||||
|
let dwg_upper = serialize_document(&doc, "DWG").expect("dwg bytes");
|
||||||
|
assert!(dwg_upper.starts_with(b"AC10"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The worker path must serialize to the same bytes the inline fallback
|
||||||
|
// would — the whole point of sharing this implementation.
|
||||||
|
#[test]
|
||||||
|
fn serialize_document_is_deterministic_across_calls() {
|
||||||
|
let doc = sample_document();
|
||||||
|
let a = serialize_document(&doc, "dxf").expect("bytes a");
|
||||||
|
let b = serialize_document(&doc, "dxf").expect("bytes b");
|
||||||
|
assert_eq!(a, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,5 +14,8 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
console_error_panic_hook = "0.1"
|
console_error_panic_hook = "0.1"
|
||||||
getrandom = { version = "0.3", features = ["wasm_js"] }
|
getrandom = { version = "0.3", features = ["wasm_js"] }
|
||||||
js-sys = "0.3"
|
js-sys = "0.3"
|
||||||
|
# Shared save serialization + worker wire format (one implementation for both
|
||||||
|
# this worker and the app's inline fallback).
|
||||||
|
ocs_save = { path = "../ocs_save" }
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
@@ -8,23 +8,11 @@ use sha2::{Digest, Sha256};
|
|||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
const HASH_MARKER: &str = "\nreport-source-sha256:";
|
const HASH_MARKER: &str = "\nreport-source-sha256:";
|
||||||
const PROTOCOL_VERSION: u16 = 4;
|
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
/// Version of the shared worker wire format (see `ocs_save`). Both sides of
|
||||||
struct EntityRuntimeFields {
|
/// the boundary — this crate and the app's `io::web_worker` — check payloads
|
||||||
handle: u64,
|
/// against it.
|
||||||
linetype_handle: Option<u64>,
|
const PROTOCOL_VERSION: u16 = ocs_save::PROTOCOL_VERSION;
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse DWG/DXF on a dedicated browser worker and return a compact serialized
|
/// 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
|
/// document. The main wasm instance only deserializes and installs it, so the
|
||||||
@@ -147,6 +135,28 @@ pub fn sha256_document(bytes: Uint8Array) -> String {
|
|||||||
sha256_document_bytes(&bytes.to_vec())
|
sha256_document_bytes(&bytes.to_vec())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Serialize an already-prepared document to DWG/DXF bytes on the dedicated
|
||||||
|
/// browser worker, so the expensive writer pass (bit-level DWG encoding /
|
||||||
|
/// ASCII DXF formatting) never occupies the UI thread. `encoded_document` is
|
||||||
|
/// the shared `ocs_save` request payload (document + runtime-field sidecar +
|
||||||
|
/// target extension); the reply is the shared response payload (protocol
|
||||||
|
/// version + `Result<bytes, error text>`). The main wasm instance only decodes
|
||||||
|
/// the result — the save-side counterpart of [`parse_document`].
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn serialize_document(
|
||||||
|
encoded_document: Uint8Array,
|
||||||
|
report_stage: &Function,
|
||||||
|
) -> Result<Uint8Array, JsValue> {
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
report_stage.call1(&JsValue::NULL, &JsValue::from_str("decode document"))?;
|
||||||
|
let (document, ext) = ocs_save::decode_request(&encoded_document.to_vec())?;
|
||||||
|
report_stage.call1(&JsValue::NULL, &JsValue::from_str("serialize drawing"))?;
|
||||||
|
let result = ocs_save::serialize_document(&document, &ext);
|
||||||
|
report_stage.call1(&JsValue::NULL, &JsValue::from_str("copy output"))?;
|
||||||
|
let encoded = ocs_save::encode_response(result)?;
|
||||||
|
Ok(Uint8Array::from(encoded.as_slice()))
|
||||||
|
}
|
||||||
|
|
||||||
fn report_fingerprint_needed(stats: &acadrust::ReadStats) -> bool {
|
fn report_fingerprint_needed(stats: &acadrust::ReadStats) -> bool {
|
||||||
stats.recovered() || stats.skipped_source_records > 0 || !stats.stream_completed
|
stats.recovered() || stats.skipped_source_records > 0 || !stats.stream_completed
|
||||||
}
|
}
|
||||||
@@ -178,33 +188,7 @@ fn encode_result(
|
|||||||
let runtime_fields = result
|
let runtime_fields = result
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok()
|
.ok()
|
||||||
.map(|outcome| {
|
.map(|outcome| ocs_save::extract_entity_runtime_fields(&outcome.document))
|
||||||
outcome
|
|
||||||
.document
|
|
||||||
.entities()
|
|
||||||
.map(|entity| {
|
|
||||||
let common = entity.common();
|
|
||||||
EntityRuntimeFields {
|
|
||||||
handle: common.handle.value(),
|
|
||||||
linetype_handle: common.linetype_handle.map(|handle| handle.value()),
|
|
||||||
color_book_handle: common.color_book_handle.map(|handle| handle.value()),
|
|
||||||
face_visual_style_handle: common
|
|
||||||
.face_visual_style_handle
|
|
||||||
.map(|handle| handle.value()),
|
|
||||||
edge_visual_style_handle: common
|
|
||||||
.edge_visual_style_handle
|
|
||||||
.map(|handle| handle.value()),
|
|
||||||
material_flags: common.material_flags,
|
|
||||||
material_handle: common.material_handle.map(|handle| handle.value()),
|
|
||||||
shadow_flags: common.shadow_flags,
|
|
||||||
plotstyle_flags: common.plotstyle_flags,
|
|
||||||
plotstyle_handle: common.plotstyle_handle.map(|handle| handle.value()),
|
|
||||||
entity_mode: common.entity_mode,
|
|
||||||
has_ds_data: common.has_ds_data,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let encoded = bincode::serialize(&(
|
let encoded = bincode::serialize(&(
|
||||||
PROTOCOL_VERSION,
|
PROTOCOL_VERSION,
|
||||||
|
|||||||
@@ -1993,11 +1993,17 @@ file =
|
|||||||
.warning-corrupt-entities-dropped-parser-junk-b = تحذير: أُسقط __ocs_fmt_0__ كيان تالف (بيانات تحليل غير صالحة — اتجاهات/أعداد سيئة)
|
.warning-corrupt-entities-dropped-parser-junk-b = تحذير: أُسقط __ocs_fmt_0__ كيان تالف (بيانات تحليل غير صالحة — اتجاهات/أعداد سيئة)
|
||||||
.warning-corrupt-xref-entities-dropped = تحذير: أُسقط __ocs_fmt_0__ كيان مرجع خارجي تالف
|
.warning-corrupt-xref-entities-dropped = تحذير: أُسقط __ocs_fmt_0__ كيان مرجع خارجي تالف
|
||||||
.xref-loaded = XREF حُمّل "__ocs_fmt_0__"
|
.xref-loaded = XREF حُمّل "__ocs_fmt_0__"
|
||||||
|
.cmd-cannot-build-the-transmittal-zip-e = __ocs_fmt_0__: لا يمكن إنشاء ZIP الإرسال (__ocs_fmt_1__).
|
||||||
.cmd-cannot-create-folder-e = __ocs_fmt_0__: تعذر إنشاء المجلد (__ocs_fmt_1__).
|
.cmd-cannot-create-folder-e = __ocs_fmt_0__: تعذر إنشاء المجلد (__ocs_fmt_1__).
|
||||||
|
.cmd-cannot-package-the-drawing-e = __ocs_fmt_0__: لا يمكن حزم الرسم (__ocs_fmt_1__).
|
||||||
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: جُمّع __ocs_fmt_1__ ملف في __ocs_fmt_2__
|
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: جُمّع __ocs_fmt_1__ ملف في __ocs_fmt_2__
|
||||||
|
.cmd-skipped-n-referenced-file-s-not-loaded-in = __ocs_fmt_0__: تم تخطي __ocs_fmt_1__ ملفًا مرجعيًا غير محمّل في هذه الجلسة: __ocs_fmt_2__
|
||||||
.error-external-change-checks-remain-active = __ocs_fmt_0__. تظل فحوص التغيير الخارجي نشطة.
|
.error-external-change-checks-remain-active = __ocs_fmt_0__. تظل فحوص التغيير الخارجي نشطة.
|
||||||
.warning-external-change-checks-remain-active = __ocs_fmt_0__. تظل فحوص التغيير الخارجي نشطة.
|
.warning-external-change-checks-remain-active = __ocs_fmt_0__. تظل فحوص التغيير الخارجي نشطة.
|
||||||
.opening-a-drawing-by-path-is-not-available-in = فتح رسم بواسطة المسار غير متاح في المتصفح.
|
.opening-a-drawing-by-path-is-not-available-in = فتح رسم بواسطة المسار غير متاح في المتصفح.
|
||||||
|
.pick-the-file-again-to-load-its-contents = اختر الملف مرة أخرى لتحميل محتوياته في المتصفح.
|
||||||
|
.adding-folders-is-not-available-in-the-browser = إضافة المجلدات غير متاحة في المتصفح. أضف ملفات الرسومات بدلاً من ذلك.
|
||||||
|
.drawing-not-loaded-in-the-browser = رسم غير محمّل في المتصفح: __ocs_fmt_0__.
|
||||||
|
|
||||||
selection =
|
selection =
|
||||||
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ حدد الكائنات (__ocs_arg_count__ محدد، Enter للتطبيق):
|
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ حدد الكائنات (__ocs_arg_count__ محدد، Enter للتطبيق):
|
||||||
@@ -3147,6 +3153,7 @@ insert =
|
|||||||
.xref-no-external-references-in-this-drawing = XREF لا توجد مراجع خارجية في هذا الرسم.
|
.xref-no-external-references-in-this-drawing = XREF لا توجد مراجع خارجية في هذا الرسم.
|
||||||
.xref-reloaded = XREF أُعيد تحميل "__ocs_fmt_0__"
|
.xref-reloaded = XREF أُعيد تحميل "__ocs_fmt_0__"
|
||||||
.xref-save-the-drawing-first-to-resolve-relativ = XREF احفظ الرسم أولًا لحل مسارات المراجع الخارجية النسبية.
|
.xref-save-the-drawing-first-to-resolve-relativ = XREF احفظ الرسم أولًا لحل مسارات المراجع الخارجية النسبية.
|
||||||
|
.xref-unresolved-external-references = XREF مراجع خارجية غير محلولة: __ocs_fmt_0__ — أفلت ملفات DWG/DXF المُشار إليها على الصفحة لتسجيلها، ثم أعد فتح هذا الرسم أو نفّذ XRELOAD.
|
||||||
.object-s-copied-to-clipboard-base-3-3 = نُسخ __ocs_fmt_0__ كائن إلى الحافظة (الأساس __ocs_fmt_1__,__ocs_fmt_2__).
|
.object-s-copied-to-clipboard-base-3-3 = نُسخ __ocs_fmt_0__ كائن إلى الحافظة (الأساس __ocs_fmt_1__,__ocs_fmt_2__).
|
||||||
.object-s-copied-to-clipboard = نُسخ __ocs_fmt_0__ كائن إلى الحافظة.
|
.object-s-copied-to-clipboard = نُسخ __ocs_fmt_0__ كائن إلى الحافظة.
|
||||||
.object-s-cut-to-clipboard = قُص __ocs_fmt_0__ كائن إلى الحافظة.
|
.object-s-cut-to-clipboard = قُص __ocs_fmt_0__ كائن إلى الحافظة.
|
||||||
|
|||||||
@@ -1980,11 +1980,17 @@ file =
|
|||||||
.warning-corrupt-entities-dropped-parser-junk-b = Предупреждение: __ocs_fmt_0__ повредени обекта отхвърлени (грешка в анализатора — лоши нормали / брой)
|
.warning-corrupt-entities-dropped-parser-junk-b = Предупреждение: __ocs_fmt_0__ повредени обекта отхвърлени (грешка в анализатора — лоши нормали / брой)
|
||||||
.warning-corrupt-xref-entities-dropped = Предупреждение: __ocs_fmt_0__ повредени xref обекта отхвърлени
|
.warning-corrupt-xref-entities-dropped = Предупреждение: __ocs_fmt_0__ повредени xref обекта отхвърлени
|
||||||
.xref-loaded = XREF Зареден "__ocs_fmt_0__"
|
.xref-loaded = XREF Зареден "__ocs_fmt_0__"
|
||||||
|
.cmd-cannot-build-the-transmittal-zip-e = __ocs_fmt_0__: не може да се създаде ZIP за предаване (__ocs_fmt_1__).
|
||||||
.cmd-cannot-create-folder-e = __ocs_fmt_0__: папката не може да бъде създадена (__ocs_fmt_1__).
|
.cmd-cannot-create-folder-e = __ocs_fmt_0__: папката не може да бъде създадена (__ocs_fmt_1__).
|
||||||
|
.cmd-cannot-package-the-drawing-e = __ocs_fmt_0__: чертежът не може да се пакетира (__ocs_fmt_1__).
|
||||||
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: пакетирани __ocs_fmt_1__ файл(а) в __ocs_fmt_2__
|
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: пакетирани __ocs_fmt_1__ файл(а) в __ocs_fmt_2__
|
||||||
|
.cmd-skipped-n-referenced-file-s-not-loaded-in = __ocs_fmt_0__: пропуснати __ocs_fmt_1__ свързани файла, незаредени в тази сесия: __ocs_fmt_2__
|
||||||
.error-external-change-checks-remain-active = __ocs_fmt_0__. Проверките за външни промени остават активни.
|
.error-external-change-checks-remain-active = __ocs_fmt_0__. Проверките за външни промени остават активни.
|
||||||
.warning-external-change-checks-remain-active = __ocs_fmt_0__. Проверките за външни промени остават активни.
|
.warning-external-change-checks-remain-active = __ocs_fmt_0__. Проверките за външни промени остават активни.
|
||||||
.opening-a-drawing-by-path-is-not-available-in = Отварянето на чертеж по път не е налично в браузъра.
|
.opening-a-drawing-by-path-is-not-available-in = Отварянето на чертеж по път не е налично в браузъра.
|
||||||
|
.pick-the-file-again-to-load-its-contents = Изберете файла отново, за да заредите съдържанието му в браузъра.
|
||||||
|
.adding-folders-is-not-available-in-the-browser = Добавянето на папки не е налично в браузъра. Вместо това добавете файлове с чертежи.
|
||||||
|
.drawing-not-loaded-in-the-browser = Чертежът не е зареден в браузъра: __ocs_fmt_0__.
|
||||||
|
|
||||||
selection =
|
selection =
|
||||||
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Избери обекти (__ocs_arg_count__ избрани, Enter за прилагане):
|
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Избери обекти (__ocs_arg_count__ избрани, Enter за прилагане):
|
||||||
@@ -3134,6 +3140,7 @@ insert =
|
|||||||
.xref-no-external-references-in-this-drawing = XREF Няма външни референции в този чертеж.
|
.xref-no-external-references-in-this-drawing = XREF Няма външни референции в този чертеж.
|
||||||
.xref-reloaded = XREF Презареден "__ocs_fmt_0__"
|
.xref-reloaded = XREF Презареден "__ocs_fmt_0__"
|
||||||
.xref-save-the-drawing-first-to-resolve-relativ = XREF Първо запази чертежа, за да се разрешат относителните XREF пътища.
|
.xref-save-the-drawing-first-to-resolve-relativ = XREF Първо запази чертежа, за да се разрешат относителните XREF пътища.
|
||||||
|
.xref-unresolved-external-references = XREF Неразрешени външни препратки: __ocs_fmt_0__ — плъзнете посочените файлове DWG/DXF върху страницата, за да ги регистрирате, след което отворете чертежа отново или изпълнете XRELOAD.
|
||||||
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ обект(а) копирани в клипборда (база __ocs_fmt_1__,__ocs_fmt_2__).
|
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ обект(а) копирани в клипборда (база __ocs_fmt_1__,__ocs_fmt_2__).
|
||||||
.object-s-copied-to-clipboard = __ocs_fmt_0__ обект(а) копирани в клипборда.
|
.object-s-copied-to-clipboard = __ocs_fmt_0__ обект(а) копирани в клипборда.
|
||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ обект(а) изрязани в клипборда.
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ обект(а) изрязани в клипборда.
|
||||||
|
|||||||
@@ -1980,11 +1980,17 @@ file =
|
|||||||
.warning-corrupt-entities-dropped-parser-junk-b = Varování: __ocs_fmt_0__ poškozené entity byly odstraněny (nevyžádaná analyzátor – špatné normály / počty)
|
.warning-corrupt-entities-dropped-parser-junk-b = Varování: __ocs_fmt_0__ poškozené entity byly odstraněny (nevyžádaná analyzátor – špatné normály / počty)
|
||||||
.warning-corrupt-xref-entities-dropped = Varování: __ocs_fmt_0__ poškozené entity externí reference byly zrušeny
|
.warning-corrupt-xref-entities-dropped = Varování: __ocs_fmt_0__ poškozené entity externí reference byly zrušeny
|
||||||
.xref-loaded = XREF Načteno "__ocs_fmt_0__"
|
.xref-loaded = XREF Načteno "__ocs_fmt_0__"
|
||||||
|
.cmd-cannot-build-the-transmittal-zip-e = __ocs_fmt_0__: nelze vytvořit přenosový ZIP (__ocs_fmt_1__).
|
||||||
.cmd-cannot-create-folder-e = __ocs_fmt_0__: Nelze vytvořit složku (__ocs_fmt_1__).
|
.cmd-cannot-create-folder-e = __ocs_fmt_0__: Nelze vytvořit složku (__ocs_fmt_1__).
|
||||||
|
.cmd-cannot-package-the-drawing-e = __ocs_fmt_0__: výkres nelze zabalit (__ocs_fmt_1__).
|
||||||
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: zabalené soubory __ocs_fmt_1__ do __ocs_fmt_2__
|
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: zabalené soubory __ocs_fmt_1__ do __ocs_fmt_2__
|
||||||
|
.cmd-skipped-n-referenced-file-s-not-loaded-in = __ocs_fmt_0__: přeskočeno __ocs_fmt_1__ odkazovaných souborů, které v této relaci nebyly načteny: __ocs_fmt_2__
|
||||||
.error-external-change-checks-remain-active = __ocs_fmt_0__. Kontroly externí změny zůstávají aktivní.
|
.error-external-change-checks-remain-active = __ocs_fmt_0__. Kontroly externí změny zůstávají aktivní.
|
||||||
.warning-external-change-checks-remain-active = __ocs_fmt_0__. Kontroly externí změny zůstávají aktivní.
|
.warning-external-change-checks-remain-active = __ocs_fmt_0__. Kontroly externí změny zůstávají aktivní.
|
||||||
.opening-a-drawing-by-path-is-not-available-in = Otevření výkresu podle cesty není v prohlížeči k dispozici.
|
.opening-a-drawing-by-path-is-not-available-in = Otevření výkresu podle cesty není v prohlížeči k dispozici.
|
||||||
|
.pick-the-file-again-to-load-its-contents = Vyberte soubor znovu, abyste načetli jeho obsah do prohlížeče.
|
||||||
|
.adding-folders-is-not-available-in-the-browser = Přidávání složek není v prohlížeči k dispozici. Místo toho přidejte soubory výkresů.
|
||||||
|
.drawing-not-loaded-in-the-browser = Výkres není v prohlížeči načten: __ocs_fmt_0__.
|
||||||
|
|
||||||
selection =
|
selection =
|
||||||
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Vyberte objekty (vybráno __ocs_arg_count__, použít Enter):
|
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Vyberte objekty (vybráno __ocs_arg_count__, použít Enter):
|
||||||
@@ -3132,6 +3138,7 @@ insert =
|
|||||||
.xref-no-external-references-in-this-drawing = XREF V tomto výkresu nejsou žádné externí odkazy.
|
.xref-no-external-references-in-this-drawing = XREF V tomto výkresu nejsou žádné externí odkazy.
|
||||||
.xref-reloaded = XREF Znovu načteno "__ocs_fmt_0__"
|
.xref-reloaded = XREF Znovu načteno "__ocs_fmt_0__"
|
||||||
.xref-save-the-drawing-first-to-resolve-relativ = XREF Nejprve uložte výkres, abyste vyřešili relativní cesty XREF.
|
.xref-save-the-drawing-first-to-resolve-relativ = XREF Nejprve uložte výkres, abyste vyřešili relativní cesty XREF.
|
||||||
|
.xref-unresolved-external-references = XREF Nevyřešené externí odkazy: __ocs_fmt_0__ — přetáhněte odkazované soubory DWG/DXF na stránku, aby se zaregistrovaly, a poté tento výkres znovu otevřete nebo spusťte XRELOAD.
|
||||||
.object-s-copied-to-clipboard-base-3-3 = Objekt(y) __ocs_fmt_0__ zkopírován do schránky (základna __ocs_fmt_1__,__ocs_fmt_2__).
|
.object-s-copied-to-clipboard-base-3-3 = Objekt(y) __ocs_fmt_0__ zkopírován do schránky (základna __ocs_fmt_1__,__ocs_fmt_2__).
|
||||||
.object-s-copied-to-clipboard = Objekt(y) __ocs_fmt_0__ zkopírován do schránky.
|
.object-s-copied-to-clipboard = Objekt(y) __ocs_fmt_0__ zkopírován do schránky.
|
||||||
.object-s-cut-to-clipboard = Objekt(y) __ocs_fmt_0__ vyříznutý do schránky.
|
.object-s-cut-to-clipboard = Objekt(y) __ocs_fmt_0__ vyříznutý do schránky.
|
||||||
|
|||||||
@@ -1981,11 +1981,17 @@ file =
|
|||||||
.warning-corrupt-entities-dropped-parser-junk-b = Warnung: __ocs_fmt_0__ beschädigte Objekte verworfen (ungültige Analysedaten — fehlerhafte Normalen oder Anzahlen)
|
.warning-corrupt-entities-dropped-parser-junk-b = Warnung: __ocs_fmt_0__ beschädigte Objekte verworfen (ungültige Analysedaten — fehlerhafte Normalen oder Anzahlen)
|
||||||
.warning-corrupt-xref-entities-dropped = Warnung: __ocs_fmt_0__ beschädigte XRef-Objekte verworfen
|
.warning-corrupt-xref-entities-dropped = Warnung: __ocs_fmt_0__ beschädigte XRef-Objekte verworfen
|
||||||
.xref-loaded = XREF Geladen "__ocs_fmt_0__"
|
.xref-loaded = XREF Geladen "__ocs_fmt_0__"
|
||||||
|
.cmd-cannot-build-the-transmittal-zip-e = __ocs_fmt_0__: das Übertragungs-ZIP kann nicht erstellt werden (__ocs_fmt_1__).
|
||||||
.cmd-cannot-create-folder-e = __ocs_fmt_0__: kann keinen Ordner erstellen (__ocs_fmt_1__).
|
.cmd-cannot-create-folder-e = __ocs_fmt_0__: kann keinen Ordner erstellen (__ocs_fmt_1__).
|
||||||
|
.cmd-cannot-package-the-drawing-e = __ocs_fmt_0__: die Zeichnung kann nicht gepackt werden (__ocs_fmt_1__).
|
||||||
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: verpackte __ocs_fmt_1__ Datei(en) in __ocs_fmt_2__
|
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: verpackte __ocs_fmt_1__ Datei(en) in __ocs_fmt_2__
|
||||||
|
.cmd-skipped-n-referenced-file-s-not-loaded-in = __ocs_fmt_0__: __ocs_fmt_1__ referenzierte Datei(en) übersprungen, die in dieser Sitzung nicht geladen wurden: __ocs_fmt_2__
|
||||||
.error-external-change-checks-remain-active = __ocs_fmt_0__. Externe Wechselprüfungen bleiben aktiv.
|
.error-external-change-checks-remain-active = __ocs_fmt_0__. Externe Wechselprüfungen bleiben aktiv.
|
||||||
.warning-external-change-checks-remain-active = __ocs_fmt_0__. Externe Wechselprüfungen bleiben aktiv.
|
.warning-external-change-checks-remain-active = __ocs_fmt_0__. Externe Wechselprüfungen bleiben aktiv.
|
||||||
.opening-a-drawing-by-path-is-not-available-in = Das Öffnen einer Zeichnung über einen Pfad ist im Browser nicht verfügbar.
|
.opening-a-drawing-by-path-is-not-available-in = Das Öffnen einer Zeichnung über einen Pfad ist im Browser nicht verfügbar.
|
||||||
|
.pick-the-file-again-to-load-its-contents = Wähle die Datei erneut aus, um ihren Inhalt in den Browser zu laden.
|
||||||
|
.adding-folders-is-not-available-in-the-browser = Das Hinzufügen von Ordnern ist im Browser nicht verfügbar. Füge stattdessen Zeichnungsdateien hinzu.
|
||||||
|
.drawing-not-loaded-in-the-browser = Zeichnung ist nicht im Browser geladen: __ocs_fmt_0__.
|
||||||
|
|
||||||
selection =
|
selection =
|
||||||
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Objekte auswählen (__ocs_arg_count__ ausgewählt, Eingabe zum Anwenden):
|
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Objekte auswählen (__ocs_arg_count__ ausgewählt, Eingabe zum Anwenden):
|
||||||
@@ -3133,6 +3139,7 @@ insert =
|
|||||||
.xref-no-external-references-in-this-drawing = XREF Keine externen Referenzen in dieser Zeichnung.
|
.xref-no-external-references-in-this-drawing = XREF Keine externen Referenzen in dieser Zeichnung.
|
||||||
.xref-reloaded = XREF Reloaded "__ocs_fmt_0__"
|
.xref-reloaded = XREF Reloaded "__ocs_fmt_0__"
|
||||||
.xref-save-the-drawing-first-to-resolve-relativ = XREF Speichern Sie zuerst die Zeichnung, um relative XREF Pfade aufzulösen.
|
.xref-save-the-drawing-first-to-resolve-relativ = XREF Speichern Sie zuerst die Zeichnung, um relative XREF Pfade aufzulösen.
|
||||||
|
.xref-unresolved-external-references = XREF Ungelöste externe Referenzen: __ocs_fmt_0__ — legen Sie die referenzierten DWG/DXF-Dateien auf der Seite ab, um sie zu registrieren, und öffnen Sie dann diese Zeichnung erneut oder führen Sie XRELOAD aus.
|
||||||
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ Objekt(e) kopiert ins Zwischenablage (Basis __ocs_fmt_1__,__ocs_fmt_2__).
|
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ Objekt(e) kopiert ins Zwischenablage (Basis __ocs_fmt_1__,__ocs_fmt_2__).
|
||||||
.object-s-copied-to-clipboard = __ocs_fmt_0__ Objekt(e) kopiert ins Clipboard.
|
.object-s-copied-to-clipboard = __ocs_fmt_0__ Objekt(e) kopiert ins Clipboard.
|
||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ Objekt(e) in die Zwischenablage geschnitten.
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ Objekt(e) in die Zwischenablage geschnitten.
|
||||||
|
|||||||
@@ -1980,11 +1980,17 @@ file =
|
|||||||
.warning-corrupt-entities-dropped-parser-junk-b = Προειδοποίηση: απορρίφθηκαν __ocs_fmt_0__ κατεστραμμένες οντότητες (άχρηστα δεδομένα αναλυτή — εσφαλμένα κάθετα διανύσματα / πλήθη)
|
.warning-corrupt-entities-dropped-parser-junk-b = Προειδοποίηση: απορρίφθηκαν __ocs_fmt_0__ κατεστραμμένες οντότητες (άχρηστα δεδομένα αναλυτή — εσφαλμένα κάθετα διανύσματα / πλήθη)
|
||||||
.warning-corrupt-xref-entities-dropped = Προειδοποίηση: απορρίφθηκαν __ocs_fmt_0__ κατεστραμμένες οντότητες xref
|
.warning-corrupt-xref-entities-dropped = Προειδοποίηση: απορρίφθηκαν __ocs_fmt_0__ κατεστραμμένες οντότητες xref
|
||||||
.xref-loaded = XREF Φορτώθηκε το "__ocs_fmt_0__"
|
.xref-loaded = XREF Φορτώθηκε το "__ocs_fmt_0__"
|
||||||
|
.cmd-cannot-build-the-transmittal-zip-e = __ocs_fmt_0__: δεν είναι δυνατή η δημιουργία του ZIP μεταφοράς (__ocs_fmt_1__).
|
||||||
.cmd-cannot-create-folder-e = __ocs_fmt_0__: δεν είναι δυνατή η δημιουργία φακέλου (__ocs_fmt_1__).
|
.cmd-cannot-create-folder-e = __ocs_fmt_0__: δεν είναι δυνατή η δημιουργία φακέλου (__ocs_fmt_1__).
|
||||||
|
.cmd-cannot-package-the-drawing-e = __ocs_fmt_0__: δεν είναι δυνατή η συσκευασία του σχεδίου (__ocs_fmt_1__).
|
||||||
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: συσκευάστηκαν __ocs_fmt_1__ αρχείο(α) στο __ocs_fmt_2__
|
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: συσκευάστηκαν __ocs_fmt_1__ αρχείο(α) στο __ocs_fmt_2__
|
||||||
|
.cmd-skipped-n-referenced-file-s-not-loaded-in = __ocs_fmt_0__: παραλείφθηκαν __ocs_fmt_1__ αναφερόμενα αρχεία που δεν φορτώθηκαν σε αυτή τη συνεδρία: __ocs_fmt_2__
|
||||||
.error-external-change-checks-remain-active = __ocs_fmt_0__. Οι έλεγχοι εξωτερικών αλλαγών παραμένουν ενεργοί.
|
.error-external-change-checks-remain-active = __ocs_fmt_0__. Οι έλεγχοι εξωτερικών αλλαγών παραμένουν ενεργοί.
|
||||||
.warning-external-change-checks-remain-active = __ocs_fmt_0__. Οι έλεγχοι εξωτερικών αλλαγών παραμένουν ενεργοί.
|
.warning-external-change-checks-remain-active = __ocs_fmt_0__. Οι έλεγχοι εξωτερικών αλλαγών παραμένουν ενεργοί.
|
||||||
.opening-a-drawing-by-path-is-not-available-in = Το άνοιγμα σχεδίου μέσω διαδρομής δεν είναι διαθέσιμο στο πρόγραμμα περιήγησης.
|
.opening-a-drawing-by-path-is-not-available-in = Το άνοιγμα σχεδίου μέσω διαδρομής δεν είναι διαθέσιμο στο πρόγραμμα περιήγησης.
|
||||||
|
.pick-the-file-again-to-load-its-contents = Επιλέξτε ξανά το αρχείο για να φορτώσετε το περιεχόμενό του στο πρόγραμμα περιήγησης.
|
||||||
|
.adding-folders-is-not-available-in-the-browser = Η προσθήκη φακέλων δεν είναι διαθέσιμη στο πρόγραμμα περιήγησης. Προσθέστε αρχεία σχεδίων αντί για αυτό.
|
||||||
|
.drawing-not-loaded-in-the-browser = Το σχέδιο δεν έχει φορτωθεί στο πρόγραμμα περιήγησης: __ocs_fmt_0__.
|
||||||
|
|
||||||
selection =
|
selection =
|
||||||
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Επιλέξτε αντικείμενα (__ocs_arg_count__ επιλεγμένα, Enter για εφαρμογή):
|
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Επιλέξτε αντικείμενα (__ocs_arg_count__ επιλεγμένα, Enter για εφαρμογή):
|
||||||
@@ -3142,6 +3148,7 @@ insert =
|
|||||||
.xref-no-external-references-in-this-drawing = XREF Δεν υπάρχουν εξωτερικές αναφορές σε αυτό το σχέδιο.
|
.xref-no-external-references-in-this-drawing = XREF Δεν υπάρχουν εξωτερικές αναφορές σε αυτό το σχέδιο.
|
||||||
.xref-reloaded = XREF Επαναφορτώθηκε το "__ocs_fmt_0__"
|
.xref-reloaded = XREF Επαναφορτώθηκε το "__ocs_fmt_0__"
|
||||||
.xref-save-the-drawing-first-to-resolve-relativ = XREF Αποθηκεύστε πρώτα το σχέδιο για να επιλυθούν οι σχετικές διαδρομές XREF.
|
.xref-save-the-drawing-first-to-resolve-relativ = XREF Αποθηκεύστε πρώτα το σχέδιο για να επιλυθούν οι σχετικές διαδρομές XREF.
|
||||||
|
.xref-unresolved-external-references = XREF Ανεπίλυτες εξωτερικές αναφορές: __ocs_fmt_0__ — σύρετε τα αναφερόμενα αρχεία DWG/DXF στη σελίδα για να καταχωρηθούν και, στη συνέχεια, ανοίξτε ξανά αυτό το σχέδιο ή εκτελέστε XRELOAD.
|
||||||
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ αντικείμενο(α) αντιγράφηκαν στο πρόχειρο (βάση __ocs_fmt_1__,__ocs_fmt_2__).
|
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ αντικείμενο(α) αντιγράφηκαν στο πρόχειρο (βάση __ocs_fmt_1__,__ocs_fmt_2__).
|
||||||
.object-s-copied-to-clipboard = __ocs_fmt_0__ αντικείμενο(α) αντιγράφηκαν στο πρόχειρο.
|
.object-s-copied-to-clipboard = __ocs_fmt_0__ αντικείμενο(α) αντιγράφηκαν στο πρόχειρο.
|
||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ αντικείμενο(α) αποκόπηκαν στο πρόχειρο.
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ αντικείμενο(α) αποκόπηκαν στο πρόχειρο.
|
||||||
|
|||||||
@@ -1981,11 +1981,17 @@ file =
|
|||||||
.warning-corrupt-entities-dropped-parser-junk-b = Warning: __ocs_fmt_0__ corrupt entities dropped (parser junk — bad normals / counts)
|
.warning-corrupt-entities-dropped-parser-junk-b = Warning: __ocs_fmt_0__ corrupt entities dropped (parser junk — bad normals / counts)
|
||||||
.warning-corrupt-xref-entities-dropped = Warning: __ocs_fmt_0__ corrupt xref entities dropped
|
.warning-corrupt-xref-entities-dropped = Warning: __ocs_fmt_0__ corrupt xref entities dropped
|
||||||
.xref-loaded = XREF Loaded "__ocs_fmt_0__"
|
.xref-loaded = XREF Loaded "__ocs_fmt_0__"
|
||||||
|
.cmd-cannot-build-the-transmittal-zip-e = __ocs_fmt_0__: cannot build the transmittal ZIP (__ocs_fmt_1__).
|
||||||
.cmd-cannot-create-folder-e = __ocs_fmt_0__: cannot create folder (__ocs_fmt_1__).
|
.cmd-cannot-create-folder-e = __ocs_fmt_0__: cannot create folder (__ocs_fmt_1__).
|
||||||
|
.cmd-cannot-package-the-drawing-e = __ocs_fmt_0__: cannot package the drawing (__ocs_fmt_1__).
|
||||||
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: packaged __ocs_fmt_1__ file(s) into __ocs_fmt_2__
|
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: packaged __ocs_fmt_1__ file(s) into __ocs_fmt_2__
|
||||||
|
.cmd-skipped-n-referenced-file-s-not-loaded-in = __ocs_fmt_0__: skipped __ocs_fmt_1__ referenced file(s) not loaded in this session: __ocs_fmt_2__
|
||||||
.error-external-change-checks-remain-active = __ocs_fmt_0__. External-change checks remain active.
|
.error-external-change-checks-remain-active = __ocs_fmt_0__. External-change checks remain active.
|
||||||
.warning-external-change-checks-remain-active = __ocs_fmt_0__. External-change checks remain active.
|
.warning-external-change-checks-remain-active = __ocs_fmt_0__. External-change checks remain active.
|
||||||
.opening-a-drawing-by-path-is-not-available-in = Opening a drawing by path is not available in the browser.
|
.opening-a-drawing-by-path-is-not-available-in = Opening a drawing by path is not available in the browser.
|
||||||
|
.pick-the-file-again-to-load-its-contents = Pick the file again to load its contents in the browser.
|
||||||
|
.adding-folders-is-not-available-in-the-browser = Adding folders is not available in the browser. Add drawing files instead.
|
||||||
|
.drawing-not-loaded-in-the-browser = Drawing not loaded in the browser: __ocs_fmt_0__.
|
||||||
|
|
||||||
selection =
|
selection =
|
||||||
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Select objects (__ocs_arg_count__ selected, Enter to apply):
|
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Select objects (__ocs_arg_count__ selected, Enter to apply):
|
||||||
@@ -3143,6 +3149,7 @@ insert =
|
|||||||
.xref-no-external-references-in-this-drawing = XREF No external references in this drawing.
|
.xref-no-external-references-in-this-drawing = XREF No external references in this drawing.
|
||||||
.xref-reloaded = XREF Reloaded "__ocs_fmt_0__"
|
.xref-reloaded = XREF Reloaded "__ocs_fmt_0__"
|
||||||
.xref-save-the-drawing-first-to-resolve-relativ = XREF Save the drawing first to resolve relative XREF paths.
|
.xref-save-the-drawing-first-to-resolve-relativ = XREF Save the drawing first to resolve relative XREF paths.
|
||||||
|
.xref-unresolved-external-references = XREF Unresolved external references: __ocs_fmt_0__ — drop the referenced DWG/DXF files on the page to register them, then reopen this drawing or run XRELOAD.
|
||||||
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ object(s) copied to clipboard (base __ocs_fmt_1__,__ocs_fmt_2__).
|
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ object(s) copied to clipboard (base __ocs_fmt_1__,__ocs_fmt_2__).
|
||||||
.object-s-copied-to-clipboard = __ocs_fmt_0__ object(s) copied to clipboard.
|
.object-s-copied-to-clipboard = __ocs_fmt_0__ object(s) copied to clipboard.
|
||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ object(s) cut to clipboard.
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ object(s) cut to clipboard.
|
||||||
|
|||||||
@@ -1981,11 +1981,17 @@ file =
|
|||||||
.warning-corrupt-entities-dropped-parser-junk-b = Advertencia: se descartaron __ocs_fmt_0__ entidades dañadas (datos inválidos del analizador: normales o recuentos erróneos)
|
.warning-corrupt-entities-dropped-parser-junk-b = Advertencia: se descartaron __ocs_fmt_0__ entidades dañadas (datos inválidos del analizador: normales o recuentos erróneos)
|
||||||
.warning-corrupt-xref-entities-dropped = Advertencia: se descartaron __ocs_fmt_0__ entidades de referencia externa dañadas
|
.warning-corrupt-xref-entities-dropped = Advertencia: se descartaron __ocs_fmt_0__ entidades de referencia externa dañadas
|
||||||
.xref-loaded = XREF Se cargó «__ocs_fmt_0__»
|
.xref-loaded = XREF Se cargó «__ocs_fmt_0__»
|
||||||
|
.cmd-cannot-build-the-transmittal-zip-e = __ocs_fmt_0__: no se puede crear el ZIP de transmisión (__ocs_fmt_1__).
|
||||||
.cmd-cannot-create-folder-e = __ocs_fmt_0__: no se puede crear la carpeta (__ocs_fmt_1__).
|
.cmd-cannot-create-folder-e = __ocs_fmt_0__: no se puede crear la carpeta (__ocs_fmt_1__).
|
||||||
|
.cmd-cannot-package-the-drawing-e = __ocs_fmt_0__: no se puede empaquetar el dibujo (__ocs_fmt_1__).
|
||||||
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: se empaquetaron __ocs_fmt_1__ archivos en __ocs_fmt_2__
|
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: se empaquetaron __ocs_fmt_1__ archivos en __ocs_fmt_2__
|
||||||
|
.cmd-skipped-n-referenced-file-s-not-loaded-in = __ocs_fmt_0__: se omitieron __ocs_fmt_1__ archivo(s) referenciado(s) no cargado(s) en esta sesión: __ocs_fmt_2__
|
||||||
.error-external-change-checks-remain-active = __ocs_fmt_0__. Las comprobaciones de cambios externos siguen activas.
|
.error-external-change-checks-remain-active = __ocs_fmt_0__. Las comprobaciones de cambios externos siguen activas.
|
||||||
.warning-external-change-checks-remain-active = __ocs_fmt_0__. Las comprobaciones de cambios externos siguen activas.
|
.warning-external-change-checks-remain-active = __ocs_fmt_0__. Las comprobaciones de cambios externos siguen activas.
|
||||||
.opening-a-drawing-by-path-is-not-available-in = Abrir un dibujo por ruta no está disponible en el navegador.
|
.opening-a-drawing-by-path-is-not-available-in = Abrir un dibujo por ruta no está disponible en el navegador.
|
||||||
|
.pick-the-file-again-to-load-its-contents = Vuelve a elegir el archivo para cargar su contenido en el navegador.
|
||||||
|
.adding-folders-is-not-available-in-the-browser = Añadir carpetas no está disponible en el navegador. Añade archivos de dibujos en su lugar.
|
||||||
|
.drawing-not-loaded-in-the-browser = Dibujo no cargado en el navegador: __ocs_fmt_0__.
|
||||||
|
|
||||||
selection =
|
selection =
|
||||||
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Seleccione objetos (__ocs_arg_count__ seleccionados, Enter para aplicar):
|
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Seleccione objetos (__ocs_arg_count__ seleccionados, Enter para aplicar):
|
||||||
@@ -3135,6 +3141,7 @@ insert =
|
|||||||
.xref-no-external-references-in-this-drawing = XREF No hay referencias externas en este dibujo.
|
.xref-no-external-references-in-this-drawing = XREF No hay referencias externas en este dibujo.
|
||||||
.xref-reloaded = XREF Se volvió a cargar «__ocs_fmt_0__»
|
.xref-reloaded = XREF Se volvió a cargar «__ocs_fmt_0__»
|
||||||
.xref-save-the-drawing-first-to-resolve-relativ = XREF Guarde primero el dibujo para resolver las rutas relativas de referencias externas.
|
.xref-save-the-drawing-first-to-resolve-relativ = XREF Guarde primero el dibujo para resolver las rutas relativas de referencias externas.
|
||||||
|
.xref-unresolved-external-references = XREF Referencias externas sin resolver: __ocs_fmt_0__ — suelte los archivos DWG/DXF de referencia en la página para registrarlos y luego reabra este dibujo o ejecute XRELOAD.
|
||||||
.object-s-copied-to-clipboard-base-3-3 = Se copiaron __ocs_fmt_0__ objetos al portapapeles (base __ocs_fmt_1__,__ocs_fmt_2__).
|
.object-s-copied-to-clipboard-base-3-3 = Se copiaron __ocs_fmt_0__ objetos al portapapeles (base __ocs_fmt_1__,__ocs_fmt_2__).
|
||||||
.object-s-copied-to-clipboard = Se copiaron __ocs_fmt_0__ objetos al portapapeles.
|
.object-s-copied-to-clipboard = Se copiaron __ocs_fmt_0__ objetos al portapapeles.
|
||||||
.object-s-cut-to-clipboard = Se cortaron __ocs_fmt_0__ objetos al portapapeles.
|
.object-s-cut-to-clipboard = Se cortaron __ocs_fmt_0__ objetos al portapapeles.
|
||||||
|
|||||||
@@ -1980,11 +1980,17 @@ file =
|
|||||||
.warning-corrupt-entities-dropped-parser-junk-b = Varoitus: __ocs_fmt_0__-korruptoituneet entiteetit pudotettiin (jäsennysroska - huonot normaalit / määrät)
|
.warning-corrupt-entities-dropped-parser-junk-b = Varoitus: __ocs_fmt_0__-korruptoituneet entiteetit pudotettiin (jäsennysroska - huonot normaalit / määrät)
|
||||||
.warning-corrupt-xref-entities-dropped = Varoitus: __ocs_fmt_0__-korruptoituneet xref-entiteetit putosivat
|
.warning-corrupt-xref-entities-dropped = Varoitus: __ocs_fmt_0__-korruptoituneet xref-entiteetit putosivat
|
||||||
.xref-loaded = XREF Ladattu "__ocs_fmt_0__"
|
.xref-loaded = XREF Ladattu "__ocs_fmt_0__"
|
||||||
|
.cmd-cannot-build-the-transmittal-zip-e = __ocs_fmt_0__: siirto-ZIP:n luominen epäonnistui (__ocs_fmt_1__).
|
||||||
.cmd-cannot-create-folder-e = __ocs_fmt_0__: kansiota (__ocs_fmt_1__) ei voi luoda.
|
.cmd-cannot-create-folder-e = __ocs_fmt_0__: kansiota (__ocs_fmt_1__) ei voi luoda.
|
||||||
|
.cmd-cannot-package-the-drawing-e = __ocs_fmt_0__: piirustuksen paketointi epäonnistui (__ocs_fmt_1__).
|
||||||
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: pakattu __ocs_fmt_1__-tiedosto(t) kohteeseen __ocs_fmt_2__
|
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: pakattu __ocs_fmt_1__-tiedosto(t) kohteeseen __ocs_fmt_2__
|
||||||
|
.cmd-skipped-n-referenced-file-s-not-loaded-in = __ocs_fmt_0__: ohitettiin __ocs_fmt_1__ viitattua tiedostoa, joita ei ladattu tässä istunnossa: __ocs_fmt_2__
|
||||||
.error-external-change-checks-remain-active = __ocs_fmt_0__. Ulkoiset muutostarkastukset pysyvät aktiivisina.
|
.error-external-change-checks-remain-active = __ocs_fmt_0__. Ulkoiset muutostarkastukset pysyvät aktiivisina.
|
||||||
.warning-external-change-checks-remain-active = __ocs_fmt_0__. Ulkoiset muutostarkastukset pysyvät aktiivisina.
|
.warning-external-change-checks-remain-active = __ocs_fmt_0__. Ulkoiset muutostarkastukset pysyvät aktiivisina.
|
||||||
.opening-a-drawing-by-path-is-not-available-in = Piirustuksen avaaminen polun mukaan ei ole käytettävissä selaimessa.
|
.opening-a-drawing-by-path-is-not-available-in = Piirustuksen avaaminen polun mukaan ei ole käytettävissä selaimessa.
|
||||||
|
.pick-the-file-again-to-load-its-contents = Valitse tiedosto uudelleen lataaksesi sen sisällön selaimeen.
|
||||||
|
.adding-folders-is-not-available-in-the-browser = Kansioiden lisääminen ei ole käytettävissä selaimessa. Lisää sen sijaan piirustustiedostoja.
|
||||||
|
.drawing-not-loaded-in-the-browser = Piirustusta ei ole ladattu selaimeen: __ocs_fmt_0__.
|
||||||
|
|
||||||
selection =
|
selection =
|
||||||
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Valitse objektit (__ocs_arg_count__ valittu, Enter käytettäväksi):
|
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Valitse objektit (__ocs_arg_count__ valittu, Enter käytettäväksi):
|
||||||
@@ -3134,6 +3140,7 @@ insert =
|
|||||||
.xref-no-external-references-in-this-drawing = XREF Tässä piirustuksessa ei ole ulkoisia viittauksia.
|
.xref-no-external-references-in-this-drawing = XREF Tässä piirustuksessa ei ole ulkoisia viittauksia.
|
||||||
.xref-reloaded = XREF Ladattu uudelleen "__ocs_fmt_0__"
|
.xref-reloaded = XREF Ladattu uudelleen "__ocs_fmt_0__"
|
||||||
.xref-save-the-drawing-first-to-resolve-relativ = XREF Tallenna piirustus ensin määrittääksesi suhteelliset XREF-polut.
|
.xref-save-the-drawing-first-to-resolve-relativ = XREF Tallenna piirustus ensin määrittääksesi suhteelliset XREF-polut.
|
||||||
|
.xref-unresolved-external-references = XREF Ratkaisemattomat ulkoiset viitteet: __ocs_fmt_0__ — pudota viitatut DWG/DXF-tiedostot sivulle rekisteröitäväksi ja avaa tämä piirros uudelleen tai suorita XRELOAD.
|
||||||
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ objekti(t) kopioitu leikepöydälle (perus __ocs_fmt_1__,__ocs_fmt_2__).
|
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ objekti(t) kopioitu leikepöydälle (perus __ocs_fmt_1__,__ocs_fmt_2__).
|
||||||
.object-s-copied-to-clipboard = __ocs_fmt_0__ objekti(t) kopioitu leikepöydälle.
|
.object-s-copied-to-clipboard = __ocs_fmt_0__ objekti(t) kopioitu leikepöydälle.
|
||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ objekti(t) leikattu leikepöydälle.
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ objekti(t) leikattu leikepöydälle.
|
||||||
|
|||||||
@@ -1981,11 +1981,17 @@ file =
|
|||||||
.warning-corrupt-entities-dropped-parser-junk-b = Avertissement : __ocs_fmt_0__ objet(s) corrompu(s) ignoré(s) (données d’analyse invalides — normales ou nombres incorrects)
|
.warning-corrupt-entities-dropped-parser-junk-b = Avertissement : __ocs_fmt_0__ objet(s) corrompu(s) ignoré(s) (données d’analyse invalides — normales ou nombres incorrects)
|
||||||
.warning-corrupt-xref-entities-dropped = Avertissement : __ocs_fmt_0__ objet(s) de référence externe corrompu(s) ignoré(s)
|
.warning-corrupt-xref-entities-dropped = Avertissement : __ocs_fmt_0__ objet(s) de référence externe corrompu(s) ignoré(s)
|
||||||
.xref-loaded = XREF Charge "__ocs_fmt_0__"
|
.xref-loaded = XREF Charge "__ocs_fmt_0__"
|
||||||
|
.cmd-cannot-build-the-transmittal-zip-e = __ocs_fmt_0__ : impossible de créer le ZIP de transmission (__ocs_fmt_1__).
|
||||||
.cmd-cannot-create-folder-e = __ocs_fmt_0__: ne peut pas créer de dossier (__ocs_fmt_1__).
|
.cmd-cannot-create-folder-e = __ocs_fmt_0__: ne peut pas créer de dossier (__ocs_fmt_1__).
|
||||||
|
.cmd-cannot-package-the-drawing-e = __ocs_fmt_0__ : impossible d'empaqueter le dessin (__ocs_fmt_1__).
|
||||||
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__ : fichier(s) empaqueté(s) __ocs_fmt_1__ dans __ocs_fmt_2__
|
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__ : fichier(s) empaqueté(s) __ocs_fmt_1__ dans __ocs_fmt_2__
|
||||||
|
.cmd-skipped-n-referenced-file-s-not-loaded-in = __ocs_fmt_0__ : __ocs_fmt_1__ fichier(s) référencé(s) non chargé(s) dans cette session ignoré(s) : __ocs_fmt_2__
|
||||||
.error-external-change-checks-remain-active = __ocs_fmt_0__. Les vérifications de changement externe demeurent actives.
|
.error-external-change-checks-remain-active = __ocs_fmt_0__. Les vérifications de changement externe demeurent actives.
|
||||||
.warning-external-change-checks-remain-active = __ocs_fmt_0__. Les vérifications de changement externe demeurent actives.
|
.warning-external-change-checks-remain-active = __ocs_fmt_0__. Les vérifications de changement externe demeurent actives.
|
||||||
.opening-a-drawing-by-path-is-not-available-in = L'ouverture d'un dessin par son chemin n'est pas disponible dans le navigateur.
|
.opening-a-drawing-by-path-is-not-available-in = L'ouverture d'un dessin par son chemin n'est pas disponible dans le navigateur.
|
||||||
|
.pick-the-file-again-to-load-its-contents = Choisissez de nouveau le fichier pour charger son contenu dans le navigateur.
|
||||||
|
.adding-folders-is-not-available-in-the-browser = L'ajout de dossiers n'est pas disponible dans le navigateur. Ajoutez plutôt des fichiers de dessins.
|
||||||
|
.drawing-not-loaded-in-the-browser = Dessin non chargé dans le navigateur : __ocs_fmt_0__.
|
||||||
|
|
||||||
selection =
|
selection =
|
||||||
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Sélectionner les objets (__ocs_arg_count__ sélectionnés, Entrez pour appliquer) :
|
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Sélectionner les objets (__ocs_arg_count__ sélectionnés, Entrez pour appliquer) :
|
||||||
@@ -3135,6 +3141,7 @@ insert =
|
|||||||
.xref-no-external-references-in-this-drawing = XREF Aucune référence externe dans ce dessin.
|
.xref-no-external-references-in-this-drawing = XREF Aucune référence externe dans ce dessin.
|
||||||
.xref-reloaded = XREF Rechargé "__ocs_fmt_0__"
|
.xref-reloaded = XREF Rechargé "__ocs_fmt_0__"
|
||||||
.xref-save-the-drawing-first-to-resolve-relativ = XREF Enregistrez d’abord le dessin afin de résoudre les chemins XREF relatifs.
|
.xref-save-the-drawing-first-to-resolve-relativ = XREF Enregistrez d’abord le dessin afin de résoudre les chemins XREF relatifs.
|
||||||
|
.xref-unresolved-external-references = XREF Références externes non résolues : __ocs_fmt_0__ — déposez les fichiers DWG/DXF concernés sur la page pour les enregistrer, puis rouvrez ce dessin ou exécutez XRELOAD.
|
||||||
.object-s-copied-to-clipboard-base-3-3 = Objets copiés dans le presse-papiers : __ocs_fmt_0__ (point de base __ocs_fmt_1__, __ocs_fmt_2__).
|
.object-s-copied-to-clipboard-base-3-3 = Objets copiés dans le presse-papiers : __ocs_fmt_0__ (point de base __ocs_fmt_1__, __ocs_fmt_2__).
|
||||||
.object-s-copied-to-clipboard = Objets copiés dans le presse-papiers : __ocs_fmt_0__.
|
.object-s-copied-to-clipboard = Objets copiés dans le presse-papiers : __ocs_fmt_0__.
|
||||||
.object-s-cut-to-clipboard = Objets coupés vers le presse-papiers : __ocs_fmt_0__.
|
.object-s-cut-to-clipboard = Objets coupés vers le presse-papiers : __ocs_fmt_0__.
|
||||||
|
|||||||
@@ -1969,11 +1969,17 @@ file =
|
|||||||
.warning-corrupt-entities-dropped-parser-junk-b = चेतावनी: __ocs_fmt_0__ दूषित ऑब्जेक्ट छोड़े गए (अमान्य पार्सर डेटा — गलत नॉर्मल या गिनती)
|
.warning-corrupt-entities-dropped-parser-junk-b = चेतावनी: __ocs_fmt_0__ दूषित ऑब्जेक्ट छोड़े गए (अमान्य पार्सर डेटा — गलत नॉर्मल या गिनती)
|
||||||
.warning-corrupt-xref-entities-dropped = चेतावनी: __ocs_fmt_0__ दूषित xref ऑब्जेक्ट छोड़े गए
|
.warning-corrupt-xref-entities-dropped = चेतावनी: __ocs_fmt_0__ दूषित xref ऑब्जेक्ट छोड़े गए
|
||||||
.xref-loaded = XREF लोड हो गया "__ocs_fmt_0__"
|
.xref-loaded = XREF लोड हो गया "__ocs_fmt_0__"
|
||||||
|
.cmd-cannot-build-the-transmittal-zip-e = __ocs_fmt_0__: ट्रांसमिट ZIP नहीं बनाया जा सकता (__ocs_fmt_1__)।
|
||||||
.cmd-cannot-create-folder-e = __ocs_fmt_0__: फ़ोल्डर नहीं बना सकता (__ocs_fmt_1__).
|
.cmd-cannot-create-folder-e = __ocs_fmt_0__: फ़ोल्डर नहीं बना सकता (__ocs_fmt_1__).
|
||||||
|
.cmd-cannot-package-the-drawing-e = __ocs_fmt_0__: चित्र को पैकेज नहीं किया जा सकता (__ocs_fmt_1__)।
|
||||||
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: __ocs_fmt_1__ फ़ाइल (s) को __ocs_fmt_2__ में पैक किया गया
|
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: __ocs_fmt_1__ फ़ाइल (s) को __ocs_fmt_2__ में पैक किया गया
|
||||||
|
.cmd-skipped-n-referenced-file-s-not-loaded-in = __ocs_fmt_0__: __ocs_fmt_1__ संदर्भित फ़ाइलें छोड़ दी गईं जो इस सत्र में लोड नहीं हुईं: __ocs_fmt_2__
|
||||||
.error-external-change-checks-remain-active = __ocs_fmt_0__. बाहरी परिवर्तन की जाँच सक्रिय रहती है।
|
.error-external-change-checks-remain-active = __ocs_fmt_0__. बाहरी परिवर्तन की जाँच सक्रिय रहती है।
|
||||||
.warning-external-change-checks-remain-active = __ocs_fmt_0__. बाहरी परिवर्तन की जाँच सक्रिय रहती है।
|
.warning-external-change-checks-remain-active = __ocs_fmt_0__. बाहरी परिवर्तन की जाँच सक्रिय रहती है।
|
||||||
.opening-a-drawing-by-path-is-not-available-in = ब्राउज़र में पथ द्वारा ड्रॉइंग खोलना उपलब्ध नहीं है।
|
.opening-a-drawing-by-path-is-not-available-in = ब्राउज़र में पथ द्वारा ड्रॉइंग खोलना उपलब्ध नहीं है।
|
||||||
|
.pick-the-file-again-to-load-its-contents = ब्राउज़र में इसकी सामग्री लोड करने के लिए फ़ाइल को फिर से चुनें।
|
||||||
|
.adding-folders-is-not-available-in-the-browser = ब्राउज़र में फ़ोल्डर जोड़ना उपलब्ध नहीं है। इसके बजाय ड्रॉइंग फ़ाइलें जोड़ें।
|
||||||
|
.drawing-not-loaded-in-the-browser = ड्रॉइंग ब्राउज़र में लोड नहीं है: __ocs_fmt_0__।
|
||||||
|
|
||||||
selection =
|
selection =
|
||||||
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ ऑब्जेक्ट चुनें (__ocs_arg_count__ चयनित, लागू करने के लिए Enter दबाएँ):
|
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ ऑब्जेक्ट चुनें (__ocs_arg_count__ चयनित, लागू करने के लिए Enter दबाएँ):
|
||||||
@@ -3123,6 +3129,7 @@ insert =
|
|||||||
.xref-no-external-references-in-this-drawing = XREF इस ड्रॉइंग में कोई बाहरी संदर्भ नहीं है।
|
.xref-no-external-references-in-this-drawing = XREF इस ड्रॉइंग में कोई बाहरी संदर्भ नहीं है।
|
||||||
.xref-reloaded = XREF “__ocs_fmt_0__” पुनः लोड किया गया
|
.xref-reloaded = XREF “__ocs_fmt_0__” पुनः लोड किया गया
|
||||||
.xref-save-the-drawing-first-to-resolve-relativ = XREF सापेक्ष पथ निर्धारित करने के लिए पहले ड्रॉइंग सहेजें।
|
.xref-save-the-drawing-first-to-resolve-relativ = XREF सापेक्ष पथ निर्धारित करने के लिए पहले ड्रॉइंग सहेजें।
|
||||||
|
.xref-unresolved-external-references = XREF अनसुलझे बाहरी संदर्भ: __ocs_fmt_0__ — उन्हें पंजीकृत करने के लिए संदर्भित DWG/DXF फ़ाइलों को पृष्ठ पर ड्रॉप करें, फिर इस चित्र को फिर से खोलें या XRELOAD चलाएँ।
|
||||||
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ ऑब्जेक्ट (s) क्लिपबोर्ड (बेस __ocs_fmt_1__, __ocs_fmt_2__) को कॉपी किया गया।
|
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ ऑब्जेक्ट (s) क्लिपबोर्ड (बेस __ocs_fmt_1__, __ocs_fmt_2__) को कॉपी किया गया।
|
||||||
.object-s-copied-to-clipboard = __ocs_fmt_0__ ऑब्जेक्ट (s) क्लिपबोर्ड पर कॉपी किया गया।
|
.object-s-copied-to-clipboard = __ocs_fmt_0__ ऑब्जेक्ट (s) क्लिपबोर्ड पर कॉपी किया गया।
|
||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ ऑब्जेक्ट क्लिपबोर्ड पर काटे गए।
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ ऑब्जेक्ट क्लिपबोर्ड पर काटे गए।
|
||||||
|
|||||||
@@ -1980,11 +1980,17 @@ file =
|
|||||||
.warning-corrupt-entities-dropped-parser-junk-b = Figyelmeztetés: A __ocs_fmt_0__ korrupt entitások kimaradtak (szemét elemző – rossz normálértékek/számítások)
|
.warning-corrupt-entities-dropped-parser-junk-b = Figyelmeztetés: A __ocs_fmt_0__ korrupt entitások kimaradtak (szemét elemző – rossz normálértékek/számítások)
|
||||||
.warning-corrupt-xref-entities-dropped = Figyelmeztetés: A __ocs_fmt_0__ korrupt xref entitások lemaradtak
|
.warning-corrupt-xref-entities-dropped = Figyelmeztetés: A __ocs_fmt_0__ korrupt xref entitások lemaradtak
|
||||||
.xref-loaded = XREF betöltve: "__ocs_fmt_0__"
|
.xref-loaded = XREF betöltve: "__ocs_fmt_0__"
|
||||||
|
.cmd-cannot-build-the-transmittal-zip-e = __ocs_fmt_0__: nem hozható létre az átviteli ZIP (__ocs_fmt_1__).
|
||||||
.cmd-cannot-create-folder-e = __ocs_fmt_0__: nem hozható létre mappa (__ocs_fmt_1__).
|
.cmd-cannot-create-folder-e = __ocs_fmt_0__: nem hozható létre mappa (__ocs_fmt_1__).
|
||||||
|
.cmd-cannot-package-the-drawing-e = __ocs_fmt_0__: a rajz nem csomagolható (__ocs_fmt_1__).
|
||||||
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: __ocs_fmt_1__ fájl(ok) a __ocs_fmt_2__ mappába csomagolva
|
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: __ocs_fmt_1__ fájl(ok) a __ocs_fmt_2__ mappába csomagolva
|
||||||
|
.cmd-skipped-n-referenced-file-s-not-loaded-in = __ocs_fmt_0__: __ocs_fmt_1__ hivatkozott fájl kihagyva, amely nem lett betöltve ebben a munkamenetben: __ocs_fmt_2__
|
||||||
.error-external-change-checks-remain-active = __ocs_fmt_0__. A külső változásellenőrzések aktívak maradnak.
|
.error-external-change-checks-remain-active = __ocs_fmt_0__. A külső változásellenőrzések aktívak maradnak.
|
||||||
.warning-external-change-checks-remain-active = __ocs_fmt_0__. A külső változásellenőrzések aktívak maradnak.
|
.warning-external-change-checks-remain-active = __ocs_fmt_0__. A külső változásellenőrzések aktívak maradnak.
|
||||||
.opening-a-drawing-by-path-is-not-available-in = A rajz útvonal szerinti megnyitása nem érhető el a böngészőben.
|
.opening-a-drawing-by-path-is-not-available-in = A rajz útvonal szerinti megnyitása nem érhető el a böngészőben.
|
||||||
|
.pick-the-file-again-to-load-its-contents = Válaszd ki újra a fájlt a tartalmának böngészőbe töltéséhez.
|
||||||
|
.adding-folders-is-not-available-in-the-browser = Mappák hozzáadása nem érhető el a böngészőben. Ehelyett rajzfájlokat adj hozzá.
|
||||||
|
.drawing-not-loaded-in-the-browser = A raj nincs betöltve a böngészőben: __ocs_fmt_0__.
|
||||||
|
|
||||||
selection =
|
selection =
|
||||||
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Objektumok kiválasztása (__ocs_arg_count__ kiválasztva, Enter alkalmazni):
|
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Objektumok kiválasztása (__ocs_arg_count__ kiválasztva, Enter alkalmazni):
|
||||||
@@ -3134,6 +3140,7 @@ insert =
|
|||||||
.xref-no-external-references-in-this-drawing = XREF Nincsenek külső hivatkozások ezen a rajzon.
|
.xref-no-external-references-in-this-drawing = XREF Nincsenek külső hivatkozások ezen a rajzon.
|
||||||
.xref-reloaded = XREF újratöltve "__ocs_fmt_0__"
|
.xref-reloaded = XREF újratöltve "__ocs_fmt_0__"
|
||||||
.xref-save-the-drawing-first-to-resolve-relativ = XREF Először mentse a rajzot a relatív XREF útvonalak feloldásához.
|
.xref-save-the-drawing-first-to-resolve-relativ = XREF Először mentse a rajzot a relatív XREF útvonalak feloldásához.
|
||||||
|
.xref-unresolved-external-references = XREF Megoldatlan külső hivatkozások: __ocs_fmt_0__ — húzza a hivatkozott DWG/DXF-fájlokat az oldalra a regisztrálásukhoz, majd nyissa meg újra ezt a rajzot, vagy futtassa az XRELOAD parancsot.
|
||||||
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ objektum(ok) a vágólapra másolva (alap __ocs_fmt_1__, __ocs_fmt_2__).
|
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ objektum(ok) a vágólapra másolva (alap __ocs_fmt_1__, __ocs_fmt_2__).
|
||||||
.object-s-copied-to-clipboard = __ocs_fmt_0__ objektum(ok) a vágólapra másolva.
|
.object-s-copied-to-clipboard = __ocs_fmt_0__ objektum(ok) a vágólapra másolva.
|
||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ objektum(ok) vágólapra vágva.
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ objektum(ok) vágólapra vágva.
|
||||||
|
|||||||
@@ -1980,11 +1980,17 @@ file =
|
|||||||
.warning-corrupt-entities-dropped-parser-junk-b = Avviso: entità corrotte __ocs_fmt_0__ eliminate (spazzatura del parser: valori normali/conteggi errati)
|
.warning-corrupt-entities-dropped-parser-junk-b = Avviso: entità corrotte __ocs_fmt_0__ eliminate (spazzatura del parser: valori normali/conteggi errati)
|
||||||
.warning-corrupt-xref-entities-dropped = Avviso: entità xrif corrotte __ocs_fmt_0__ eliminate
|
.warning-corrupt-xref-entities-dropped = Avviso: entità xrif corrotte __ocs_fmt_0__ eliminate
|
||||||
.xref-loaded = XREF Caricato "__ocs_fmt_0__"
|
.xref-loaded = XREF Caricato "__ocs_fmt_0__"
|
||||||
|
.cmd-cannot-build-the-transmittal-zip-e = __ocs_fmt_0__: impossibile creare il ZIP di trasmissione (__ocs_fmt_1__).
|
||||||
.cmd-cannot-create-folder-e = __ocs_fmt_0__: impossibile creare la cartella (__ocs_fmt_1__).
|
.cmd-cannot-create-folder-e = __ocs_fmt_0__: impossibile creare la cartella (__ocs_fmt_1__).
|
||||||
|
.cmd-cannot-package-the-drawing-e = __ocs_fmt_0__: impossibile pacchettizzare il disegno (__ocs_fmt_1__).
|
||||||
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: file __ocs_fmt_1__ compressi in __ocs_fmt_2__
|
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: file __ocs_fmt_1__ compressi in __ocs_fmt_2__
|
||||||
|
.cmd-skipped-n-referenced-file-s-not-loaded-in = __ocs_fmt_0__: saltati __ocs_fmt_1__ file riferiti non caricati in questa sessione: __ocs_fmt_2__
|
||||||
.error-external-change-checks-remain-active = __ocs_fmt_0__. I controlli di modifica esterna rimangono attivi.
|
.error-external-change-checks-remain-active = __ocs_fmt_0__. I controlli di modifica esterna rimangono attivi.
|
||||||
.warning-external-change-checks-remain-active = __ocs_fmt_0__. I controlli di modifica esterna rimangono attivi.
|
.warning-external-change-checks-remain-active = __ocs_fmt_0__. I controlli di modifica esterna rimangono attivi.
|
||||||
.opening-a-drawing-by-path-is-not-available-in = L'apertura di un disegno per percorso non è disponibile nel browser.
|
.opening-a-drawing-by-path-is-not-available-in = L'apertura di un disegno per percorso non è disponibile nel browser.
|
||||||
|
.pick-the-file-again-to-load-its-contents = Scegli di nuovo il file per caricarne il contenuto nel browser.
|
||||||
|
.adding-folders-is-not-available-in-the-browser = L'aggiunta di cartelle non è disponibile nel browser. Aggiungi invece file di disegni.
|
||||||
|
.drawing-not-loaded-in-the-browser = Disegno non caricato nel browser: __ocs_fmt_0__.
|
||||||
|
|
||||||
selection =
|
selection =
|
||||||
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Seleziona oggetti (__ocs_arg_count__ selezionato, Enter da applicare):
|
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Seleziona oggetti (__ocs_arg_count__ selezionato, Enter da applicare):
|
||||||
@@ -3134,6 +3140,7 @@ insert =
|
|||||||
.xref-no-external-references-in-this-drawing = XREF Nessun riferimento esterno in questo disegno.
|
.xref-no-external-references-in-this-drawing = XREF Nessun riferimento esterno in questo disegno.
|
||||||
.xref-reloaded = XREF Ricaricato "__ocs_fmt_0__"
|
.xref-reloaded = XREF Ricaricato "__ocs_fmt_0__"
|
||||||
.xref-save-the-drawing-first-to-resolve-relativ = XREF Salvare prima il disegno per risolvere i percorsi XREF relativi.
|
.xref-save-the-drawing-first-to-resolve-relativ = XREF Salvare prima il disegno per risolvere i percorsi XREF relativi.
|
||||||
|
.xref-unresolved-external-references = XREF Riferimenti esterni non risolti: __ocs_fmt_0__ — trascina sulla pagina i file DWG/DXF di riferimento per registrarli, poi riapri questo disegno o esegui XRELOAD.
|
||||||
.object-s-copied-to-clipboard-base-3-3 = Oggetto/i __ocs_fmt_0__ copiati negli appunti (base __ocs_fmt_1__,__ocs_fmt_2__).
|
.object-s-copied-to-clipboard-base-3-3 = Oggetto/i __ocs_fmt_0__ copiati negli appunti (base __ocs_fmt_1__,__ocs_fmt_2__).
|
||||||
.object-s-copied-to-clipboard = __ocs_fmt_0__ oggetto/i copiati negli appunti.
|
.object-s-copied-to-clipboard = __ocs_fmt_0__ oggetto/i copiati negli appunti.
|
||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ oggetto/i tagliato negli appunti.
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ oggetto/i tagliato negli appunti.
|
||||||
|
|||||||
@@ -1971,11 +1971,17 @@ file =
|
|||||||
.warning-corrupt-entities-dropped-parser-junk-b = 警告: __ocs_fmt_0__ 個の破損した図形を除外しました(パーサーの不正データ — 法線 / 数の異常)
|
.warning-corrupt-entities-dropped-parser-junk-b = 警告: __ocs_fmt_0__ 個の破損した図形を除外しました(パーサーの不正データ — 法線 / 数の異常)
|
||||||
.warning-corrupt-xref-entities-dropped = 警告: __ocs_fmt_0__ 個の破損した外部参照図形を除外しました
|
.warning-corrupt-xref-entities-dropped = 警告: __ocs_fmt_0__ 個の破損した外部参照図形を除外しました
|
||||||
.xref-loaded = XREF 「__ocs_fmt_0__」をロードしました
|
.xref-loaded = XREF 「__ocs_fmt_0__」をロードしました
|
||||||
|
.cmd-cannot-build-the-transmittal-zip-e = __ocs_fmt_0__: 転送 ZIP を作成できません(__ocs_fmt_1__)。
|
||||||
.cmd-cannot-create-folder-e = __ocs_fmt_0__: フォルダーを作成できません(__ocs_fmt_1__)。
|
.cmd-cannot-create-folder-e = __ocs_fmt_0__: フォルダーを作成できません(__ocs_fmt_1__)。
|
||||||
|
.cmd-cannot-package-the-drawing-e = __ocs_fmt_0__: 図面をパッケージ化できません(__ocs_fmt_1__)。
|
||||||
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: __ocs_fmt_1__ 個のファイルを __ocs_fmt_2__ にパッケージ化しました
|
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: __ocs_fmt_1__ 個のファイルを __ocs_fmt_2__ にパッケージ化しました
|
||||||
|
.cmd-skipped-n-referenced-file-s-not-loaded-in = __ocs_fmt_0__: このセッションで読み込まれていない参照ファイル __ocs_fmt_1__ 個をスキップしました: __ocs_fmt_2__
|
||||||
.error-external-change-checks-remain-active = __ocs_fmt_0__。外部変更の確認は引き続き有効です。
|
.error-external-change-checks-remain-active = __ocs_fmt_0__。外部変更の確認は引き続き有効です。
|
||||||
.warning-external-change-checks-remain-active = __ocs_fmt_0__。外部変更の確認は引き続き有効です。
|
.warning-external-change-checks-remain-active = __ocs_fmt_0__。外部変更の確認は引き続き有効です。
|
||||||
.opening-a-drawing-by-path-is-not-available-in = ブラウザーではパスを指定して図面を開くことはできません。
|
.opening-a-drawing-by-path-is-not-available-in = ブラウザーではパスを指定して図面を開くことはできません。
|
||||||
|
.pick-the-file-again-to-load-its-contents = ブラウザーに内容を読み込むには、ファイルをもう一度選択してください。
|
||||||
|
.adding-folders-is-not-available-in-the-browser = ブラウザーではフォルダーを追加できません。代わりに図面ファイルを追加してください。
|
||||||
|
.drawing-not-loaded-in-the-browser = 図面がブラウザーに読み込まれていません: __ocs_fmt_0__。
|
||||||
|
|
||||||
selection =
|
selection =
|
||||||
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ オブジェクトを選択(__ocs_arg_count__ 個選択、Enter で適用):
|
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ オブジェクトを選択(__ocs_arg_count__ 個選択、Enter で適用):
|
||||||
@@ -3123,6 +3129,7 @@ insert =
|
|||||||
.xref-no-external-references-in-this-drawing = XREF この図面には外部参照がありません。
|
.xref-no-external-references-in-this-drawing = XREF この図面には外部参照がありません。
|
||||||
.xref-reloaded = XREF 「__ocs_fmt_0__」を再ロードしました
|
.xref-reloaded = XREF 「__ocs_fmt_0__」を再ロードしました
|
||||||
.xref-save-the-drawing-first-to-resolve-relativ = XREF 相対 XREF パスを解決するには、先に図面を保存してください。
|
.xref-save-the-drawing-first-to-resolve-relativ = XREF 相対 XREF パスを解決するには、先に図面を保存してください。
|
||||||
|
.xref-unresolved-external-references = XREF 未解決の外部参照: __ocs_fmt_0__ — 参照先の DWG/DXF ファイルをページ上にドロップして登録し、この図面を開き直すか XRELOAD を実行してください。
|
||||||
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ 個のオブジェクトをクリップボードにコピーしました(基点 __ocs_fmt_1__,__ocs_fmt_2__)。
|
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ 個のオブジェクトをクリップボードにコピーしました(基点 __ocs_fmt_1__,__ocs_fmt_2__)。
|
||||||
.object-s-copied-to-clipboard = __ocs_fmt_0__ 個のオブジェクトをクリップボードにコピーしました。
|
.object-s-copied-to-clipboard = __ocs_fmt_0__ 個のオブジェクトをクリップボードにコピーしました。
|
||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ 個のオブジェクトを切り取ってクリップボードに保存しました。
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ 個のオブジェクトを切り取ってクリップボードに保存しました。
|
||||||
|
|||||||
@@ -1980,11 +1980,17 @@ file =
|
|||||||
.warning-corrupt-entities-dropped-parser-junk-b = 경고: __ocs_fmt_0__ 손상된 엔터티가 삭제됨(파서 정크 - 잘못된 노멀/카운트)
|
.warning-corrupt-entities-dropped-parser-junk-b = 경고: __ocs_fmt_0__ 손상된 엔터티가 삭제됨(파서 정크 - 잘못된 노멀/카운트)
|
||||||
.warning-corrupt-xref-entities-dropped = 경고: __ocs_fmt_0__ 손상된 외부 참조 엔티티가 삭제되었습니다.
|
.warning-corrupt-xref-entities-dropped = 경고: __ocs_fmt_0__ 손상된 외부 참조 엔티티가 삭제되었습니다.
|
||||||
.xref-loaded = XREF가 "__ocs_fmt_0__"를 로드했습니다.
|
.xref-loaded = XREF가 "__ocs_fmt_0__"를 로드했습니다.
|
||||||
|
.cmd-cannot-build-the-transmittal-zip-e = __ocs_fmt_0__: 전송 ZIP을 만들 수 없습니다(__ocs_fmt_1__).
|
||||||
.cmd-cannot-create-folder-e = __ocs_fmt_0__: 폴더(__ocs_fmt_1__)를 생성할 수 없습니다.
|
.cmd-cannot-create-folder-e = __ocs_fmt_0__: 폴더(__ocs_fmt_1__)를 생성할 수 없습니다.
|
||||||
|
.cmd-cannot-package-the-drawing-e = __ocs_fmt_0__: 도면을 패키지할 수 없습니다(__ocs_fmt_1__).
|
||||||
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: __ocs_fmt_1__ 파일을 __ocs_fmt_2__로 패키징했습니다.
|
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: __ocs_fmt_1__ 파일을 __ocs_fmt_2__로 패키징했습니다.
|
||||||
|
.cmd-skipped-n-referenced-file-s-not-loaded-in = __ocs_fmt_0__: 이 세션에서 로드되지 않은 참조 파일 __ocs_fmt_1__개를 건너뛰었습니다: __ocs_fmt_2__
|
||||||
.error-external-change-checks-remain-active = __ocs_fmt_0__. 외부 변경 검사는 활성 상태로 유지됩니다.
|
.error-external-change-checks-remain-active = __ocs_fmt_0__. 외부 변경 검사는 활성 상태로 유지됩니다.
|
||||||
.warning-external-change-checks-remain-active = __ocs_fmt_0__. 외부 변경 검사는 활성 상태로 유지됩니다.
|
.warning-external-change-checks-remain-active = __ocs_fmt_0__. 외부 변경 검사는 활성 상태로 유지됩니다.
|
||||||
.opening-a-drawing-by-path-is-not-available-in = 브라우저에서는 경로로 도면을 열 수 없습니다.
|
.opening-a-drawing-by-path-is-not-available-in = 브라우저에서는 경로로 도면을 열 수 없습니다.
|
||||||
|
.pick-the-file-again-to-load-its-contents = 콘텐츠를 브라우저에 로드하려면 파일을 다시 선택하세요.
|
||||||
|
.adding-folders-is-not-available-in-the-browser = 브라우저에서는 폴더를 추가할 수 없습니다. 대신 도면 파일을 추가하세요.
|
||||||
|
.drawing-not-loaded-in-the-browser = 도면이 브라우저에 로드되지 않았습니다: __ocs_fmt_0__.
|
||||||
|
|
||||||
selection =
|
selection =
|
||||||
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ 객체 선택(__ocs_arg_count__ 선택, Enter 적용):
|
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ 객체 선택(__ocs_arg_count__ 선택, Enter 적용):
|
||||||
@@ -3134,6 +3140,7 @@ insert =
|
|||||||
.xref-no-external-references-in-this-drawing = XREF 이 도면에는 외부 참조가 없습니다.
|
.xref-no-external-references-in-this-drawing = XREF 이 도면에는 외부 참조가 없습니다.
|
||||||
.xref-reloaded = XREF가 "__ocs_fmt_0__"를 다시 로드했습니다.
|
.xref-reloaded = XREF가 "__ocs_fmt_0__"를 다시 로드했습니다.
|
||||||
.xref-save-the-drawing-first-to-resolve-relativ = XREF 상대 XREF 경로를 확인하려면 먼저 도면을 저장하십시오.
|
.xref-save-the-drawing-first-to-resolve-relativ = XREF 상대 XREF 경로를 확인하려면 먼저 도면을 저장하십시오.
|
||||||
|
.xref-unresolved-external-references = XREF 확인되지 않은 외부 참조: __ocs_fmt_0__ — 참조된 DWG/DXF 파일을 페이지에 놓아 등록한 다음 이 도면을 다시 열거나 XRELOAD를 실행하세요.
|
||||||
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ 개체가 클립보드에 복사되었습니다(기본 __ocs_fmt_1__,__ocs_fmt_2__).
|
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ 개체가 클립보드에 복사되었습니다(기본 __ocs_fmt_1__,__ocs_fmt_2__).
|
||||||
.object-s-copied-to-clipboard = __ocs_fmt_0__ 개체가 클립보드에 복사되었습니다.
|
.object-s-copied-to-clipboard = __ocs_fmt_0__ 개체가 클립보드에 복사되었습니다.
|
||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ 개체를 클립보드로 잘라냈습니다.
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ 개체를 클립보드로 잘라냈습니다.
|
||||||
|
|||||||
@@ -1981,11 +1981,17 @@ file =
|
|||||||
.warning-corrupt-entities-dropped-parser-junk-b = Waarschuwing: __ocs_fmt_0__ beschadigde objecten overgeslagen (ongeldige parsergegevens — foutieve normalen of aantallen)
|
.warning-corrupt-entities-dropped-parser-junk-b = Waarschuwing: __ocs_fmt_0__ beschadigde objecten overgeslagen (ongeldige parsergegevens — foutieve normalen of aantallen)
|
||||||
.warning-corrupt-xref-entities-dropped = Waarschuwing: __ocs_fmt_0__ beschadigde xref-objecten overgeslagen
|
.warning-corrupt-xref-entities-dropped = Waarschuwing: __ocs_fmt_0__ beschadigde xref-objecten overgeslagen
|
||||||
.xref-loaded = XREF geladen "__ocs_fmt_0__"
|
.xref-loaded = XREF geladen "__ocs_fmt_0__"
|
||||||
|
.cmd-cannot-build-the-transmittal-zip-e = __ocs_fmt_0__: kan de transmissie-ZIP niet maken (__ocs_fmt_1__).
|
||||||
.cmd-cannot-create-folder-e = __ocs_fmt_0__: kan map (__ocs_fmt_1__) niet aanmaken.
|
.cmd-cannot-create-folder-e = __ocs_fmt_0__: kan map (__ocs_fmt_1__) niet aanmaken.
|
||||||
|
.cmd-cannot-package-the-drawing-e = __ocs_fmt_0__: kan de tekening niet inpakken (__ocs_fmt_1__).
|
||||||
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: verpakt __ocs_fmt_1__ bestand(s) in __ocs_fmt_2__
|
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: verpakt __ocs_fmt_1__ bestand(s) in __ocs_fmt_2__
|
||||||
|
.cmd-skipped-n-referenced-file-s-not-loaded-in = __ocs_fmt_0__: __ocs_fmt_1__ gerefereerde bestand(en) overgeslagen die niet in deze sessie zijn geladen: __ocs_fmt_2__
|
||||||
.error-external-change-checks-remain-active = __ocs_fmt_0__. Externe wijzigingen blijven actief.
|
.error-external-change-checks-remain-active = __ocs_fmt_0__. Externe wijzigingen blijven actief.
|
||||||
.warning-external-change-checks-remain-active = __ocs_fmt_0__. Externe wijzigingen blijven actief.
|
.warning-external-change-checks-remain-active = __ocs_fmt_0__. Externe wijzigingen blijven actief.
|
||||||
.opening-a-drawing-by-path-is-not-available-in = Een tekening openen via een pad is niet beschikbaar in de browser.
|
.opening-a-drawing-by-path-is-not-available-in = Een tekening openen via een pad is niet beschikbaar in de browser.
|
||||||
|
.pick-the-file-again-to-load-its-contents = Kies het bestand opnieuw om de inhoud ervan in de browser te laden.
|
||||||
|
.adding-folders-is-not-available-in-the-browser = Mappen toevoegen is niet beschikbaar in de browser. Voeg in plaats daarvan tekeningbestanden toe.
|
||||||
|
.drawing-not-loaded-in-the-browser = Tekening is niet geladen in de browser: __ocs_fmt_0__.
|
||||||
|
|
||||||
selection =
|
selection =
|
||||||
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Selecteer objecten (__ocs_arg_count__ geselecteerd, Voer in om toe te passen):
|
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Selecteer objecten (__ocs_arg_count__ geselecteerd, Voer in om toe te passen):
|
||||||
@@ -3133,6 +3139,7 @@ insert =
|
|||||||
.xref-no-external-references-in-this-drawing = XREF Geen externe referenties in deze tekening.
|
.xref-no-external-references-in-this-drawing = XREF Geen externe referenties in deze tekening.
|
||||||
.xref-reloaded = XREF Herladen "__ocs_fmt_0__"
|
.xref-reloaded = XREF Herladen "__ocs_fmt_0__"
|
||||||
.xref-save-the-drawing-first-to-resolve-relativ = XREF Sla de tekening eerst op om relatieve XREF paden op te lossen.
|
.xref-save-the-drawing-first-to-resolve-relativ = XREF Sla de tekening eerst op om relatieve XREF paden op te lossen.
|
||||||
|
.xref-unresolved-external-references = XREF Onopgeloste externe referenties: __ocs_fmt_0__ — sleep de bedoelde DWG/DXF-bestanden naar de pagina om ze te registreren en open deze tekening daarna opnieuw of voer XRELOAD uit.
|
||||||
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ object(en) naar het klembord gekopieerd (basispunt __ocs_fmt_1__, __ocs_fmt_2__).
|
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ object(en) naar het klembord gekopieerd (basispunt __ocs_fmt_1__, __ocs_fmt_2__).
|
||||||
.object-s-copied-to-clipboard = __ocs_fmt_0__ object(en) naar het klembord gekopieerd.
|
.object-s-copied-to-clipboard = __ocs_fmt_0__ object(en) naar het klembord gekopieerd.
|
||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ object(en) naar het klembord geknipt.
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ object(en) naar het klembord geknipt.
|
||||||
|
|||||||
@@ -1980,11 +1980,17 @@ file =
|
|||||||
.warning-corrupt-entities-dropped-parser-junk-b = Ostrzeżenie: usunięte zostały uszkodzone jednostki __ocs_fmt_0__ (śmieci parsera — złe wartości normalne/liczby)
|
.warning-corrupt-entities-dropped-parser-junk-b = Ostrzeżenie: usunięte zostały uszkodzone jednostki __ocs_fmt_0__ (śmieci parsera — złe wartości normalne/liczby)
|
||||||
.warning-corrupt-xref-entities-dropped = Ostrzeżenie: usunięte zostały uszkodzone elementy odnośników zewnętrznych __ocs_fmt_0__
|
.warning-corrupt-xref-entities-dropped = Ostrzeżenie: usunięte zostały uszkodzone elementy odnośników zewnętrznych __ocs_fmt_0__
|
||||||
.xref-loaded = XREF Załadowano „__ocs_fmt_0__”
|
.xref-loaded = XREF Załadowano „__ocs_fmt_0__”
|
||||||
|
.cmd-cannot-build-the-transmittal-zip-e = __ocs_fmt_0__: nie można utworzyć archiwum ZIP transmittalu (__ocs_fmt_1__).
|
||||||
.cmd-cannot-create-folder-e = __ocs_fmt_0__: nie można utworzyć folderu (__ocs_fmt_1__).
|
.cmd-cannot-create-folder-e = __ocs_fmt_0__: nie można utworzyć folderu (__ocs_fmt_1__).
|
||||||
|
.cmd-cannot-package-the-drawing-e = __ocs_fmt_0__: nie można spakować rysunku (__ocs_fmt_1__).
|
||||||
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: spakowane pliki __ocs_fmt_1__ do __ocs_fmt_2__
|
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: spakowane pliki __ocs_fmt_1__ do __ocs_fmt_2__
|
||||||
|
.cmd-skipped-n-referenced-file-s-not-loaded-in = __ocs_fmt_0__: pominięto __ocs_fmt_1__ przywołanych plików, niezaładowanych w tej sesji: __ocs_fmt_2__
|
||||||
.error-external-change-checks-remain-active = __ocs_fmt_0__. Kontrole zmian zewnętrznych pozostają aktywne.
|
.error-external-change-checks-remain-active = __ocs_fmt_0__. Kontrole zmian zewnętrznych pozostają aktywne.
|
||||||
.warning-external-change-checks-remain-active = __ocs_fmt_0__. Kontrole zmian zewnętrznych pozostają aktywne.
|
.warning-external-change-checks-remain-active = __ocs_fmt_0__. Kontrole zmian zewnętrznych pozostają aktywne.
|
||||||
.opening-a-drawing-by-path-is-not-available-in = Otwieranie rysunku po ścieżce nie jest dostępne w przeglądarce.
|
.opening-a-drawing-by-path-is-not-available-in = Otwieranie rysunku po ścieżce nie jest dostępne w przeglądarce.
|
||||||
|
.pick-the-file-again-to-load-its-contents = Wybierz plik ponownie, aby załadować jego zawartość do przeglądarki.
|
||||||
|
.adding-folders-is-not-available-in-the-browser = Dodawanie folderów nie jest dostępne w przeglądarce. Zamiast tego dodaj pliki rysunków.
|
||||||
|
.drawing-not-loaded-in-the-browser = Rysunek nie został załadowany w przeglądarce: __ocs_fmt_0__.
|
||||||
|
|
||||||
selection =
|
selection =
|
||||||
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Wybierz obiekty (wybrano __ocs_arg_count__, zastosowano Enter):
|
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Wybierz obiekty (wybrano __ocs_arg_count__, zastosowano Enter):
|
||||||
@@ -3134,6 +3140,7 @@ insert =
|
|||||||
.xref-no-external-references-in-this-drawing = XREF Brak zewnętrznych odniesień na tym rysunku.
|
.xref-no-external-references-in-this-drawing = XREF Brak zewnętrznych odniesień na tym rysunku.
|
||||||
.xref-reloaded = XREF Załadowano ponownie „__ocs_fmt_0__”
|
.xref-reloaded = XREF Załadowano ponownie „__ocs_fmt_0__”
|
||||||
.xref-save-the-drawing-first-to-resolve-relativ = XREF Zapisz najpierw rysunek, aby ustalić względne ścieżki XREF.
|
.xref-save-the-drawing-first-to-resolve-relativ = XREF Zapisz najpierw rysunek, aby ustalić względne ścieżki XREF.
|
||||||
|
.xref-unresolved-external-references = XREF Nierozwiązane odniesienia zewnętrzne: __ocs_fmt_0__ — upuść przywoływane pliki DWG/DXF na stronę, aby je zarejestrować, a następnie otwórz ponownie ten rysunek lub uruchom XRELOAD.
|
||||||
.object-s-copied-to-clipboard-base-3-3 = Obiekt(y) __ocs_fmt_0__ skopiowano do schowka (bazowe __ocs_fmt_1__,__ocs_fmt_2__).
|
.object-s-copied-to-clipboard-base-3-3 = Obiekt(y) __ocs_fmt_0__ skopiowano do schowka (bazowe __ocs_fmt_1__,__ocs_fmt_2__).
|
||||||
.object-s-copied-to-clipboard = Obiekt(y) __ocs_fmt_0__ skopiowano do schowka.
|
.object-s-copied-to-clipboard = Obiekt(y) __ocs_fmt_0__ skopiowano do schowka.
|
||||||
.object-s-cut-to-clipboard = Obiekt(y) __ocs_fmt_0__ wycięty do schowka.
|
.object-s-cut-to-clipboard = Obiekt(y) __ocs_fmt_0__ wycięty do schowka.
|
||||||
|
|||||||
@@ -1981,11 +1981,17 @@ file =
|
|||||||
.warning-corrupt-entities-dropped-parser-junk-b = Aviso: __ocs_fmt_0__ entidades corrompidas descartadas (dados inválidos do analisador — normais/contagens incorretas)
|
.warning-corrupt-entities-dropped-parser-junk-b = Aviso: __ocs_fmt_0__ entidades corrompidas descartadas (dados inválidos do analisador — normais/contagens incorretas)
|
||||||
.warning-corrupt-xref-entities-dropped = Aviso: __ocs_fmt_0__ entidades de referência externa corrompidas descartadas
|
.warning-corrupt-xref-entities-dropped = Aviso: __ocs_fmt_0__ entidades de referência externa corrompidas descartadas
|
||||||
.xref-loaded = XREF “__ocs_fmt_0__” carregada
|
.xref-loaded = XREF “__ocs_fmt_0__” carregada
|
||||||
|
.cmd-cannot-build-the-transmittal-zip-e = __ocs_fmt_0__: não foi possível criar o ZIP de transmissão (__ocs_fmt_1__).
|
||||||
.cmd-cannot-create-folder-e = __ocs_fmt_0__: não é possível criar a pasta (__ocs_fmt_1__).
|
.cmd-cannot-create-folder-e = __ocs_fmt_0__: não é possível criar a pasta (__ocs_fmt_1__).
|
||||||
|
.cmd-cannot-package-the-drawing-e = __ocs_fmt_0__: não foi possível empacotar o desenho (__ocs_fmt_1__).
|
||||||
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: __ocs_fmt_1__ arquivos empacotados em __ocs_fmt_2__
|
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: __ocs_fmt_1__ arquivos empacotados em __ocs_fmt_2__
|
||||||
|
.cmd-skipped-n-referenced-file-s-not-loaded-in = __ocs_fmt_0__: __ocs_fmt_1__ arquivo(s) referenciado(s) não carregado(s) nesta sessão foram ignorados: __ocs_fmt_2__
|
||||||
.error-external-change-checks-remain-active = __ocs_fmt_0__. As verificações de alterações externas permanecem ativas.
|
.error-external-change-checks-remain-active = __ocs_fmt_0__. As verificações de alterações externas permanecem ativas.
|
||||||
.warning-external-change-checks-remain-active = __ocs_fmt_0__. As verificações de alterações externas permanecem ativas.
|
.warning-external-change-checks-remain-active = __ocs_fmt_0__. As verificações de alterações externas permanecem ativas.
|
||||||
.opening-a-drawing-by-path-is-not-available-in = Abrir um desenho pelo caminho não está disponível no navegador.
|
.opening-a-drawing-by-path-is-not-available-in = Abrir um desenho pelo caminho não está disponível no navegador.
|
||||||
|
.pick-the-file-again-to-load-its-contents = Escolha o arquivo novamente para carregar o conteúdo dele no navegador.
|
||||||
|
.adding-folders-is-not-available-in-the-browser = Adicionar pastas não está disponível no navegador. Em vez disso, adicione arquivos de desenhos.
|
||||||
|
.drawing-not-loaded-in-the-browser = Desenho não carregado no navegador: __ocs_fmt_0__.
|
||||||
|
|
||||||
selection =
|
selection =
|
||||||
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Selecione objetos (__ocs_arg_count__ selecionados, Enter para aplicar):
|
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Selecione objetos (__ocs_arg_count__ selecionados, Enter para aplicar):
|
||||||
@@ -3135,6 +3141,7 @@ insert =
|
|||||||
.xref-no-external-references-in-this-drawing = XREF Nenhuma referência externa neste desenho.
|
.xref-no-external-references-in-this-drawing = XREF Nenhuma referência externa neste desenho.
|
||||||
.xref-reloaded = XREF “__ocs_fmt_0__” recarregada
|
.xref-reloaded = XREF “__ocs_fmt_0__” recarregada
|
||||||
.xref-save-the-drawing-first-to-resolve-relativ = XREF Salve primeiro o desenho para resolver os caminhos relativos das referências externas.
|
.xref-save-the-drawing-first-to-resolve-relativ = XREF Salve primeiro o desenho para resolver os caminhos relativos das referências externas.
|
||||||
|
.xref-unresolved-external-references = XREF Referências externas não resolvidas: __ocs_fmt_0__ — solte os arquivos DWG/DXF referenciados na página para registrá-los e depois reabra este desenho ou execute XRELOAD.
|
||||||
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ objetos copiados para a área de transferência (base __ocs_fmt_1__,__ocs_fmt_2__).
|
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ objetos copiados para a área de transferência (base __ocs_fmt_1__,__ocs_fmt_2__).
|
||||||
.object-s-copied-to-clipboard = __ocs_fmt_0__ objetos copiados para a área de transferência.
|
.object-s-copied-to-clipboard = __ocs_fmt_0__ objetos copiados para a área de transferência.
|
||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ objetos recortados para a área de transferência.
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ objetos recortados para a área de transferência.
|
||||||
|
|||||||
@@ -1984,11 +1984,17 @@ file =
|
|||||||
.warning-corrupt-entities-dropped-parser-junk-b = Предупреждение: повреждённые объекты пропущены: __ocs_fmt_0__ (ошибочные данные анализатора — неверные нормали или количества)
|
.warning-corrupt-entities-dropped-parser-junk-b = Предупреждение: повреждённые объекты пропущены: __ocs_fmt_0__ (ошибочные данные анализатора — неверные нормали или количества)
|
||||||
.warning-corrupt-xref-entities-dropped = Предупреждение: повреждённые объекты внешней ссылки пропущены: __ocs_fmt_0__
|
.warning-corrupt-xref-entities-dropped = Предупреждение: повреждённые объекты внешней ссылки пропущены: __ocs_fmt_0__
|
||||||
.xref-loaded = XREF Загруженный "__ocs_fmt_0__"
|
.xref-loaded = XREF Загруженный "__ocs_fmt_0__"
|
||||||
|
.cmd-cannot-build-the-transmittal-zip-e = __ocs_fmt_0__: не удалось создать ZIP-пакет передачи (__ocs_fmt_1__).
|
||||||
.cmd-cannot-create-folder-e = __ocs_fmt_0__: не может создавать папку (__ocs_fmt_1__).
|
.cmd-cannot-create-folder-e = __ocs_fmt_0__: не может создавать папку (__ocs_fmt_1__).
|
||||||
|
.cmd-cannot-package-the-drawing-e = __ocs_fmt_0__: не удалось упаковать чертёж (__ocs_fmt_1__).
|
||||||
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: упакованный файл(ы) __ocs_fmt_1__ в __ocs_fmt_2__
|
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: упакованный файл(ы) __ocs_fmt_1__ в __ocs_fmt_2__
|
||||||
|
.cmd-skipped-n-referenced-file-s-not-loaded-in = __ocs_fmt_0__: пропущено __ocs_fmt_1__ связанных файлов, не загруженных в этом сеансе: __ocs_fmt_2__
|
||||||
.error-external-change-checks-remain-active = __ocs_fmt_0__ Проверки внешних изменений остаются активными.
|
.error-external-change-checks-remain-active = __ocs_fmt_0__ Проверки внешних изменений остаются активными.
|
||||||
.warning-external-change-checks-remain-active = __ocs_fmt_0__ Проверки внешних изменений остаются активными.
|
.warning-external-change-checks-remain-active = __ocs_fmt_0__ Проверки внешних изменений остаются активными.
|
||||||
.opening-a-drawing-by-path-is-not-available-in = Открытие чертежа по пути недоступно в браузере.
|
.opening-a-drawing-by-path-is-not-available-in = Открытие чертежа по пути недоступно в браузере.
|
||||||
|
.pick-the-file-again-to-load-its-contents = Выберите файл ещё раз, чтобы загрузить его содержимое в браузер.
|
||||||
|
.adding-folders-is-not-available-in-the-browser = Добавление папок в браузере недоступно. Вместо этого добавьте файлы чертежей.
|
||||||
|
.drawing-not-loaded-in-the-browser = Чертёж не загружен в браузере: __ocs_fmt_0__.
|
||||||
|
|
||||||
selection =
|
selection =
|
||||||
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Выберите объекты (__ocs_arg_count__ Выберите, Введите для применения):
|
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Выберите объекты (__ocs_arg_count__ Выберите, Введите для применения):
|
||||||
@@ -3136,6 +3142,7 @@ insert =
|
|||||||
.xref-no-external-references-in-this-drawing = XREF В этом чертеже нет внешних ссылок.
|
.xref-no-external-references-in-this-drawing = XREF В этом чертеже нет внешних ссылок.
|
||||||
.xref-reloaded = XREF Перезагрузка "__ocs_fmt_0__"
|
.xref-reloaded = XREF Перезагрузка "__ocs_fmt_0__"
|
||||||
.xref-save-the-drawing-first-to-resolve-relativ = XREF Сначала сохраните чертёж, чтобы определить относительные пути внешних ссылок.
|
.xref-save-the-drawing-first-to-resolve-relativ = XREF Сначала сохраните чертёж, чтобы определить относительные пути внешних ссылок.
|
||||||
|
.xref-unresolved-external-references = XREF Неразрешённые внешние ссылки: __ocs_fmt_0__ — перетащите указанные файлы DWG/DXF на страницу, чтобы зарегистрировать их, затем снова откройте этот чертёж или выполните XRELOAD.
|
||||||
.object-s-copied-to-clipboard-base-3-3 = В буфер обмена скопировано объектов: __ocs_fmt_0__ (базовая точка: __ocs_fmt_1__, __ocs_fmt_2__).
|
.object-s-copied-to-clipboard-base-3-3 = В буфер обмена скопировано объектов: __ocs_fmt_0__ (базовая точка: __ocs_fmt_1__, __ocs_fmt_2__).
|
||||||
.object-s-copied-to-clipboard = В буфер обмена скопировано объектов: __ocs_fmt_0__.
|
.object-s-copied-to-clipboard = В буфер обмена скопировано объектов: __ocs_fmt_0__.
|
||||||
.object-s-cut-to-clipboard = В буфер обмена вырезано объектов: __ocs_fmt_0__.
|
.object-s-cut-to-clipboard = В буфер обмена вырезано объектов: __ocs_fmt_0__.
|
||||||
|
|||||||
@@ -1964,11 +1964,17 @@ file =
|
|||||||
.warning-corrupt-entities-dropped-parser-junk-b = Uyarı: __ocs_fmt_0__ bozuk nesne atıldı (ayrıştırıcı artığı — hatalı normaller / sayılar)
|
.warning-corrupt-entities-dropped-parser-junk-b = Uyarı: __ocs_fmt_0__ bozuk nesne atıldı (ayrıştırıcı artığı — hatalı normaller / sayılar)
|
||||||
.warning-corrupt-xref-entities-dropped = Uyarı: __ocs_fmt_0__ bozuk dış referans nesnesi atıldı
|
.warning-corrupt-xref-entities-dropped = Uyarı: __ocs_fmt_0__ bozuk dış referans nesnesi atıldı
|
||||||
.xref-loaded = XREF "__ocs_fmt_0__" yüklendi
|
.xref-loaded = XREF "__ocs_fmt_0__" yüklendi
|
||||||
|
.cmd-cannot-build-the-transmittal-zip-e = __ocs_fmt_0__: aktarım ZIP'i oluşturulamıyor (__ocs_fmt_1__).
|
||||||
.cmd-cannot-create-folder-e = __ocs_fmt_0__: Klasör oluşturulamıyor (__ocs_fmt_1__).
|
.cmd-cannot-create-folder-e = __ocs_fmt_0__: Klasör oluşturulamıyor (__ocs_fmt_1__).
|
||||||
|
.cmd-cannot-package-the-drawing-e = __ocs_fmt_0__: çizim paketlenemiyor (__ocs_fmt_1__).
|
||||||
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: __ocs_fmt_1__ dosya __ocs_fmt_2__ içine paketlendi
|
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__: __ocs_fmt_1__ dosya __ocs_fmt_2__ içine paketlendi
|
||||||
|
.cmd-skipped-n-referenced-file-s-not-loaded-in = __ocs_fmt_0__: bu oturumda yüklenmemiş __ocs_fmt_1__ başvurulan dosya atlandı: __ocs_fmt_2__
|
||||||
.error-external-change-checks-remain-active = __ocs_fmt_0__. Dış değişiklik denetimleri etkin kalır.
|
.error-external-change-checks-remain-active = __ocs_fmt_0__. Dış değişiklik denetimleri etkin kalır.
|
||||||
.warning-external-change-checks-remain-active = __ocs_fmt_0__. Dış değişiklik denetimleri etkin kalır.
|
.warning-external-change-checks-remain-active = __ocs_fmt_0__. Dış değişiklik denetimleri etkin kalır.
|
||||||
.opening-a-drawing-by-path-is-not-available-in = Tarayıcıda dosya yoluyla çizim açılamaz.
|
.opening-a-drawing-by-path-is-not-available-in = Tarayıcıda dosya yoluyla çizim açılamaz.
|
||||||
|
.pick-the-file-again-to-load-its-contents = İçeriğini tarayıcıya yüklemek için dosyayı yeniden seçin.
|
||||||
|
.adding-folders-is-not-available-in-the-browser = Tarayıcıda klasör ekleme kullanılamıyor. Bunun yerine çizim dosyaları ekleyin.
|
||||||
|
.drawing-not-loaded-in-the-browser = Çizim tarayıcıya yüklenmedi: __ocs_fmt_0__.
|
||||||
|
|
||||||
selection =
|
selection =
|
||||||
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Nesneleri seçin (__ocs_arg_count__ seçili, uygulamak için Enter):
|
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Nesneleri seçin (__ocs_arg_count__ seçili, uygulamak için Enter):
|
||||||
@@ -3102,6 +3108,7 @@ insert =
|
|||||||
.xref-no-external-references-in-this-drawing = XREF Bu çizimde dış referans yok.
|
.xref-no-external-references-in-this-drawing = XREF Bu çizimde dış referans yok.
|
||||||
.xref-reloaded = XREF "__ocs_fmt_0__" yeniden yüklendi
|
.xref-reloaded = XREF "__ocs_fmt_0__" yeniden yüklendi
|
||||||
.xref-save-the-drawing-first-to-resolve-relativ = XREF Göreli XREF yollarını çözümlemek için önce çizimi kaydedin.
|
.xref-save-the-drawing-first-to-resolve-relativ = XREF Göreli XREF yollarını çözümlemek için önce çizimi kaydedin.
|
||||||
|
.xref-unresolved-external-references = XREF Çözümlenemeyen harici referanslar: __ocs_fmt_0__ — referans verilen DWG/DXF dosyalarını sayfaya bırakarak kaydedin, sonra bu çizimi yeniden açın veya XRELOAD komutunu çalıştırın.
|
||||||
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ nesne panoya kopyalandı (taban __ocs_fmt_1__,__ocs_fmt_2__).
|
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ nesne panoya kopyalandı (taban __ocs_fmt_1__,__ocs_fmt_2__).
|
||||||
.object-s-copied-to-clipboard = __ocs_fmt_0__ nesne panoya kopyalandı.
|
.object-s-copied-to-clipboard = __ocs_fmt_0__ nesne panoya kopyalandı.
|
||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ nesne panoya kesildi.
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ nesne panoya kesildi.
|
||||||
|
|||||||
@@ -1968,11 +1968,17 @@ file =
|
|||||||
.warning-corrupt-entities-dropped-parser-junk-b = 警告:已丢弃 __ocs_fmt_0__ 个损坏对象(解析数据无效——法线或计数错误)
|
.warning-corrupt-entities-dropped-parser-junk-b = 警告:已丢弃 __ocs_fmt_0__ 个损坏对象(解析数据无效——法线或计数错误)
|
||||||
.warning-corrupt-xref-entities-dropped = 警告:已丢弃 __ocs_fmt_0__ 个损坏的 xref 对象
|
.warning-corrupt-xref-entities-dropped = 警告:已丢弃 __ocs_fmt_0__ 个损坏的 xref 对象
|
||||||
.xref-loaded = XREF已装入"__ocs_fmt_0__"
|
.xref-loaded = XREF已装入"__ocs_fmt_0__"
|
||||||
|
.cmd-cannot-build-the-transmittal-zip-e = __ocs_fmt_0__: 无法创建传输 ZIP(__ocs_fmt_1__)。
|
||||||
.cmd-cannot-create-folder-e = __ocs_fmt_0__:无法创建文件夹(__ocs_fmt_1__).
|
.cmd-cannot-create-folder-e = __ocs_fmt_0__:无法创建文件夹(__ocs_fmt_1__).
|
||||||
|
.cmd-cannot-package-the-drawing-e = __ocs_fmt_0__: 无法打包图形(__ocs_fmt_1__)。
|
||||||
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__:已将 __ocs_fmt_1__文件打包入 __ocs_fmt_2__
|
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__:已将 __ocs_fmt_1__文件打包入 __ocs_fmt_2__
|
||||||
|
.cmd-skipped-n-referenced-file-s-not-loaded-in = __ocs_fmt_0__: 已跳过 __ocs_fmt_1__ 个本次会话中未加载的参照文件: __ocs_fmt_2__
|
||||||
.error-external-change-checks-remain-active = __ocs_fmt_0__. 对外变更检查仍在进行中。
|
.error-external-change-checks-remain-active = __ocs_fmt_0__. 对外变更检查仍在进行中。
|
||||||
.warning-external-change-checks-remain-active = __ocs_fmt_0__. 对外变更检查仍在进行中。
|
.warning-external-change-checks-remain-active = __ocs_fmt_0__. 对外变更检查仍在进行中。
|
||||||
.opening-a-drawing-by-path-is-not-available-in = 浏览器不支持通过路径打开图形。
|
.opening-a-drawing-by-path-is-not-available-in = 浏览器不支持通过路径打开图形。
|
||||||
|
.pick-the-file-again-to-load-its-contents = 请重新选择文件以将其内容加载到浏览器中。
|
||||||
|
.adding-folders-is-not-available-in-the-browser = 浏览器中无法添加文件夹。请改为添加图形文件。
|
||||||
|
.drawing-not-loaded-in-the-browser = 图形未加载到浏览器中:__ocs_fmt_0__。
|
||||||
|
|
||||||
selection =
|
selection =
|
||||||
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ 选择对象(已选择 __ocs_arg_count__ 个,按 Enter 应用):
|
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ 选择对象(已选择 __ocs_arg_count__ 个,按 Enter 应用):
|
||||||
@@ -3118,6 +3124,7 @@ insert =
|
|||||||
.xref-no-external-references-in-this-drawing = XREF 此绘图中没有外部引用 。
|
.xref-no-external-references-in-this-drawing = XREF 此绘图中没有外部引用 。
|
||||||
.xref-reloaded = XREF 重新装入了 “ __ocs_fmt_0__ ”
|
.xref-reloaded = XREF 重新装入了 “ __ocs_fmt_0__ ”
|
||||||
.xref-save-the-drawing-first-to-resolve-relativ = XREF 先保存绘图以解析相亲的XREF路径 。
|
.xref-save-the-drawing-first-to-resolve-relativ = XREF 先保存绘图以解析相亲的XREF路径 。
|
||||||
|
.xref-unresolved-external-references = XREF 未解析的外部参照:__ocs_fmt_0__ —— 将参照的 DWG/DXF 文件拖放到页面上以注册它们,然后重新打开此图形或运行 XRELOAD。
|
||||||
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ 对象已复制到剪贴板( 基准 __ocs_fmt_1__,__ocs_fmt_2__) 。
|
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ 对象已复制到剪贴板( 基准 __ocs_fmt_1__,__ocs_fmt_2__) 。
|
||||||
.object-s-copied-to-clipboard = __ocs_fmt_0__ 对象被复制到剪贴板。
|
.object-s-copied-to-clipboard = __ocs_fmt_0__ 对象被复制到剪贴板。
|
||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ 对象切入剪贴板。
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ 对象切入剪贴板。
|
||||||
|
|||||||
@@ -1980,11 +1980,17 @@ file =
|
|||||||
.warning-corrupt-entities-dropped-parser-junk-b = 警告:已丟棄 __ocs_fmt_0__ 個損壞物件(解析資料無效——法線或計數錯誤)
|
.warning-corrupt-entities-dropped-parser-junk-b = 警告:已丟棄 __ocs_fmt_0__ 個損壞物件(解析資料無效——法線或計數錯誤)
|
||||||
.warning-corrupt-xref-entities-dropped = 警告:已丟棄 __ocs_fmt_0__ 個損壞的 xref 物件
|
.warning-corrupt-xref-entities-dropped = 警告:已丟棄 __ocs_fmt_0__ 個損壞的 xref 物件
|
||||||
.xref-loaded = XREF已載入"__ocs_fmt_0__"
|
.xref-loaded = XREF已載入"__ocs_fmt_0__"
|
||||||
|
.cmd-cannot-build-the-transmittal-zip-e = __ocs_fmt_0__: 無法建立傳送 ZIP(__ocs_fmt_1__)。
|
||||||
.cmd-cannot-create-folder-e = __ocs_fmt_0__:無法建立資料夾(__ocs_fmt_1__).
|
.cmd-cannot-create-folder-e = __ocs_fmt_0__:無法建立資料夾(__ocs_fmt_1__).
|
||||||
|
.cmd-cannot-package-the-drawing-e = __ocs_fmt_0__: 無法打包圖面(__ocs_fmt_1__)。
|
||||||
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__:已將 __ocs_fmt_1__檔案打包入 __ocs_fmt_2__
|
.cmd-packaged-copied-file-s-into = __ocs_fmt_0__:已將 __ocs_fmt_1__檔案打包入 __ocs_fmt_2__
|
||||||
|
.cmd-skipped-n-referenced-file-s-not-loaded-in = __ocs_fmt_0__: 已跳過 __ocs_fmt_1__ 個本次工作階段中未載入的參考檔案: __ocs_fmt_2__
|
||||||
.error-external-change-checks-remain-active = __ocs_fmt_0__. 對外變更檢查仍在進行中。
|
.error-external-change-checks-remain-active = __ocs_fmt_0__. 對外變更檢查仍在進行中。
|
||||||
.warning-external-change-checks-remain-active = __ocs_fmt_0__. 對外變更檢查仍在進行中。
|
.warning-external-change-checks-remain-active = __ocs_fmt_0__. 對外變更檢查仍在進行中。
|
||||||
.opening-a-drawing-by-path-is-not-available-in = 瀏覽器不支援通過路徑開啟圖形。
|
.opening-a-drawing-by-path-is-not-available-in = 瀏覽器不支援通過路徑開啟圖形。
|
||||||
|
.pick-the-file-again-to-load-its-contents = 請重新選擇檔案,將其內容載入瀏覽器。
|
||||||
|
.adding-folders-is-not-available-in-the-browser = 瀏覽器中無法新增資料夾。請改為新增圖面檔案。
|
||||||
|
.drawing-not-loaded-in-the-browser = 圖面未載入瀏覽器:__ocs_fmt_0__。
|
||||||
|
|
||||||
selection =
|
selection =
|
||||||
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ 選擇物件(已選擇 __ocs_arg_count__ 個,按 Enter 套用):
|
.cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ 選擇物件(已選擇 __ocs_arg_count__ 個,按 Enter 套用):
|
||||||
@@ -3130,6 +3136,7 @@ insert =
|
|||||||
.xref-no-external-references-in-this-drawing = XREF 此繪圖中沒有外部引用 。
|
.xref-no-external-references-in-this-drawing = XREF 此繪圖中沒有外部引用 。
|
||||||
.xref-reloaded = XREF 重新載入了 “ __ocs_fmt_0__ ”
|
.xref-reloaded = XREF 重新載入了 “ __ocs_fmt_0__ ”
|
||||||
.xref-save-the-drawing-first-to-resolve-relativ = XREF 先儲存繪圖以解析相親的XREF路徑 。
|
.xref-save-the-drawing-first-to-resolve-relativ = XREF 先儲存繪圖以解析相親的XREF路徑 。
|
||||||
|
.xref-unresolved-external-references = XREF 未解析的外部參考: __ocs_fmt_0__ — 將參考的 DWG/DXF 檔案拖放到頁面上以註冊它們,然後重新開啟此圖面或執行 XRELOAD。
|
||||||
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ 物件已複製到剪貼簿( 基準 __ocs_fmt_1__,__ocs_fmt_2__) 。
|
.object-s-copied-to-clipboard-base-3-3 = __ocs_fmt_0__ 物件已複製到剪貼簿( 基準 __ocs_fmt_1__,__ocs_fmt_2__) 。
|
||||||
.object-s-copied-to-clipboard = __ocs_fmt_0__ 物件被複製到剪貼簿。
|
.object-s-copied-to-clipboard = __ocs_fmt_0__ 物件被複製到剪貼簿。
|
||||||
.object-s-cut-to-clipboard = __ocs_fmt_0__ 物件切入剪貼簿。
|
.object-s-cut-to-clipboard = __ocs_fmt_0__ 物件切入剪貼簿。
|
||||||
|
|||||||
+363
-58
@@ -1,5 +1,7 @@
|
|||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||||
|
use std::io::Cursor;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::{Arc, Mutex, OnceLock};
|
||||||
|
|
||||||
use acadrust::entities::{table::CellRange, TableBuilder};
|
use acadrust::entities::{table::CellRange, TableBuilder};
|
||||||
use acadrust::objects::{ClassObject, ClassObjectData, DataLink, DataLinkCustomData, ObjectType};
|
use acadrust::objects::{ClassObject, ClassObjectData, DataLink, DataLinkCustomData, ObjectType};
|
||||||
@@ -25,6 +27,100 @@ struct ExtractionRecord {
|
|||||||
details: String,
|
details: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── In-session picked-file registries (web) ───────────────────────────────
|
||||||
|
//
|
||||||
|
// The browser exposes no reusable filesystem path: a picked file's bytes exist
|
||||||
|
// only while the app keeps them. Both registries below hold those bytes (and
|
||||||
|
// what was parsed from them) keyed by the reference the drawing or wizard
|
||||||
|
// carries, following the image-model / PDF-raster pattern. Native builds never
|
||||||
|
// fill them — the real path stays readable — but the lookup and matching
|
||||||
|
// logic compiles everywhere so it stays unit-testable.
|
||||||
|
|
||||||
|
fn picked_sources() -> &'static Mutex<HashMap<String, Arc<Vec<u8>>>> {
|
||||||
|
static SOURCES: OnceLock<Mutex<HashMap<String, Arc<Vec<u8>>>>> = OnceLock::new();
|
||||||
|
SOURCES.get_or_init(|| Mutex::new(HashMap::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
|
||||||
|
fn extraction_documents() -> &'static Mutex<HashMap<String, Arc<CadDocument>>> {
|
||||||
|
static DOCUMENTS: OnceLock<Mutex<HashMap<String, Arc<CadDocument>>>> = OnceLock::new();
|
||||||
|
DOCUMENTS.get_or_init(|| Mutex::new(HashMap::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lower-cased trailing component of a path-style reference. Links and wizard
|
||||||
|
/// sources carry whatever the author stored, so the file name — not the full
|
||||||
|
/// path — is the only component a re-picked file can be expected to match.
|
||||||
|
fn source_name_key(path: &str) -> String {
|
||||||
|
path.rsplit(['/', '\\'])
|
||||||
|
.next()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_ascii_lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Do two source references name the same file? Compares trailing components
|
||||||
|
/// case-insensitively (Windows paths are case-insensitive where the link was
|
||||||
|
/// authored, and the web has no path beyond the name at all).
|
||||||
|
fn same_source_name(a: &str, b: &str) -> bool {
|
||||||
|
let (a, b) = (source_name_key(a), source_name_key(b));
|
||||||
|
!a.is_empty() && a == b
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keep the bytes of a spreadsheet or settings file chosen through a picker.
|
||||||
|
/// Re-opening the link in-session re-reads these instead of demanding a new
|
||||||
|
/// pick; re-picking the same file overwrites the entry.
|
||||||
|
// Only the web registers sources (native re-reads the real file, so a file
|
||||||
|
// edited after picking is picked up); tests exercise the registry directly.
|
||||||
|
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
|
||||||
|
pub(crate) fn register_picked_source(path: &str, bytes: Arc<Vec<u8>>) {
|
||||||
|
picked_sources()
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|error| error.into_inner())
|
||||||
|
.insert(path.to_string(), bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bytes registered for this reference — exact key first, then any entry that
|
||||||
|
/// names the same file (the web fallback for links whose stored path is the
|
||||||
|
/// original author's, while the user picked only the bare file).
|
||||||
|
pub(crate) fn picked_source_bytes(path: &str) -> Option<Arc<Vec<u8>>> {
|
||||||
|
let cache = picked_sources()
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|error| error.into_inner());
|
||||||
|
if let Some(bytes) = cache.get(path) {
|
||||||
|
return Some(Arc::clone(bytes));
|
||||||
|
}
|
||||||
|
cache
|
||||||
|
.iter()
|
||||||
|
.find(|(key, _)| same_source_name(key, path))
|
||||||
|
.map(|(_, bytes)| Arc::clone(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keep a source drawing parsed at pick time, keyed by the name the wizard
|
||||||
|
/// lists. The extraction pipeline reads these on the web instead of calling
|
||||||
|
/// the path-based `load_file`.
|
||||||
|
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
|
||||||
|
pub(crate) fn register_extraction_document(name: &str, document: CadDocument) {
|
||||||
|
extraction_documents()
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|error| error.into_inner())
|
||||||
|
.insert(name.to_string(), Arc::new(document));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The parsed drawing registered under this source reference, matched by exact
|
||||||
|
/// key or by file name.
|
||||||
|
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
|
||||||
|
pub(crate) fn extraction_document(path: &str) -> Option<Arc<CadDocument>> {
|
||||||
|
let cache = extraction_documents()
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|error| error.into_inner());
|
||||||
|
if let Some(document) = cache.get(path) {
|
||||||
|
return Some(Arc::clone(document));
|
||||||
|
}
|
||||||
|
cache
|
||||||
|
.iter()
|
||||||
|
.find(|(key, _)| same_source_name(key, path))
|
||||||
|
.map(|(_, document)| Arc::clone(document))
|
||||||
|
}
|
||||||
|
|
||||||
impl ExtractionRecord {
|
impl ExtractionRecord {
|
||||||
fn value(&self, key: &str) -> String {
|
fn value(&self, key: &str) -> String {
|
||||||
match key {
|
match key {
|
||||||
@@ -88,11 +184,15 @@ fn data_link_choices(doc: &CadDocument) -> Vec<DataLinkChoice> {
|
|||||||
ObjectType::ClassObject(object) => match &object.data {
|
ObjectType::ClassObject(object) => match &object.data {
|
||||||
ClassObjectData::DataLink(link) => {
|
ClassObjectData::DataLink(link) => {
|
||||||
let path = resolve_link_path(doc, link);
|
let path = resolve_link_path(doc, link);
|
||||||
|
let path_text = path.to_string_lossy().into_owned();
|
||||||
|
// The web has no filesystem: a link is valid when its
|
||||||
|
// picked bytes are still registered in-session.
|
||||||
|
let valid = path.is_file() || picked_source_bytes(&path_text).is_some();
|
||||||
Some(DataLinkChoice {
|
Some(DataLinkChoice {
|
||||||
handle: *handle,
|
handle: *handle,
|
||||||
name: link_display_name(link),
|
name: link_display_name(link),
|
||||||
path: path.to_string_lossy().into_owned(),
|
path: path_text,
|
||||||
valid: path.is_file(),
|
valid,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -171,6 +271,12 @@ fn workbook_metadata(path: &Path) -> Result<(Vec<String>, Vec<String>), String>
|
|||||||
if ext == "csv" || ext == "txt" {
|
if ext == "csv" || ext == "txt" {
|
||||||
return Ok((Vec::new(), Vec::new()));
|
return Ok((Vec::new(), Vec::new()));
|
||||||
}
|
}
|
||||||
|
if let Some(bytes) = picked_source_bytes(&path.to_string_lossy()) {
|
||||||
|
return workbook_metadata_bytes(&bytes);
|
||||||
|
}
|
||||||
|
if cfg!(target_arch = "wasm32") {
|
||||||
|
return Err(crate::t!("Pick the file again to load its contents in the browser.").into_owned());
|
||||||
|
}
|
||||||
use calamine::{open_workbook_auto, Reader};
|
use calamine::{open_workbook_auto, Reader};
|
||||||
let workbook = open_workbook_auto(path).map_err(|error| error.to_string())?;
|
let workbook = open_workbook_auto(path).map_err(|error| error.to_string())?;
|
||||||
let sheets = workbook.sheet_names();
|
let sheets = workbook.sheet_names();
|
||||||
@@ -182,6 +288,20 @@ fn workbook_metadata(path: &Path) -> Result<(Vec<String>, Vec<String>), String>
|
|||||||
Ok((sheets, names))
|
Ok((sheets, names))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sheet and defined-name catalog read straight from bytes (a picked file on
|
||||||
|
/// the web, where no path exists to reopen).
|
||||||
|
fn workbook_metadata_bytes(bytes: &[u8]) -> Result<(Vec<String>, Vec<String>), String> {
|
||||||
|
use calamine::{open_workbook_auto_from_rs, Reader};
|
||||||
|
let workbook = open_workbook_auto_from_rs(Cursor::new(bytes)).map_err(|error| error.to_string())?;
|
||||||
|
let sheets = workbook.sheet_names();
|
||||||
|
let names = workbook
|
||||||
|
.defined_names()
|
||||||
|
.iter()
|
||||||
|
.map(|(name, _)| name.clone())
|
||||||
|
.collect();
|
||||||
|
Ok((sheets, names))
|
||||||
|
}
|
||||||
|
|
||||||
fn named_range_target(formula: &str) -> Option<(String, String)> {
|
fn named_range_target(formula: &str) -> Option<(String, String)> {
|
||||||
let formula = formula.trim().trim_start_matches('=');
|
let formula = formula.trim().trim_start_matches('=');
|
||||||
let (sheet, range) = formula.rsplit_once('!')?;
|
let (sheet, range) = formula.rsplit_once('!')?;
|
||||||
@@ -193,6 +313,66 @@ fn named_range_target(formula: &str) -> Option<(String, String)> {
|
|||||||
(!sheet.is_empty() && !range.is_empty()).then_some((sheet, range))
|
(!sheet.is_empty() && !range.is_empty()).then_some((sheet, range))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read the text of a CSV/TXT (or settings) source: registered picked bytes
|
||||||
|
/// first, then the native filesystem. On the web an unregistered path has
|
||||||
|
/// nothing behind it, so it asks for the pick instead of failing on I/O.
|
||||||
|
fn read_text_source(path: &Path) -> Result<String, String> {
|
||||||
|
if let Some(bytes) = picked_source_bytes(&path.to_string_lossy()) {
|
||||||
|
return Ok(String::from_utf8_lossy(&bytes).into_owned());
|
||||||
|
}
|
||||||
|
if cfg!(target_arch = "wasm32") {
|
||||||
|
return Err(crate::t!("Pick the file again to load its contents in the browser.").into_owned());
|
||||||
|
}
|
||||||
|
std::fs::read_to_string(path).map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sheet selection + range resolution shared by the path- and bytes-based
|
||||||
|
/// workbook readers. Returns the raw rows and the cell range to crop to, if
|
||||||
|
/// any.
|
||||||
|
fn workbook_rows<RS: std::io::Read + std::io::Seek>(
|
||||||
|
workbook: &mut calamine::Sheets<RS>,
|
||||||
|
sheet: Option<&str>,
|
||||||
|
range_kind: LinkRange,
|
||||||
|
range_value: Option<&str>,
|
||||||
|
) -> Result<(Vec<Vec<String>>, Option<String>), String> {
|
||||||
|
use calamine::Reader;
|
||||||
|
let (selected_sheet, cell_range) = if range_kind == LinkRange::NamedRange {
|
||||||
|
let name = range_value
|
||||||
|
.filter(|value| !value.trim().is_empty())
|
||||||
|
.ok_or_else(|| crate::t!("Select a named range.").into_owned())?;
|
||||||
|
let formula = workbook
|
||||||
|
.defined_names()
|
||||||
|
.iter()
|
||||||
|
.find(|(candidate, _)| candidate.eq_ignore_ascii_case(name))
|
||||||
|
.map(|(_, formula)| formula.clone())
|
||||||
|
.ok_or_else(|| crate::t!("The named range was not found.").into_owned())?;
|
||||||
|
named_range_target(&formula)
|
||||||
|
.ok_or_else(|| crate::t!("The named range does not refer to a worksheet cell range.").into_owned())?
|
||||||
|
} else {
|
||||||
|
let selected_sheet = sheet
|
||||||
|
.filter(|value| !value.trim().is_empty())
|
||||||
|
.map(str::to_string)
|
||||||
|
.or_else(|| workbook.sheet_names().first().cloned())
|
||||||
|
.ok_or_else(|| crate::t!("The spreadsheet contains no worksheets.").into_owned())?;
|
||||||
|
let range = (range_kind == LinkRange::CellRange)
|
||||||
|
.then_some(range_value)
|
||||||
|
.flatten()
|
||||||
|
.filter(|value| !value.trim().is_empty())
|
||||||
|
.map(str::to_string)
|
||||||
|
.unwrap_or_default();
|
||||||
|
(selected_sheet, range)
|
||||||
|
};
|
||||||
|
let data = workbook
|
||||||
|
.worksheet_range(&selected_sheet)
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
Ok((
|
||||||
|
data.rows()
|
||||||
|
.map(|row| row.iter().map(ToString::to_string).collect())
|
||||||
|
.collect(),
|
||||||
|
(!cell_range.is_empty()).then_some(cell_range),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn read_tabular_file(
|
pub(crate) fn read_tabular_file(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
sheet: Option<&str>,
|
sheet: Option<&str>,
|
||||||
@@ -204,52 +384,24 @@ pub(crate) fn read_tabular_file(
|
|||||||
if range_kind == LinkRange::NamedRange {
|
if range_kind == LinkRange::NamedRange {
|
||||||
return Err(crate::t!("Named ranges require a spreadsheet file.").into_owned());
|
return Err(crate::t!("Named ranges require a spreadsheet file.").into_owned());
|
||||||
}
|
}
|
||||||
let text = std::fs::read_to_string(path).map_err(|error| error.to_string())?;
|
|
||||||
(
|
(
|
||||||
crate::app::commands::display::parse_csv_table(&text),
|
crate::app::commands::display::parse_csv_table(&read_text_source(path)?),
|
||||||
(range_kind == LinkRange::CellRange)
|
(range_kind == LinkRange::CellRange)
|
||||||
.then_some(range_value)
|
.then_some(range_value)
|
||||||
.flatten()
|
.flatten()
|
||||||
.map(str::to_string),
|
.map(str::to_string),
|
||||||
)
|
)
|
||||||
} else {
|
} else if let Some(bytes) = picked_source_bytes(&path.to_string_lossy()) {
|
||||||
use calamine::{open_workbook_auto, Reader};
|
use calamine::open_workbook_auto_from_rs;
|
||||||
let mut workbook = open_workbook_auto(path).map_err(|error| error.to_string())?;
|
let mut workbook = open_workbook_auto_from_rs(Cursor::new(bytes.as_slice()))
|
||||||
let (selected_sheet, cell_range) = if range_kind == LinkRange::NamedRange {
|
|
||||||
let name = range_value
|
|
||||||
.filter(|value| !value.trim().is_empty())
|
|
||||||
.ok_or_else(|| crate::t!("Select a named range.").into_owned())?;
|
|
||||||
let formula = workbook
|
|
||||||
.defined_names()
|
|
||||||
.iter()
|
|
||||||
.find(|(candidate, _)| candidate.eq_ignore_ascii_case(name))
|
|
||||||
.map(|(_, formula)| formula.clone())
|
|
||||||
.ok_or_else(|| crate::t!("The named range was not found.").into_owned())?;
|
|
||||||
named_range_target(&formula)
|
|
||||||
.ok_or_else(|| crate::t!("The named range does not refer to a worksheet cell range.").into_owned())?
|
|
||||||
} else {
|
|
||||||
let selected_sheet = sheet
|
|
||||||
.filter(|value| !value.trim().is_empty())
|
|
||||||
.map(str::to_string)
|
|
||||||
.or_else(|| workbook.sheet_names().first().cloned())
|
|
||||||
.ok_or_else(|| crate::t!("The spreadsheet contains no worksheets.").into_owned())?;
|
|
||||||
let range = (range_kind == LinkRange::CellRange)
|
|
||||||
.then_some(range_value)
|
|
||||||
.flatten()
|
|
||||||
.filter(|value| !value.trim().is_empty())
|
|
||||||
.map(str::to_string)
|
|
||||||
.unwrap_or_default();
|
|
||||||
(selected_sheet, range)
|
|
||||||
};
|
|
||||||
let data = workbook
|
|
||||||
.worksheet_range(&selected_sheet)
|
|
||||||
.map_err(|error| error.to_string())?;
|
.map_err(|error| error.to_string())?;
|
||||||
(
|
workbook_rows(&mut workbook, sheet, range_kind, range_value)?
|
||||||
data.rows()
|
} else if cfg!(target_arch = "wasm32") {
|
||||||
.map(|row| row.iter().map(ToString::to_string).collect())
|
return Err(crate::t!("Pick the file again to load its contents in the browser.").into_owned());
|
||||||
.collect(),
|
} else {
|
||||||
(!cell_range.is_empty()).then_some(cell_range),
|
use calamine::open_workbook_auto;
|
||||||
)
|
let mut workbook = open_workbook_auto(path).map_err(|error| error.to_string())?;
|
||||||
|
workbook_rows(&mut workbook, sheet, range_kind, range_value)?
|
||||||
};
|
};
|
||||||
let rows = match cell_range.as_deref().filter(|value| !value.trim().is_empty()) {
|
let rows = match cell_range.as_deref().filter(|value| !value.trim().is_empty()) {
|
||||||
Some(value) => crop_range(rows, value)?,
|
Some(value) => crop_range(rows, value)?,
|
||||||
@@ -429,12 +581,14 @@ fn collect_records(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn is_drawing_file(path: &Path) -> bool {
|
fn is_drawing_file(path: &Path) -> bool {
|
||||||
path.extension()
|
path.extension()
|
||||||
.and_then(|value| value.to_str())
|
.and_then(|value| value.to_str())
|
||||||
.is_some_and(|value| value.eq_ignore_ascii_case("dwg") || value.eq_ignore_ascii_case("dxf"))
|
.is_some_and(|value| value.eq_ignore_ascii_case("dwg") || value.eq_ignore_ascii_case("dxf"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn collect_drawing_paths(
|
fn collect_drawing_paths(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
recursive: bool,
|
recursive: bool,
|
||||||
@@ -461,6 +615,7 @@ fn collect_drawing_paths(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn extraction_drawing_paths(state: &DataExtractionState) -> Result<Vec<PathBuf>, String> {
|
fn extraction_drawing_paths(state: &DataExtractionState) -> Result<Vec<PathBuf>, String> {
|
||||||
let mut paths = Vec::new();
|
let mut paths = Vec::new();
|
||||||
for source in &state.source_files {
|
for source in &state.source_files {
|
||||||
@@ -471,6 +626,36 @@ fn extraction_drawing_paths(state: &DataExtractionState) -> Result<Vec<PathBuf>,
|
|||||||
Ok(paths)
|
Ok(paths)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Source drawings the extraction pipeline should read beyond the current
|
||||||
|
/// one. Native loads them from disk (folders walked recursively); the web
|
||||||
|
/// reads the documents parsed at pick time from the in-session registry —
|
||||||
|
/// every listed source must have been picked, or it asks for the pick again.
|
||||||
|
fn extraction_documents_for(
|
||||||
|
state: &DataExtractionState,
|
||||||
|
) -> Result<Vec<(String, Arc<CadDocument>)>, String> {
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{
|
||||||
|
let mut documents = Vec::new();
|
||||||
|
for source in &state.source_files {
|
||||||
|
let document = extraction_document(source).ok_or_else(|| {
|
||||||
|
crate::tf!("Drawing not loaded in the browser: {name}.", name = source).into_owned()
|
||||||
|
})?;
|
||||||
|
documents.push((source.clone(), document));
|
||||||
|
}
|
||||||
|
Ok(documents)
|
||||||
|
}
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
let mut documents = Vec::new();
|
||||||
|
for source in extraction_drawing_paths(state)? {
|
||||||
|
let document = crate::io::load_file(&source)
|
||||||
|
.map_err(|error| format!("{}: {error}", source.display()))?;
|
||||||
|
documents.push((source.to_string_lossy().into_owned(), Arc::new(document)));
|
||||||
|
}
|
||||||
|
Ok(documents)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn collect_type_counts(
|
fn collect_type_counts(
|
||||||
doc: &CadDocument,
|
doc: &CadDocument,
|
||||||
allowed_handles: Option<&BTreeSet<Handle>>,
|
allowed_handles: Option<&BTreeSet<Handle>>,
|
||||||
@@ -500,9 +685,8 @@ fn extraction_preview(state: &DataExtractionState, current: &CadDocument) -> Res
|
|||||||
if state.include_current {
|
if state.include_current {
|
||||||
collect_records(current, None, &allowed_types, &mut records);
|
collect_records(current, None, &allowed_types, &mut records);
|
||||||
}
|
}
|
||||||
for source in extraction_drawing_paths(state)? {
|
for (_, document) in extraction_documents_for(state)? {
|
||||||
let doc = crate::io::load_file(&source).map_err(|error| format!("{}: {error}", source.display()))?;
|
collect_records(&document, None, &allowed_types, &mut records);
|
||||||
collect_records(&doc, None, &allowed_types, &mut records);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -598,11 +782,25 @@ fn save_extraction_settings(state: &DataExtractionState) -> Result<(), String> {
|
|||||||
"tableTitle": state.table_title,
|
"tableTitle": state.table_title,
|
||||||
});
|
});
|
||||||
let text = serde_json::to_string_pretty(&value).map_err(|error| error.to_string())?;
|
let text = serde_json::to_string_pretty(&value).map_err(|error| error.to_string())?;
|
||||||
std::fs::write(&state.settings_path, text).map_err(|error| error.to_string())
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{
|
||||||
|
// No filesystem to write to: hand the settings to the browser as a
|
||||||
|
// download, mirroring the native write of the same JSON.
|
||||||
|
let name = Path::new(state.settings_path.trim())
|
||||||
|
.file_name()
|
||||||
|
.map(|value| value.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| "extraction.dxex".into());
|
||||||
|
crate::sys::download_bytes(&name, text.as_bytes());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
std::fs::write(&state.settings_path, text).map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_extraction_settings(state: &mut DataExtractionState) -> Result<(), String> {
|
fn load_extraction_settings(state: &mut DataExtractionState) -> Result<(), String> {
|
||||||
let text = std::fs::read_to_string(&state.settings_path).map_err(|error| error.to_string())?;
|
let text = read_text_source(Path::new(state.settings_path.trim()))?;
|
||||||
let value: serde_json::Value = serde_json::from_str(&text).map_err(|error| error.to_string())?;
|
let value: serde_json::Value = serde_json::from_str(&text).map_err(|error| error.to_string())?;
|
||||||
state.source = match value["source"].as_str() {
|
state.source = match value["source"].as_str() {
|
||||||
Some("currentSelection") => ExtractionSource::CurrentSelection,
|
Some("currentSelection") => ExtractionSource::CurrentSelection,
|
||||||
@@ -878,13 +1076,22 @@ impl OpenCADStudio {
|
|||||||
pub(super) fn on_data_link_browse(&mut self) -> Task<Message> {
|
pub(super) fn on_data_link_browse(&mut self) -> Task<Message> {
|
||||||
Task::perform(
|
Task::perform(
|
||||||
async {
|
async {
|
||||||
crate::sys::file_dialog()
|
let Some(handle) = crate::sys::file_dialog()
|
||||||
.set_title(crate::t!("Choose a Spreadsheet File").as_ref())
|
.set_title(crate::t!("Choose a Spreadsheet File").as_ref())
|
||||||
.add_filter(crate::t!("Spreadsheet Files").as_ref(), &["csv", "txt", "xls", "xlsx", "xlsb", "ods"])
|
.add_filter(crate::t!("Spreadsheet Files").as_ref(), &["csv", "txt", "xls", "xlsx", "xlsb", "ods"])
|
||||||
.add_filter(crate::t!("All Files").as_ref(), &["*"])
|
.add_filter(crate::t!("All Files").as_ref(), &["*"])
|
||||||
.pick_file()
|
.pick_file()
|
||||||
.await
|
.await
|
||||||
.map(|handle| crate::sys::handle_path(&handle))
|
else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let path = crate::sys::handle_path(&handle);
|
||||||
|
// The browser forgets the file the moment the pick resolves:
|
||||||
|
// keep its bytes so preview, save, and later re-opens read
|
||||||
|
// them instead of asking for the picker again.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
register_picked_source(&path.to_string_lossy(), Arc::new(handle.read().await));
|
||||||
|
Some(path)
|
||||||
},
|
},
|
||||||
Message::DataLinkBrowseResult,
|
Message::DataLinkBrowseResult,
|
||||||
)
|
)
|
||||||
@@ -1125,10 +1332,8 @@ impl OpenCADStudio {
|
|||||||
if state.include_current {
|
if state.include_current {
|
||||||
collect_type_counts(current, None, &mut counts);
|
collect_type_counts(current, None, &mut counts);
|
||||||
}
|
}
|
||||||
for path in extraction_drawing_paths(&state)? {
|
for (_, document) in extraction_documents_for(&state)? {
|
||||||
let doc = crate::io::load_file(&path)
|
collect_type_counts(&document, None, &mut counts);
|
||||||
.map_err(|error| format!("{}: {error}", path.display()))?;
|
|
||||||
collect_type_counts(&doc, None, &mut counts);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1253,11 +1458,23 @@ impl OpenCADStudio {
|
|||||||
let save = self.data_extraction.begin == ExtractionBegin::New;
|
let save = self.data_extraction.begin == ExtractionBegin::New;
|
||||||
Task::perform(async move {
|
Task::perform(async move {
|
||||||
let dialog = crate::sys::file_dialog().set_title(crate::t!("Data Extraction Settings").as_ref()).add_filter(crate::t!("Data Extraction Settings").as_ref(), &["dxex"]).add_filter(crate::t!("All Files").as_ref(), &["*"]);
|
let dialog = crate::sys::file_dialog().set_title(crate::t!("Data Extraction Settings").as_ref()).add_filter(crate::t!("Data Extraction Settings").as_ref(), &["dxex"]).add_filter(crate::t!("All Files").as_ref(), &["*"]);
|
||||||
if save {
|
let handle = if save {
|
||||||
dialog.set_file_name("extraction.dxex").save_file().await.map(|handle| crate::sys::handle_path(&handle))
|
dialog.set_file_name("extraction.dxex").save_file().await
|
||||||
} else {
|
} else {
|
||||||
dialog.pick_file().await.map(|handle| crate::sys::handle_path(&handle))
|
dialog.pick_file().await
|
||||||
|
};
|
||||||
|
let Some(handle) = handle else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let path = crate::sys::handle_path(&handle);
|
||||||
|
// Loading settings in the browser: the picked .dxex has no path
|
||||||
|
// to read back from, so its bytes are kept for the load that
|
||||||
|
// follows this message.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
if !save {
|
||||||
|
register_picked_source(&path.to_string_lossy(), Arc::new(handle.read().await));
|
||||||
}
|
}
|
||||||
|
Some(path)
|
||||||
}, Message::DataExtractionBrowseSettingsResult)
|
}, Message::DataExtractionBrowseSettingsResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1275,9 +1492,10 @@ impl OpenCADStudio {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub(super) fn on_data_extraction_add_drawings(&mut self) -> Task<Message> {
|
pub(super) fn on_data_extraction_add_drawings(&mut self) -> Task<Message> {
|
||||||
Task::perform(async {
|
Task::perform(async {
|
||||||
crate::sys::file_dialog()
|
Ok(crate::sys::file_dialog()
|
||||||
.set_title(crate::t!("Add Drawings").as_ref())
|
.set_title(crate::t!("Add Drawings").as_ref())
|
||||||
.add_filter(crate::t!("CAD Files").as_ref(), &["dwg", "dxf", "DWG", "DXF"])
|
.add_filter(crate::t!("CAD Files").as_ref(), &["dwg", "dxf", "DWG", "DXF"])
|
||||||
.add_filter(crate::t!("All Files").as_ref(), &["*"])
|
.add_filter(crate::t!("All Files").as_ref(), &["*"])
|
||||||
@@ -1286,10 +1504,42 @@ impl OpenCADStudio {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|handle| crate::sys::handle_path(&handle))
|
.map(|handle| crate::sys::handle_path(&handle))
|
||||||
.collect()
|
.collect())
|
||||||
}, Message::DataExtractionAddDrawingsResult)
|
}, Message::DataExtractionAddDrawingsResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Web: the picked drawings have no paths to re-open later, so each one is
|
||||||
|
/// parsed on the shared worker right here and the document registered
|
||||||
|
/// under its name — the pipeline then reads the registry instead of the
|
||||||
|
/// filesystem. The first file that fails to parse aborts the add with the
|
||||||
|
/// same `name: error` shape the native refresh reports.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub(super) fn on_data_extraction_add_drawings(&mut self) -> Task<Message> {
|
||||||
|
Task::perform(
|
||||||
|
async {
|
||||||
|
let handles = crate::sys::file_dialog()
|
||||||
|
.set_title(crate::t!("Add Drawings").as_ref())
|
||||||
|
.add_filter(crate::t!("CAD Files").as_ref(), &["dwg", "dxf", "DWG", "DXF"])
|
||||||
|
.add_filter(crate::t!("All Files").as_ref(), &["*"])
|
||||||
|
.pick_files()
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
let mut paths = Vec::new();
|
||||||
|
for handle in handles {
|
||||||
|
let name = handle.file_name();
|
||||||
|
let bytes = handle.read().await;
|
||||||
|
let document = crate::io::parse_drawing_bytes_web(&name, &bytes)
|
||||||
|
.await
|
||||||
|
.map_err(|error| format!("{}: {error}", name))?;
|
||||||
|
register_extraction_document(&name, document);
|
||||||
|
paths.push(PathBuf::from(name));
|
||||||
|
}
|
||||||
|
Ok(paths)
|
||||||
|
},
|
||||||
|
Message::DataExtractionAddDrawingsResult,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn on_data_extraction_add_drawings_result(&mut self, paths: Vec<PathBuf>) -> Task<Message> {
|
pub(super) fn on_data_extraction_add_drawings_result(&mut self, paths: Vec<PathBuf>) -> Task<Message> {
|
||||||
for path in paths {
|
for path in paths {
|
||||||
let text = path.to_string_lossy().into_owned();
|
let text = path.to_string_lossy().into_owned();
|
||||||
@@ -1379,6 +1629,18 @@ impl OpenCADStudio {
|
|||||||
} else {
|
} else {
|
||||||
rows_to_csv(&rows)
|
rows_to_csv(&rows)
|
||||||
};
|
};
|
||||||
|
// Web output mirrors the native CSV/TXT write (there is no xlsx
|
||||||
|
// writer on either target): the browser gets the same bytes as a
|
||||||
|
// download under the picked name.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{
|
||||||
|
let name = path
|
||||||
|
.file_name()
|
||||||
|
.map(|value| value.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| "extraction.csv".into());
|
||||||
|
crate::sys::download_bytes(&name, text.as_bytes());
|
||||||
|
}
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
if let Err(error) = std::fs::write(&path, text) {
|
if let Err(error) = std::fs::write(&path, text) {
|
||||||
self.data_extraction.error = error.to_string();
|
self.data_extraction.error = error.to_string();
|
||||||
return Task::none();
|
return Task::none();
|
||||||
@@ -1451,4 +1713,47 @@ mod tests {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn csv_links_read_from_registered_bytes() {
|
||||||
|
let name = format!("ocs-registry-{}-{}.csv", std::process::id(), line!());
|
||||||
|
register_picked_source(&name, Arc::new(b"a,b,c\nd,e,f\n".to_vec()));
|
||||||
|
let rows = read_tabular_file(Path::new(&name), None, LinkRange::CellRange, Some("B1:C2"));
|
||||||
|
assert_eq!(
|
||||||
|
rows.unwrap(),
|
||||||
|
vec![
|
||||||
|
vec!["b".to_string(), "c".to_string()],
|
||||||
|
vec!["e".to_string(), "f".to_string()],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn registered_bytes_match_by_file_name() {
|
||||||
|
let key = format!("ocs-match-{}-{}.xlsx", std::process::id(), line!());
|
||||||
|
register_picked_source(
|
||||||
|
&format!(r"C:\drawings\{key}"),
|
||||||
|
Arc::new(Vec::new()),
|
||||||
|
);
|
||||||
|
// Exact key…
|
||||||
|
assert!(picked_source_bytes(&format!(r"C:\drawings\{key}")).is_some());
|
||||||
|
// …same file under a differently-cased bare name (the web stores only
|
||||||
|
// the name, links may store the author's path)…
|
||||||
|
assert!(picked_source_bytes(&key.to_ascii_uppercase()).is_some());
|
||||||
|
// …and not another file.
|
||||||
|
assert!(picked_source_bytes("ocs-match-other.xlsx").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn registered_extraction_documents_match_by_file_name() {
|
||||||
|
let key = format!("ocs-doc-{}-{}.dwg", std::process::id(), line!());
|
||||||
|
register_extraction_document(&key, CadDocument::new());
|
||||||
|
assert!(extraction_document(&key.to_ascii_uppercase()).is_some());
|
||||||
|
assert!(extraction_document("ocs-doc-other.dwg").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workbook_metadata_bytes_rejects_non_workbooks() {
|
||||||
|
assert!(workbook_metadata_bytes(b"not a workbook").is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -571,13 +571,29 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
"XRELOAD" => {
|
"XRELOAD" => {
|
||||||
// Reload all xrefs for the current drawing.
|
// Reload all xrefs for the current drawing. Web: xrefs
|
||||||
if let Some(path) = &self.tabs[i].current_path.clone() {
|
// re-read from the in-session bytes registry, so an unsaved
|
||||||
if let Some(base_dir) = path.parent() {
|
// drawing reloads too; native needs the drawing's own folder
|
||||||
|
// for relative xref paths.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
let base_dir: Option<std::path::PathBuf> = Some(std::path::PathBuf::new());
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
let base_dir: Option<std::path::PathBuf> = self
|
||||||
|
.tabs[i]
|
||||||
|
.current_path
|
||||||
|
.clone()
|
||||||
|
.and_then(|path| path.parent().map(std::path::Path::to_path_buf));
|
||||||
|
if let Some(base_dir) = base_dir {
|
||||||
|
{
|
||||||
let (infos, _dropped) = crate::io::xref::resolve_xrefs(
|
let (infos, _dropped) = crate::io::xref::resolve_xrefs(
|
||||||
&mut self.tabs[i].scene.document,
|
&mut self.tabs[i].scene.document,
|
||||||
base_dir,
|
&base_dir,
|
||||||
);
|
);
|
||||||
|
// Web: unresolved references surface once, with the
|
||||||
|
// drop hint, instead of a filesystem path no browser
|
||||||
|
// can open.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
let mut unresolved: Vec<String> = Vec::new();
|
||||||
for info in &infos {
|
for info in &infos {
|
||||||
match info.status {
|
match info.status {
|
||||||
crate::io::xref::XrefStatus::Loaded => {
|
crate::io::xref::XrefStatus::Loaded => {
|
||||||
@@ -590,12 +606,17 @@ impl OpenCADStudio {
|
|||||||
info.name
|
info.name
|
||||||
).as_ref());
|
).as_ref());
|
||||||
}
|
}
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
crate::io::xref::XrefStatus::NotFound => {
|
crate::io::xref::XrefStatus::NotFound => {
|
||||||
self.command_line.push_error(crate::tf!(
|
self.command_line.push_error(crate::tf!(
|
||||||
"XREF Not found: \"{}\" ({})",
|
"XREF Not found: \"{}\" ({})",
|
||||||
info.name, info.path
|
info.name, info.path
|
||||||
).as_ref());
|
).as_ref());
|
||||||
}
|
}
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
crate::io::xref::XrefStatus::NotFound => {
|
||||||
|
unresolved.push(info.name.clone());
|
||||||
|
}
|
||||||
crate::io::xref::XrefStatus::Failed => {
|
crate::io::xref::XrefStatus::Failed => {
|
||||||
self.command_line.push_error(crate::tf!(
|
self.command_line.push_error(crate::tf!(
|
||||||
"XREF Reload failed: \"{}\" ({})",
|
"XREF Reload failed: \"{}\" ({})",
|
||||||
@@ -610,6 +631,16 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
if !unresolved.is_empty() {
|
||||||
|
let names = unresolved.join(", ");
|
||||||
|
self.command_line.push_warning(
|
||||||
|
crate::tf!(
|
||||||
|
"XREF Unresolved external references: {names} — drop the referenced DWG/DXF files on the page to register them, then reopen this drawing or run XRELOAD."
|
||||||
|
)
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
self.tabs[i].scene.populate_hatches_from_document();
|
self.tabs[i].scene.populate_hatches_from_document();
|
||||||
self.tabs[i].scene.populate_images_from_document();
|
self.tabs[i].scene.populate_images_from_document();
|
||||||
self.tabs[i].scene.populate_meshes_from_document();
|
self.tabs[i].scene.populate_meshes_from_document();
|
||||||
|
|||||||
+248
-56
@@ -148,72 +148,81 @@ impl OpenCADStudio {
|
|||||||
|
|
||||||
// ARCHIVE / ETRANSMIT — package the drawing and its referenced files
|
// ARCHIVE / ETRANSMIT — package the drawing and its referenced files
|
||||||
// (xrefs + raster images) into a sibling "<name>_archive" folder.
|
// (xrefs + raster images) into a sibling "<name>_archive" folder.
|
||||||
|
// Web: there is no folder to copy into — the same dependency set is
|
||||||
|
// collected from the session's bytes registries and delivered as a
|
||||||
|
// "<name>_transmit.zip" download (`archive_web` below).
|
||||||
"ARCHIVE" | "ETRANSMIT" => {
|
"ARCHIVE" | "ETRANSMIT" => {
|
||||||
use std::path::PathBuf;
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
if let Some(src) = self.tabs[i].current_path.clone() {
|
{
|
||||||
let stem = src
|
use std::path::PathBuf;
|
||||||
.file_stem()
|
if let Some(src) = self.tabs[i].current_path.clone() {
|
||||||
.map(|s| s.to_string_lossy().into_owned())
|
let stem = src
|
||||||
.unwrap_or_else(|| "drawing".into());
|
.file_stem()
|
||||||
let parent = src
|
.map(|s| s.to_string_lossy().into_owned())
|
||||||
.parent()
|
.unwrap_or_else(|| "drawing".into());
|
||||||
.map(|p| p.to_path_buf())
|
let parent = src
|
||||||
.unwrap_or_else(|| PathBuf::from("."));
|
.parent()
|
||||||
let folder = parent.join(format!("{stem}_archive"));
|
.map(|p| p.to_path_buf())
|
||||||
match std::fs::create_dir_all(&folder) {
|
.unwrap_or_else(|| PathBuf::from("."));
|
||||||
Ok(()) => {
|
let folder = parent.join(format!("{stem}_archive"));
|
||||||
let mut copied = 0usize;
|
match std::fs::create_dir_all(&folder) {
|
||||||
if let Some(fname) = src.file_name() {
|
Ok(()) => {
|
||||||
if std::fs::copy(&src, folder.join(fname)).is_ok() {
|
let mut copied = 0usize;
|
||||||
copied += 1;
|
if let Some(fname) = src.file_name() {
|
||||||
}
|
if std::fs::copy(&src, folder.join(fname)).is_ok() {
|
||||||
}
|
copied += 1;
|
||||||
let mut deps: Vec<String> = Vec::new();
|
|
||||||
for br in self.tabs[i].scene.document.block_records.iter() {
|
|
||||||
if !br.xref_path.trim().is_empty() {
|
|
||||||
deps.push(br.xref_path.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for e in self.tabs[i].scene.document.entities() {
|
|
||||||
if let acadrust::EntityType::RasterImage(img) = e {
|
|
||||||
if !img.file_path.trim().is_empty() {
|
|
||||||
deps.push(img.file_path.clone());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
let mut deps: Vec<String> = Vec::new();
|
||||||
for d in deps {
|
for br in self.tabs[i].scene.document.block_records.iter() {
|
||||||
let dp = PathBuf::from(&d);
|
if !br.xref_path.trim().is_empty() {
|
||||||
let resolved = if dp.is_absolute() {
|
deps.push(br.xref_path.clone());
|
||||||
dp
|
}
|
||||||
} else {
|
}
|
||||||
parent.join(&dp)
|
for e in self.tabs[i].scene.document.entities() {
|
||||||
};
|
if let acadrust::EntityType::RasterImage(img) = e {
|
||||||
if resolved.exists() {
|
if !img.file_path.trim().is_empty() {
|
||||||
if let Some(fname) = resolved.file_name() {
|
deps.push(img.file_path.clone());
|
||||||
if std::fs::copy(&resolved, folder.join(fname)).is_ok() {
|
|
||||||
copied += 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for d in deps {
|
||||||
|
let dp = PathBuf::from(&d);
|
||||||
|
let resolved = if dp.is_absolute() {
|
||||||
|
dp
|
||||||
|
} else {
|
||||||
|
parent.join(&dp)
|
||||||
|
};
|
||||||
|
if resolved.exists() {
|
||||||
|
if let Some(fname) = resolved.file_name() {
|
||||||
|
if std::fs::copy(&resolved, folder.join(fname)).is_ok()
|
||||||
|
{
|
||||||
|
copied += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.command_line.push_output(
|
||||||
|
crate::tf!(
|
||||||
|
"{cmd}: packaged {copied} file(s) into {}",
|
||||||
|
folder.display()
|
||||||
|
)
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
self.command_line.push_output(
|
Err(e) => self.command_line.push_error(
|
||||||
crate::tf!(
|
crate::tf!("{cmd}: cannot create folder ({e}).").as_ref(),
|
||||||
"{cmd}: packaged {copied} file(s) into {}",
|
),
|
||||||
folder.display()
|
|
||||||
)
|
|
||||||
.as_ref(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
Err(e) => self
|
} else {
|
||||||
.command_line
|
self.command_line.push_error(
|
||||||
.push_error(crate::tf!("{cmd}: cannot create folder ({e}).").as_ref()),
|
crate::t!("ARCHIVE: save the drawing first (it has no file path yet).")
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
self.command_line.push_error(
|
|
||||||
crate::t!("ARCHIVE: save the drawing first (it has no file path yet).")
|
|
||||||
.as_ref(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
self.archive_web(i, cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
"EXIT" | "QUIT" => {
|
"EXIT" | "QUIT" => {
|
||||||
@@ -499,3 +508,186 @@ fn parse_background_color(args: &[&str]) -> Option<[f32; 4]> {
|
|||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Web ARCHIVE / ETRANSMIT ──────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// The browser has no folder to copy into, so the transmittal is assembled in
|
||||||
|
// memory and downloaded as one ZIP — the same dependency set the native
|
||||||
|
// folder walk collects (drawing, xrefs, raster images) plus the web-only
|
||||||
|
// bytes-backed kinds (PDF underlays, session-picked plot tables). Anything
|
||||||
|
// whose bytes never entered the session is reported as skipped rather than
|
||||||
|
// silently dropped, the web counterpart of native's missing-file skip.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
impl OpenCADStudio {
|
||||||
|
pub(in crate::app) fn archive_web(&mut self, i: usize, cmd: &str) {
|
||||||
|
use crate::io::zip::ZipEntry;
|
||||||
|
|
||||||
|
let Some(src) = self.tabs[i].current_path.clone() else {
|
||||||
|
self.command_line.push_error(
|
||||||
|
crate::t!("ARCHIVE: save the drawing first (it has no file path yet).").as_ref(),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let stem = src
|
||||||
|
.file_stem()
|
||||||
|
.map(|s| s.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| "drawing".into());
|
||||||
|
let ext = src
|
||||||
|
.extension()
|
||||||
|
.map(|e| e.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| "dwg".into());
|
||||||
|
let filename = src
|
||||||
|
.file_name()
|
||||||
|
.map(|n| n.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| format!("{stem}.{ext}"));
|
||||||
|
let version = self.tabs[i].scene.document.version;
|
||||||
|
|
||||||
|
// The drawing itself: the same serialization a web Save performs
|
||||||
|
// (native copies the last-saved file; the web build has none, so the
|
||||||
|
// current document is re-serialized).
|
||||||
|
let drawing = match crate::io::save_to_bytes(&self.tabs[i].scene.document, &ext, version) {
|
||||||
|
Ok(bytes) => bytes,
|
||||||
|
Err(e) => {
|
||||||
|
self.command_line.push_error(
|
||||||
|
crate::tf!("{cmd}: cannot package the drawing ({e}).").as_ref(),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut entries: Vec<ZipEntry> = vec![ZipEntry::new(filename, drawing)];
|
||||||
|
let mut skipped: Vec<String> = Vec::new();
|
||||||
|
|
||||||
|
// Xrefs — the block-record walk native copies. No xref bytes store
|
||||||
|
// exists on the web build (the file behind the path never entered
|
||||||
|
// the session), so every reference reports as skipped.
|
||||||
|
for br in self.tabs[i].scene.document.block_records.iter() {
|
||||||
|
if !br.xref_path.trim().is_empty() {
|
||||||
|
push_skipped(&mut skipped, &transmit_file_name(&br.xref_path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Raster images — picked/dropped bytes registered for the reference
|
||||||
|
// stand in for the file native would copy.
|
||||||
|
for e in self.tabs[i].scene.document.entities() {
|
||||||
|
if let acadrust::EntityType::RasterImage(img) = e {
|
||||||
|
if img.file_path.trim().is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match crate::scene::model::image_model::registered_source_bytes(&img.file_path) {
|
||||||
|
Some(bytes) => push_entry(
|
||||||
|
&mut entries,
|
||||||
|
transmit_file_name(&img.file_path),
|
||||||
|
(*bytes).clone(),
|
||||||
|
),
|
||||||
|
None => push_skipped(&mut skipped, &transmit_file_name(&img.file_path)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PDF underlays — bytes registered at attach time.
|
||||||
|
for o in self.tabs[i].scene.document.objects.values() {
|
||||||
|
if let acadrust::objects::ObjectType::UnderlayDefinition(def) = o {
|
||||||
|
if !matches!(def.underlay_type, acadrust::entities::UnderlayType::Pdf)
|
||||||
|
|| def.file_path.trim().is_empty()
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match crate::scene::model::pdf_raster::registered_source_bytes(&def.file_path) {
|
||||||
|
Some(bytes) => push_entry(
|
||||||
|
&mut entries,
|
||||||
|
transmit_file_name(&def.file_path),
|
||||||
|
(*bytes).clone(),
|
||||||
|
),
|
||||||
|
None => push_skipped(&mut skipped, &transmit_file_name(&def.file_path)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plot tables named by the layouts (and the session's active one).
|
||||||
|
// Only tables picked this session have bytes; the built-ins ship
|
||||||
|
// with the app, so they are neither packaged nor reported missing.
|
||||||
|
let mut style_names: Vec<String> = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.objects
|
||||||
|
.values()
|
||||||
|
.filter_map(|o| {
|
||||||
|
if let acadrust::objects::ObjectType::Layout(l) = o {
|
||||||
|
Some(l.plot_style_sheet.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if let Some(table) = self.active_plot_style.as_ref() {
|
||||||
|
style_names.push(table.name.clone());
|
||||||
|
}
|
||||||
|
for name in style_names {
|
||||||
|
if name.trim().is_empty()
|
||||||
|
|| crate::io::plot_style::PlotStyleTable::builtin(&name).is_ok()
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match crate::io::plot_style::session_table(&name) {
|
||||||
|
Some(bytes) => push_entry(&mut entries, name, (*bytes).clone()),
|
||||||
|
None => push_skipped(&mut skipped, &name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let zip_name = format!("{stem}_transmit.zip");
|
||||||
|
match crate::io::zip::build_zip(&entries) {
|
||||||
|
Ok(zip) => {
|
||||||
|
crate::sys::download_bytes(&zip_name, &zip);
|
||||||
|
// The same report shape the native folder transmittal makes.
|
||||||
|
let copied = entries.len();
|
||||||
|
self.command_line.push_output(
|
||||||
|
crate::tf!("{cmd}: packaged {copied} file(s) into {}", zip_name).as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => self.command_line.push_error(
|
||||||
|
crate::tf!("{cmd}: cannot build the transmittal ZIP ({e}).").as_ref(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
if !skipped.is_empty() {
|
||||||
|
let n = skipped.len();
|
||||||
|
let names = skipped.join(", ");
|
||||||
|
self.command_line.push_output(
|
||||||
|
crate::tf!("{cmd}: skipped {n} referenced file(s) not loaded in this session: {names}")
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trailing component of a reference path — the flat name a transmittal
|
||||||
|
/// packages it under (native copies `file_name()` into one folder).
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn transmit_file_name(path: &str) -> String {
|
||||||
|
path.trim()
|
||||||
|
.rsplit(['/', '\\'])
|
||||||
|
.next()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add one entry under its flat name; a later dependency with the same name
|
||||||
|
/// replaces the bytes (native's `fs::copy` overwrite into the folder).
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn push_entry(entries: &mut Vec<crate::io::zip::ZipEntry>, name: String, data: Vec<u8>) {
|
||||||
|
if let Some(slot) = entries
|
||||||
|
.iter_mut()
|
||||||
|
.find(|entry| entry.name.eq_ignore_ascii_case(&name))
|
||||||
|
{
|
||||||
|
slot.data = data;
|
||||||
|
} else {
|
||||||
|
entries.push(crate::io::zip::ZipEntry::new(name, data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remember a missing dependency for the report, once per name.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn push_skipped(skipped: &mut Vec<String>, name: &str) {
|
||||||
|
if !name.is_empty() && !skipped.iter().any(|s| s.eq_ignore_ascii_case(name)) {
|
||||||
|
skipped.push(name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
-1
@@ -2305,6 +2305,22 @@ pub enum Message {
|
|||||||
bounds: Option<iced::Rectangle>,
|
bounds: Option<iced::Rectangle>,
|
||||||
screenshot: Option<iced::window::Screenshot>,
|
screenshot: Option<iced::window::Screenshot>,
|
||||||
},
|
},
|
||||||
|
/// Web SAVE: the serializer worker (or the inline fallback) produced the
|
||||||
|
/// drawing bytes; download them and finish the save bookkeeping. The
|
||||||
|
/// epoch/revision/camera markers snapshot the tab when serialization
|
||||||
|
/// started — the web counterpart of native's `snapshot_is_current`, so a
|
||||||
|
/// save that raced user edits does not wipe the newer dirty flag.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
WebSaveSerialized {
|
||||||
|
tab_id: u64,
|
||||||
|
filename: String,
|
||||||
|
autosave_key: String,
|
||||||
|
version: acadrust::DxfVersion,
|
||||||
|
geometry_epoch: u64,
|
||||||
|
edit_revision: u64,
|
||||||
|
camera_generation: u64,
|
||||||
|
result: Result<Vec<u8>, String>,
|
||||||
|
},
|
||||||
/// Retry the failed save after the other application releases the file.
|
/// Retry the failed save after the other application releases the file.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
SaveFileInUseRetry,
|
SaveFileInUseRetry,
|
||||||
@@ -3579,7 +3595,9 @@ pub enum Message {
|
|||||||
DataExtractionBrowseSettings,
|
DataExtractionBrowseSettings,
|
||||||
DataExtractionBrowseSettingsResult(Option<std::path::PathBuf>),
|
DataExtractionBrowseSettingsResult(Option<std::path::PathBuf>),
|
||||||
DataExtractionAddDrawings,
|
DataExtractionAddDrawings,
|
||||||
DataExtractionAddDrawingsResult(Vec<std::path::PathBuf>),
|
/// Picked source drawings — `Err` carries a pick/parse failure (web
|
||||||
|
/// parses each file on the worker at pick time).
|
||||||
|
DataExtractionAddDrawingsResult(Result<Vec<std::path::PathBuf>, String>),
|
||||||
DataExtractionAddFolder,
|
DataExtractionAddFolder,
|
||||||
DataExtractionAddFolderResult(Option<std::path::PathBuf>),
|
DataExtractionAddFolderResult(Option<std::path::PathBuf>),
|
||||||
DataExtractionClearSources,
|
DataExtractionClearSources,
|
||||||
|
|||||||
+155
-63
@@ -1429,11 +1429,18 @@ impl OpenCADStudio {
|
|||||||
crate::tf!("XREF Recovered with warnings: \"{}\"", info.name).as_ref(),
|
crate::tf!("XREF Recovered with warnings: \"{}\"", info.name).as_ref(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Web: NotFound xrefs are summarized in one warning after the
|
||||||
|
// loop (see below) — the per-reference line carries a
|
||||||
|
// filesystem path no browser can open, and a drawing with
|
||||||
|
// several unresolved xrefs would repeat the same hint.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
crate::io::xref::XrefStatus::NotFound => {
|
crate::io::xref::XrefStatus::NotFound => {
|
||||||
self.command_line.push_error(
|
self.command_line.push_error(
|
||||||
crate::tf!("XREF Not found: \"{}\" ({})", info.name, info.path).as_ref(),
|
crate::tf!("XREF Not found: \"{}\" ({})", info.name, info.path).as_ref(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
crate::io::xref::XrefStatus::NotFound => {}
|
||||||
crate::io::xref::XrefStatus::Failed => {
|
crate::io::xref::XrefStatus::Failed => {
|
||||||
self.command_line.push_error(
|
self.command_line.push_error(
|
||||||
crate::tf!("XREF Recovery failed: \"{}\" ({})", info.name, info.path)
|
crate::tf!("XREF Recovery failed: \"{}\" ({})", info.name, info.path)
|
||||||
@@ -1447,6 +1454,22 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Web: one warning per open lists every unresolved xref and the way
|
||||||
|
// to fix it — dropping the files on the page registers their bytes,
|
||||||
|
// and reopening the drawing (or XRELOAD) then resolves them.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{
|
||||||
|
let unresolved = crate::io::xref::unresolved_xref_names(&caches.xrefs);
|
||||||
|
if !unresolved.is_empty() {
|
||||||
|
let names = unresolved.join(", ");
|
||||||
|
self.command_line.push_warning(
|
||||||
|
crate::tf!(
|
||||||
|
"XREF Unresolved external references: {names} — drop the referenced DWG/DXF files on the page to register them, then reopen this drawing or run XRELOAD."
|
||||||
|
)
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let thumbs_task = self.push_recent(path.clone());
|
let thumbs_task = self.push_recent(path.clone());
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
@@ -2001,82 +2024,144 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].scene.document.preview = Some(preview);
|
self.tabs[i].scene.document.preview = Some(preview);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut recent_task = Task::none();
|
|
||||||
// The name the autosave snapshot for this tab is keyed by — capture it
|
// The name the autosave snapshot for this tab is keyed by — capture it
|
||||||
// before the save re-points `current_path` at the download name.
|
// before the save re-points `current_path` at the download name.
|
||||||
let autosave_key = Self::web_autosave_key(&self.tabs[i]);
|
let autosave_key = Self::web_autosave_key(&self.tabs[i]);
|
||||||
let saved = match crate::io::save_to_bytes(&self.tabs[i].scene.document, &ext, version) {
|
// Race guard (the web counterpart of native's `snapshot_is_current`):
|
||||||
Ok(bytes) => {
|
// completion only clears the dirty flag if the tab did not change
|
||||||
crate::sys::download_bytes(&filename, &bytes);
|
// while the writer ran off-thread.
|
||||||
let cache_name = std::path::Path::new(&filename)
|
let geometry_epoch = self.tabs[i].scene.geometry_epoch;
|
||||||
.file_name()
|
let edit_revision = self.tabs[i].edit_revision;
|
||||||
.map(|name| name.to_string_lossy().into_owned())
|
let camera_generation = self.tabs[i].scene.camera_generation;
|
||||||
.unwrap_or_else(|| filename.clone());
|
// Prepare the document here (clone + style sync + dimension bake — the
|
||||||
let path = std::path::PathBuf::from(cache_name);
|
// app-side half of the pipeline), then hand the DWG/DXF writer pass to
|
||||||
self.tabs[i].current_path = Some(path.clone());
|
// the browser worker so the UI thread stays responsive on large
|
||||||
self.tabs[i].scene.document.version = version;
|
// drawings. Completion (download + bookkeeping) continues in
|
||||||
self.tabs[i].dirty = false;
|
// `on_web_save_serialized`.
|
||||||
self.tabs[i].recovery_save_as_required = false;
|
let prepared =
|
||||||
recent_task = Task::perform(
|
crate::io::prepare_document_for_save(&self.tabs[i].scene.document, version);
|
||||||
async move {
|
Task::perform(
|
||||||
crate::io::web_recent::store(&path.to_string_lossy(), &bytes)
|
async move { crate::io::serialize_prepared_document(&prepared, &ext).await },
|
||||||
.await
|
move |result| Message::WebSaveSerialized {
|
||||||
.map(|_| path)
|
tab_id,
|
||||||
},
|
filename,
|
||||||
Message::WebRecentStored,
|
autosave_key,
|
||||||
);
|
version,
|
||||||
// A clean save supersedes any crash-recovery snapshot for this
|
geometry_epoch,
|
||||||
// drawing — drop the old key and the new one so stale copies
|
edit_revision,
|
||||||
// don't resurface in the next boot scan. But an autosave
|
camera_generation,
|
||||||
// round may still be writing the same key: its write would
|
result,
|
||||||
// land AFTER the remove and resurrect the snapshot (with a
|
},
|
||||||
// newer mtime, so the next scan would offer it as
|
)
|
||||||
// recoverable). Defer the removes to the end of the round.
|
}
|
||||||
let new_key = self.tabs[i]
|
|
||||||
.current_path
|
/// Web SAVE completion: the serializer worker (or inline fallback) has the
|
||||||
.as_ref()
|
/// drawing bytes. The download and tab bookkeeping that used to run
|
||||||
.map(|path| path.to_string_lossy().into_owned())
|
/// synchronously inside `on_web_save_screenshot`, unchanged in semantics —
|
||||||
.unwrap_or_default();
|
/// except the dirty flag is only cleared when the serialized snapshot is
|
||||||
if self.web_autosave_in_flight {
|
/// still the tab's current state.
|
||||||
self.web_autosave_clear_after.push(autosave_key);
|
#[cfg(target_arch = "wasm32")]
|
||||||
if !new_key.is_empty() {
|
pub(super) fn on_web_save_serialized(
|
||||||
self.web_autosave_clear_after.push(new_key);
|
&mut self,
|
||||||
}
|
tab_id: u64,
|
||||||
} else {
|
filename: String,
|
||||||
recent_task = Task::batch([
|
autosave_key: String,
|
||||||
recent_task,
|
version: acadrust::DxfVersion,
|
||||||
Task::perform(
|
geometry_epoch: u64,
|
||||||
async move {
|
edit_revision: u64,
|
||||||
let _ = crate::io::web_autosave::remove(&autosave_key).await;
|
camera_generation: u64,
|
||||||
let _ = crate::io::web_autosave::remove(&new_key).await;
|
result: Result<Vec<u8>, String>,
|
||||||
},
|
) -> Task<Message> {
|
||||||
|_| Message::Noop,
|
let bytes = match result {
|
||||||
),
|
Ok(bytes) => bytes,
|
||||||
]);
|
|
||||||
}
|
|
||||||
self.command_line
|
|
||||||
.push_output(crate::tf!("Saved: {filename}").as_ref());
|
|
||||||
true
|
|
||||||
}
|
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_error(crate::tf!("Save failed: {error}").as_ref());
|
.push_error(crate::tf!("Save failed: {error}").as_ref());
|
||||||
false
|
return self.finish_web_save(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// The download is the only artifact of the save — fire it even if the
|
||||||
|
// tab was closed while serialization was in flight.
|
||||||
|
crate::sys::download_bytes(&filename, &bytes);
|
||||||
|
let cache_name = std::path::Path::new(&filename)
|
||||||
|
.file_name()
|
||||||
|
.map(|name| name.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| filename.clone());
|
||||||
|
let path = std::path::PathBuf::from(cache_name);
|
||||||
|
let mut recent_task = Task::perform(
|
||||||
|
async move {
|
||||||
|
crate::io::web_recent::store(&path.to_string_lossy(), &bytes)
|
||||||
|
.await
|
||||||
|
.map(|_| path)
|
||||||
|
},
|
||||||
|
Message::WebRecentStored,
|
||||||
|
);
|
||||||
|
let Some(i) = self.tabs.iter().position(|tab| tab.id == tab_id) else {
|
||||||
|
return recent_task;
|
||||||
|
};
|
||||||
|
self.tabs[i].current_path = Some(
|
||||||
|
std::path::Path::new(&filename)
|
||||||
|
.file_name()
|
||||||
|
.map(|name| std::path::PathBuf::from(name.to_string_lossy().into_owned()))
|
||||||
|
.unwrap_or_else(|| std::path::PathBuf::from(filename.clone())),
|
||||||
|
);
|
||||||
|
self.tabs[i].scene.document.version = version;
|
||||||
|
self.tabs[i].recovery_save_as_required = false;
|
||||||
|
let snapshot_is_current = self.tabs[i].scene.geometry_epoch == geometry_epoch
|
||||||
|
&& self.tabs[i].edit_revision == edit_revision
|
||||||
|
&& self.tabs[i].scene.camera_generation == camera_generation;
|
||||||
|
if snapshot_is_current {
|
||||||
|
self.tabs[i].dirty = false;
|
||||||
|
}
|
||||||
|
// A clean save supersedes any crash-recovery snapshot for this
|
||||||
|
// drawing — drop the old key and the new one so stale copies don't
|
||||||
|
// resurface in the next boot scan. But an autosave round may still be
|
||||||
|
// writing the same key: its write would land AFTER the remove and
|
||||||
|
// resurrect the snapshot (with a newer mtime, so the next scan would
|
||||||
|
// offer it as recoverable). Defer the removes to the end of the round.
|
||||||
|
let new_key = self.tabs[i]
|
||||||
|
.current_path
|
||||||
|
.as_ref()
|
||||||
|
.map(|path| path.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_default();
|
||||||
|
if self.web_autosave_in_flight {
|
||||||
|
self.web_autosave_clear_after.push(autosave_key);
|
||||||
|
if !new_key.is_empty() {
|
||||||
|
self.web_autosave_clear_after.push(new_key);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
recent_task = Task::batch([
|
||||||
|
recent_task,
|
||||||
|
Task::perform(
|
||||||
|
async move {
|
||||||
|
let _ = crate::io::web_autosave::remove(&autosave_key).await;
|
||||||
|
let _ = crate::io::web_autosave::remove(&new_key).await;
|
||||||
|
},
|
||||||
|
|_| Message::Noop,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
self.command_line
|
||||||
|
.push_output(crate::tf!("Saved: {filename}").as_ref());
|
||||||
|
Task::batch([recent_task, self.finish_web_save(true)])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tail of the web save flow: continue a pending close when the save
|
||||||
|
/// succeeded, or re-open the unsaved-changes dialog when it failed.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn finish_web_save(&mut self, saved: bool) -> Task<Message> {
|
||||||
if self.save_dialog_for_unsaved {
|
if self.save_dialog_for_unsaved {
|
||||||
if saved {
|
if saved {
|
||||||
if let Some(crate::app::PendingClose::Tab(index)) = self.pending_close.take() {
|
if let Some(crate::app::PendingClose::Tab(index)) = self.pending_close.take() {
|
||||||
let continuation = self.update(Message::TabClose(index));
|
let continuation = self.update(Message::TabClose(index));
|
||||||
let rest = self.continue_tab_close_queue();
|
let rest = self.continue_tab_close_queue();
|
||||||
return Task::batch([recent_task, continuation, rest]);
|
return Task::batch([continuation, rest]);
|
||||||
}
|
}
|
||||||
} else if self.pending_close.is_some() {
|
} else if self.pending_close.is_some() {
|
||||||
let retry = self.open_unsaved_dialog_window();
|
return self.open_unsaved_dialog_window();
|
||||||
return Task::batch([recent_task, retry]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
recent_task
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
@@ -3017,7 +3102,10 @@ impl OpenCADStudio {
|
|||||||
/// native `.sv$` copy — best-effort and non-destructive: no download, no
|
/// native `.sv$` copy — best-effort and non-destructive: no download, no
|
||||||
/// recents update, dirty flag untouched. Snapshots run serially in one
|
/// recents update, dirty flag untouched. Snapshots run serially in one
|
||||||
/// round; while it lasts (or a web save's thumbnail capture is pending)
|
/// round; while it lasts (or a web save's thumbnail capture is pending)
|
||||||
/// further ticks are dropped, not queued.
|
/// further ticks are dropped, not queued. Each tab is prepared (clone +
|
||||||
|
/// style sync + dimension bake) once up front, then the DWG/DXF writer
|
||||||
|
/// pass runs on the serializer worker — the UI thread only awaits the
|
||||||
|
/// result between OPFS writes.
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub(super) fn on_autosave(&mut self) -> Task<Message> {
|
pub(super) fn on_autosave(&mut self) -> Task<Message> {
|
||||||
if self.web_autosave_in_flight || self.pending_web_thumbnail_save.is_some() {
|
if self.web_autosave_in_flight || self.pending_web_thumbnail_save.is_some() {
|
||||||
@@ -3044,7 +3132,11 @@ impl OpenCADStudio {
|
|||||||
let is_dxf = crate::io::source_is_dxf(tab.current_path.as_deref(), &tab.scene.document);
|
let is_dxf = crate::io::source_is_dxf(tab.current_path.as_deref(), &tab.scene.document);
|
||||||
let ext = if is_dxf { "dxf" } else { "dwg" }.to_string();
|
let ext = if is_dxf { "dxf" } else { "dwg" }.to_string();
|
||||||
let version = tab.scene.document.version;
|
let version = tab.scene.document.version;
|
||||||
pending.push((name, ext, version, tab.scene.document.clone()));
|
pending.push((
|
||||||
|
name,
|
||||||
|
ext,
|
||||||
|
crate::io::prepare_document_for_save(&tab.scene.document, version),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
if pending.is_empty() {
|
if pending.is_empty() {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
@@ -3053,10 +3145,10 @@ impl OpenCADStudio {
|
|||||||
Task::perform(
|
Task::perform(
|
||||||
async move {
|
async move {
|
||||||
let mut failure = None;
|
let mut failure = None;
|
||||||
for (name, ext, version, doc) in pending {
|
for (name, ext, prepared) in pending {
|
||||||
// Autosave exists to recover geometry, not pretty recents:
|
// Autosave exists to recover geometry, not pretty recents:
|
||||||
// no thumbnail capture, just bytes into OPFS.
|
// no thumbnail capture, just bytes into OPFS.
|
||||||
match crate::io::save_to_bytes(&doc, &ext, version) {
|
match crate::io::serialize_prepared_document(&prepared, &ext).await {
|
||||||
Ok(bytes) => {
|
Ok(bytes) => {
|
||||||
if let Err(error) =
|
if let Err(error) =
|
||||||
crate::io::web_autosave::snapshot(&name, &bytes, None).await
|
crate::io::web_autosave::snapshot(&name, &bytes, None).await
|
||||||
|
|||||||
+80
-26
@@ -1143,26 +1143,46 @@ impl OpenCADStudio {
|
|||||||
|
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::XAttachPick => Task::perform(
|
Message::XAttachPick => {
|
||||||
async {
|
// Web: the browser hands back bytes, not a path — read them,
|
||||||
let handle = crate::sys::file_dialog()
|
// register them name-keyed (so reloads and re-opens of the
|
||||||
.set_title(crate::t!("Select External Reference File").as_ref())
|
// host drawing re-resolve the xref in-session) and pre-parse
|
||||||
.add_filter(
|
// on the worker in one step. The returned name-only PathBuf
|
||||||
crate::t!("CAD Files").as_ref(),
|
// is what the native branch's `handle_path` yields there.
|
||||||
&["dwg", "dxf", "bak", "DWG", "DXF", "BAK"],
|
#[cfg(target_arch = "wasm32")]
|
||||||
)
|
{
|
||||||
.add_filter(crate::t!("DWG Files").as_ref(), &["dwg", "DWG"])
|
Task::perform(
|
||||||
.add_filter(crate::t!("DXF Files").as_ref(), &["dxf", "DXF"])
|
crate::io::pick_and_register_xref_web(),
|
||||||
.add_filter(crate::t!("Backup Files").as_ref(), &["bak", "BAK"])
|
Message::XAttachPickResult,
|
||||||
.pick_file()
|
)
|
||||||
.await;
|
}
|
||||||
match handle {
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
Some(h) => Ok(crate::sys::handle_path(&h)),
|
{
|
||||||
None => Err("Cancelled".to_string()),
|
Task::perform(
|
||||||
}
|
async {
|
||||||
},
|
let handle = crate::sys::file_dialog()
|
||||||
Message::XAttachPickResult,
|
.set_title(crate::t!("Select External Reference File").as_ref())
|
||||||
),
|
.add_filter(
|
||||||
|
crate::t!("CAD Files").as_ref(),
|
||||||
|
&["dwg", "dxf", "bak", "DWG", "DXF", "BAK"],
|
||||||
|
)
|
||||||
|
.add_filter(crate::t!("DWG Files").as_ref(), &["dwg", "DWG"])
|
||||||
|
.add_filter(crate::t!("DXF Files").as_ref(), &["dxf", "DXF"])
|
||||||
|
.add_filter(
|
||||||
|
crate::t!("Backup Files").as_ref(),
|
||||||
|
&["bak", "BAK"],
|
||||||
|
)
|
||||||
|
.pick_file()
|
||||||
|
.await;
|
||||||
|
match handle {
|
||||||
|
Some(h) => Ok(crate::sys::handle_path(&h)),
|
||||||
|
None => Err("Cancelled".to_string()),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Message::XAttachPickResult,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Message::XAttachPickResult(Ok(path)) => {
|
Message::XAttachPickResult(Ok(path)) => {
|
||||||
use crate::command::CadCommand;
|
use crate::command::CadCommand;
|
||||||
@@ -1255,9 +1275,13 @@ impl OpenCADStudio {
|
|||||||
self.on_data_extraction_browse_settings_result(path)
|
self.on_data_extraction_browse_settings_result(path)
|
||||||
}
|
}
|
||||||
Message::DataExtractionAddDrawings => self.on_data_extraction_add_drawings(),
|
Message::DataExtractionAddDrawings => self.on_data_extraction_add_drawings(),
|
||||||
Message::DataExtractionAddDrawingsResult(paths) => {
|
Message::DataExtractionAddDrawingsResult(Ok(paths)) => {
|
||||||
self.on_data_extraction_add_drawings_result(paths)
|
self.on_data_extraction_add_drawings_result(paths)
|
||||||
}
|
}
|
||||||
|
Message::DataExtractionAddDrawingsResult(Err(error)) => {
|
||||||
|
self.data_extraction.error = error;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
Message::DataExtractionAddFolder => self.on_data_extraction_add_folder(),
|
Message::DataExtractionAddFolder => self.on_data_extraction_add_folder(),
|
||||||
Message::DataExtractionAddFolderResult(path) => {
|
Message::DataExtractionAddFolderResult(path) => {
|
||||||
self.on_data_extraction_add_folder_result(path)
|
self.on_data_extraction_add_folder_result(path)
|
||||||
@@ -1291,13 +1315,22 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Message::DataExtractionSaveResult(csv, Some(path)) => {
|
Message::DataExtractionSaveResult(csv, Some(path)) => {
|
||||||
match std::fs::write(&path, csv.as_bytes()) {
|
let fname = path
|
||||||
|
.file_name()
|
||||||
|
.map(|n| n.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| path.to_string_lossy().into_owned());
|
||||||
|
// The browser has no writable path behind the picked name:
|
||||||
|
// the CSV leaves as a download instead of an fs::write.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
let write_result: Result<(), String> = {
|
||||||
|
crate::sys::download_bytes(&fname, csv.as_bytes());
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
let write_result = std::fs::write(&path, csv.as_bytes()).map_err(|e| e.to_string());
|
||||||
|
match write_result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let rows = csv.lines().count().saturating_sub(1);
|
let rows = csv.lines().count().saturating_sub(1);
|
||||||
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(
|
self.command_line.push_output(
|
||||||
crate::tf!("DATAEXTRACTION {rows} rows → \"{fname}\"").as_ref(),
|
crate::tf!("DATAEXTRACTION {rows} rows → \"{fname}\"").as_ref(),
|
||||||
);
|
);
|
||||||
@@ -8205,6 +8238,27 @@ impl OpenCADStudio {
|
|||||||
screenshot,
|
screenshot,
|
||||||
} => self.on_web_save_screenshot(tab_id, filename, ext, version, bounds, screenshot),
|
} => self.on_web_save_screenshot(tab_id, filename, ext, version, bounds, screenshot),
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
Message::WebSaveSerialized {
|
||||||
|
tab_id,
|
||||||
|
filename,
|
||||||
|
autosave_key,
|
||||||
|
version,
|
||||||
|
geometry_epoch,
|
||||||
|
edit_revision,
|
||||||
|
camera_generation,
|
||||||
|
result,
|
||||||
|
} => self.on_web_save_serialized(
|
||||||
|
tab_id,
|
||||||
|
filename,
|
||||||
|
autosave_key,
|
||||||
|
version,
|
||||||
|
geometry_epoch,
|
||||||
|
edit_revision,
|
||||||
|
camera_generation,
|
||||||
|
result,
|
||||||
|
),
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
Message::SaveFileInUseRetry => self.on_save_file_in_use_retry(),
|
Message::SaveFileInUseRetry => self.on_save_file_in_use_retry(),
|
||||||
|
|
||||||
|
|||||||
+296
-31
@@ -30,13 +30,19 @@ pub mod xref;
|
|||||||
pub(crate) mod web_autosave;
|
pub(crate) mod web_autosave;
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub(crate) mod web_drop;
|
pub(crate) mod web_drop;
|
||||||
|
// Minimal in-memory ZIP writer (classic format, flate2 CRC/deflate) used by
|
||||||
|
// the web ARCHIVE/ETRANSMIT transmittal download.
|
||||||
|
pub mod zip;
|
||||||
|
|
||||||
use crate::scene::DerivedCaches;
|
use crate::scene::DerivedCaches;
|
||||||
use acadrust::entities::EntityType;
|
use acadrust::entities::EntityType;
|
||||||
|
// The path-based readers are native-only; web parsing runs through the
|
||||||
|
// parse worker / the xref bytes registry instead.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use acadrust::io::dwg::DwgReader;
|
use acadrust::io::dwg::DwgReader;
|
||||||
use acadrust::{
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
CadDocument, DwgReadOptions, DwgWriter, DxfReader, DxfReaderConfiguration, DxfWriter,
|
use acadrust::{DwgReadOptions, DxfReader, DxfReaderConfiguration};
|
||||||
};
|
use acadrust::{CadDocument, DwgWriter, DxfWriter};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::atomic::{AtomicU16, AtomicU32, AtomicU8, Ordering};
|
use std::sync::atomic::{AtomicU16, AtomicU32, AtomicU8, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -563,6 +569,10 @@ pub async fn pick_and_load_web(progress: Arc<OpenProgressState>) -> WebOpenOutco
|
|||||||
let name = handle.file_name();
|
let name = handle.file_name();
|
||||||
progress.set(crate::app::OPEN_PHASE_READING, 500, 1, 2);
|
progress.set(crate::app::OPEN_PHASE_READING, 500, 1, 2);
|
||||||
let bytes: Arc<[u8]> = Arc::from(handle.read().await);
|
let bytes: Arc<[u8]> = Arc::from(handle.read().await);
|
||||||
|
// The opened drawing's own bytes enter the xref registry too: another
|
||||||
|
// open drawing may reference it by name, and the registry — not the
|
||||||
|
// absent filesystem — is what re-resolves that reference in-session.
|
||||||
|
crate::io::xref::register_source(&name, Arc::new(bytes.to_vec()));
|
||||||
let size_bytes = bytes.len() as u64;
|
let size_bytes = bytes.len() as u64;
|
||||||
let result = load_web_bytes(&name, &bytes, progress.clone(), false, "", None).await;
|
let result = load_web_bytes(&name, &bytes, progress.clone(), false, "", None).await;
|
||||||
let keep_for_recovery = result
|
let keep_for_recovery = result
|
||||||
@@ -712,9 +722,16 @@ async fn load_web_bytes(
|
|||||||
recovery_available: true,
|
recovery_available: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// XREFs inside the opened drawing resolve from the in-session bytes
|
||||||
|
// registry (see `resolve_web_xrefs`) before the caches snapshot the
|
||||||
|
// document, so merged xref geometry is part of the caches — the same
|
||||||
|
// phase order the native open uses.
|
||||||
|
let (xref_infos, xref_dropped) = resolve_web_xrefs(&mut doc, progress.clone()).await;
|
||||||
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;
|
||||||
caches.read_stats = Some(outcome.stats);
|
caches.read_stats = Some(outcome.stats);
|
||||||
|
caches.xrefs = xref_infos;
|
||||||
|
caches.xref_dropped = xref_dropped;
|
||||||
if source_sha256.is_none() && recovery_fingerprint_needed(&caches) {
|
if source_sha256.is_none() && recovery_fingerprint_needed(&caches) {
|
||||||
source_sha256 = web_worker::sha256_document(bytes).await.ok();
|
source_sha256 = web_worker::sha256_document(bytes).await.ok();
|
||||||
}
|
}
|
||||||
@@ -768,6 +785,91 @@ pub async fn pick_and_parse_web(
|
|||||||
Ok(Some((name, document)))
|
Ok(Some((name, document)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Web: parse drawing bytes already in hand on the shared worker — the
|
||||||
|
/// byte-level counterpart of `load_file` for callers that ran their own
|
||||||
|
/// picker (Data Extraction's multi-file source list). Same normalization as
|
||||||
|
/// every other web-opened drawing.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub async fn parse_drawing_bytes_web(name: &str, bytes: &[u8]) -> Result<CadDocument, String> {
|
||||||
|
parse_web_document(name, bytes).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Web: resolve the XREF blocks inside a freshly opened drawing against the
|
||||||
|
/// in-session bytes registry — the browser counterpart of the native open's
|
||||||
|
/// xref phase. Every referenced file whose bytes are registered (picked or
|
||||||
|
/// dropped earlier this session) is parsed on the shared worker first, so
|
||||||
|
/// the UI thread only runs the (cheap) merge; unregistered references come
|
||||||
|
/// back as `NotFound` for the open handler to warn about.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
async fn resolve_web_xrefs(
|
||||||
|
doc: &mut CadDocument,
|
||||||
|
progress: Arc<OpenProgressState>,
|
||||||
|
) -> (Vec<crate::io::xref::XrefInfo>, usize) {
|
||||||
|
let pending: Vec<String> = doc
|
||||||
|
.block_records
|
||||||
|
.iter()
|
||||||
|
.filter(|br| (br.flags.is_xref || br.flags.is_xref_overlay) && !br.xref_path.is_empty())
|
||||||
|
.map(|br| br.xref_path.clone())
|
||||||
|
.collect();
|
||||||
|
let total = pending.len().max(1);
|
||||||
|
for (index, raw_path) in pending.iter().enumerate() {
|
||||||
|
progress.set(crate::app::OPEN_PHASE_XREF, 5900, index, total);
|
||||||
|
let Some(key) = crate::io::xref::find_registered_source(raw_path) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(bytes) = crate::io::xref::registered_bytes(&key) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
// The strict worker refuses recoverable parses; those fall through
|
||||||
|
// to a direct registry read inside the resolve so their stats (and
|
||||||
|
// the Failed status derived from them) match the native loader.
|
||||||
|
if let Ok((outcome, _)) = web_worker::parse_document(&key, &bytes, false, "").await {
|
||||||
|
crate::io::xref::register_parsed(&key, outcome);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
progress.set(crate::app::OPEN_PHASE_XREF, 5900, total, total);
|
||||||
|
// `set_fraction` is native-only; the web phase reports completed/total
|
||||||
|
// directly, which the open overlay surfaces alongside the fixed weight.
|
||||||
|
let xref_progress: Arc<dyn Fn(usize, usize) + Send + Sync> = {
|
||||||
|
let progress = Arc::clone(&progress);
|
||||||
|
Arc::new(move |completed, total| {
|
||||||
|
progress.set(crate::app::OPEN_PHASE_XREF, 5900, completed, total);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
crate::io::xref::resolve_xrefs_with_progress(doc, Path::new(""), Some(xref_progress))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Web XATTACH pick: read the chosen drawing's bytes, keep them in the xref
|
||||||
|
/// registry (so later reloads and re-opens of the host drawing re-resolve
|
||||||
|
/// the reference in-session), and pre-parse on the worker so the
|
||||||
|
/// commit-time merge stays cheap. A parse failure defers to commit time,
|
||||||
|
/// which re-reads the registered bytes and reports the same Failed status
|
||||||
|
/// the native attach would. `Err("Cancelled")` mirrors the native pick.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub async fn pick_and_register_xref_web() -> Result<PathBuf, String> {
|
||||||
|
let Some(handle) = crate::sys::file_dialog()
|
||||||
|
.set_title(crate::t!("Select External Reference File").as_ref())
|
||||||
|
.add_filter(
|
||||||
|
crate::t!("CAD Files").as_ref(),
|
||||||
|
&["dwg", "dxf", "bak", "DWG", "DXF", "BAK"],
|
||||||
|
)
|
||||||
|
.add_filter(crate::t!("DWG Files").as_ref(), &["dwg", "DWG"])
|
||||||
|
.add_filter(crate::t!("DXF Files").as_ref(), &["dxf", "DXF"])
|
||||||
|
.add_filter(crate::t!("Backup Files").as_ref(), &["bak", "BAK"])
|
||||||
|
.pick_file()
|
||||||
|
.await
|
||||||
|
else {
|
||||||
|
return Err("Cancelled".to_string());
|
||||||
|
};
|
||||||
|
let name = handle.file_name();
|
||||||
|
let bytes = handle.read().await;
|
||||||
|
crate::io::xref::register_source(&name, Arc::new(bytes.clone()));
|
||||||
|
if let Ok((outcome, _)) = web_worker::parse_document(&name, &bytes, false, "").await {
|
||||||
|
crate::io::xref::register_parsed(&name, outcome);
|
||||||
|
}
|
||||||
|
Ok(PathBuf::from(name))
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse a drawing from bytes using its name or recovery-file signature.
|
/// Parse a drawing from bytes using its name or recovery-file signature.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn load_bytes(name: &str, bytes: Vec<u8>) -> Result<CadDocument, String> {
|
pub fn load_bytes(name: &str, bytes: Vec<u8>) -> Result<CadDocument, String> {
|
||||||
@@ -819,6 +921,8 @@ pub fn load_bytes_finalized(path: &Path, bytes: Vec<u8>) -> Result<(CadDocument,
|
|||||||
/// Load a DWG or DXF file directly from a path (auto-detect by extension).
|
/// Load a DWG or DXF file directly from a path (auto-detect by extension).
|
||||||
/// Peek at a file's leading bytes to tell a DWG (version tag "AC10xx") from a
|
/// Peek at a file's leading bytes to tell a DWG (version tag "AC10xx") from a
|
||||||
/// DXF. Used for `.bak` copies, whose extension hides the real format.
|
/// DXF. Used for `.bak` copies, whose extension hides the real format.
|
||||||
|
/// Native-only: the web reads registered bytes instead of the filesystem.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn sniff_dwg_or_dxf(path: &Path) -> String {
|
fn sniff_dwg_or_dxf(path: &Path) -> String {
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
let mut buf = [0u8; 6];
|
let mut buf = [0u8; 6];
|
||||||
@@ -854,6 +958,10 @@ pub fn load_file(_path: &Path) -> Result<CadDocument, String> {
|
|||||||
Err(crate::t!("Opening a drawing by path is not available in the browser.").into_owned())
|
Err(crate::t!("Opening a drawing by path is not available in the browser.").into_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Path-based reader with progress — native-only: on the web xrefs resolve
|
||||||
|
/// from the bytes registry (`io::xref::read_registered_outcome`), never a
|
||||||
|
/// filesystem path.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub(crate) fn load_file_with_progress(
|
pub(crate) fn load_file_with_progress(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
_progress: Option<Arc<dyn Fn(u16) + Send + Sync>>,
|
_progress: Option<Arc<dyn Fn(u16) + Send + Sync>>,
|
||||||
@@ -950,12 +1058,14 @@ fn load_file_for_open(
|
|||||||
finalize_loaded_outcome(path, outcome).map_err(OpenAttemptFailure::from)
|
finalize_loaded_outcome(path, outcome).map_err(OpenAttemptFailure::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
struct ReaderFailure {
|
struct ReaderFailure {
|
||||||
message: String,
|
message: String,
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
recoverable: bool,
|
recoverable: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
impl ReaderFailure {
|
impl ReaderFailure {
|
||||||
fn from_reader(error: acadrust::DxfError) -> Self {
|
fn from_reader(error: acadrust::DxfError) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -1016,6 +1126,7 @@ fn merge_read_stats(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn read_file_attempt(
|
fn read_file_attempt(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
progress: Option<Arc<dyn Fn(u16) + Send + Sync>>,
|
progress: Option<Arc<dyn Fn(u16) + Send + Sync>>,
|
||||||
@@ -1043,6 +1154,7 @@ fn read_file_attempt(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn finalize_loaded_outcome(
|
fn finalize_loaded_outcome(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
mut outcome: acadrust::ReadOutcome,
|
mut outcome: acadrust::ReadOutcome,
|
||||||
@@ -1060,6 +1172,7 @@ fn finalize_loaded_outcome(
|
|||||||
Ok(outcome)
|
Ok(outcome)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn read_dwg_path(
|
fn read_dwg_path(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
progress: Option<Arc<dyn Fn(u16) + Send + Sync>>,
|
progress: Option<Arc<dyn Fn(u16) + Send + Sync>>,
|
||||||
@@ -1070,21 +1183,18 @@ fn read_dwg_path(
|
|||||||
} else {
|
} else {
|
||||||
DwgReadOptions::default()
|
DwgReadOptions::default()
|
||||||
};
|
};
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
|
||||||
let mut reader = {
|
let mut reader = {
|
||||||
let mut reader = DwgReader::from_mmap(path).map_err(ReaderFailure::from_reader)?;
|
let mut reader = DwgReader::from_mmap(path).map_err(ReaderFailure::from_reader)?;
|
||||||
reader.options = options;
|
reader.options = options;
|
||||||
reader
|
reader
|
||||||
};
|
};
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
let mut reader =
|
|
||||||
DwgReader::from_file_with_options(path, options).map_err(ReaderFailure::from_reader)?;
|
|
||||||
if let Some(progress) = progress {
|
if let Some(progress) = progress {
|
||||||
reader.set_progress_callback(progress);
|
reader.set_progress_callback(progress);
|
||||||
}
|
}
|
||||||
reader.read_with_stats().map_err(ReaderFailure::from_reader)
|
reader.read_with_stats().map_err(ReaderFailure::from_reader)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn read_dxf_path(path: &Path, failsafe: bool) -> Result<acadrust::ReadOutcome, ReaderFailure> {
|
fn read_dxf_path(path: &Path, failsafe: bool) -> Result<acadrust::ReadOutcome, ReaderFailure> {
|
||||||
DxfReader::from_file(path)
|
DxfReader::from_file(path)
|
||||||
.map_err(ReaderFailure::from_reader)?
|
.map_err(ReaderFailure::from_reader)?
|
||||||
@@ -1157,6 +1267,9 @@ fn normalize_block_origins(doc: &mut CadDocument) {
|
|||||||
/// that doesn't exist on this machine. Resolve each image to a usable path:
|
/// that doesn't exist on this machine. Resolve each image to a usable path:
|
||||||
/// the stored path if it exists, otherwise the same file name next to the
|
/// the stored path if it exists, otherwise the same file name next to the
|
||||||
/// drawing — and write the result onto the entity so the renderer finds it.
|
/// drawing — and write the result onto the entity so the renderer finds it.
|
||||||
|
/// Native-only: its callers resolve against a real folder; web images bind
|
||||||
|
/// through the name-keyed byte registry instead.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn resolve_raster_image_paths(doc: &mut CadDocument, base_dir: Option<&Path>) {
|
fn resolve_raster_image_paths(doc: &mut CadDocument, base_dir: Option<&Path>) {
|
||||||
use acadrust::objects::ObjectType;
|
use acadrust::objects::ObjectType;
|
||||||
use acadrust::EntityType;
|
use acadrust::EntityType;
|
||||||
@@ -1949,6 +2062,13 @@ pub fn save(doc: &CadDocument, path: &Path) -> Result<(), String> {
|
|||||||
/// `doc.version`). Used by the web build, which hands the bytes to a browser
|
/// `doc.version`). Used by the web build, which hands the bytes to a browser
|
||||||
/// download instead of writing a path — the byte-buffer counterpart to
|
/// download instead of writing a path — the byte-buffer counterpart to
|
||||||
/// [`save_as_version`].
|
/// [`save_as_version`].
|
||||||
|
///
|
||||||
|
/// This is the *inline* path (main thread on wasm). The web SAVE/AUTOSAVE
|
||||||
|
/// flows prefer [`prepare_document_for_save`] +
|
||||||
|
/// [`serialize_prepared_document`], which runs the writer half on the browser
|
||||||
|
/// worker; both routes share the same preparation and the same writer
|
||||||
|
/// dispatch (`ocs_save::serialize_document`), so the produced bytes are
|
||||||
|
/// identical.
|
||||||
#[allow(dead_code)] // web/wasm build only; unused in the native target
|
#[allow(dead_code)] // web/wasm build only; unused in the native target
|
||||||
pub fn save_to_bytes(
|
pub fn save_to_bytes(
|
||||||
doc: &CadDocument,
|
doc: &CadDocument,
|
||||||
@@ -1957,35 +2077,14 @@ pub fn save_to_bytes(
|
|||||||
) -> Result<Vec<u8>, String> {
|
) -> Result<Vec<u8>, String> {
|
||||||
let perf = crate::perf::enabled();
|
let perf = crate::perf::enabled();
|
||||||
let total_started = iced::time::Instant::now();
|
let total_started = iced::time::Instant::now();
|
||||||
let clone_started = iced::time::Instant::now();
|
let doc = prepare_document_for_save(doc, version);
|
||||||
let mut doc = doc.clone();
|
|
||||||
let clone_ms = clone_started.elapsed().as_secs_f64() * 1000.0;
|
|
||||||
doc.version = version;
|
|
||||||
let styles_started = iced::time::Instant::now();
|
|
||||||
sync_current_styles_on_save(&mut doc);
|
|
||||||
let styles_ms = styles_started.elapsed().as_secs_f64() * 1000.0;
|
|
||||||
let dimensions_started = iced::time::Instant::now();
|
|
||||||
crate::modules::draw::modify::explode::bake_dimension_blocks(&mut doc);
|
|
||||||
let dimensions_ms = dimensions_started.elapsed().as_secs_f64() * 1000.0;
|
|
||||||
let write_started = iced::time::Instant::now();
|
let write_started = iced::time::Instant::now();
|
||||||
let result = match ext.to_lowercase().as_str() {
|
let result = ocs_save::serialize_document(&doc, ext);
|
||||||
"dxf" => DxfWriter::new(&doc)
|
|
||||||
.write_to_vec()
|
|
||||||
.map_err(|e| e.to_string()),
|
|
||||||
_ => {
|
|
||||||
let mut buf = std::io::Cursor::new(Vec::new());
|
|
||||||
DwgWriter::write_to_writer(&mut buf, &doc).map_err(|e| e.to_string())?;
|
|
||||||
Ok(buf.into_inner())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if perf {
|
if perf {
|
||||||
let bytes = result.as_ref().map_or(0, Vec::len);
|
let bytes = result.as_ref().map_or(0, Vec::len);
|
||||||
crate::perf_record!(
|
crate::perf_record!(
|
||||||
"[perf] save-bytes total={:.1}ms clone={:.1} styles={:.1} dimensions={:.1} write={:.1} bytes={} entities={} objects={}",
|
"[perf] save-bytes total={:.1}ms write={:.1} bytes={} entities={} objects={}",
|
||||||
total_started.elapsed().as_secs_f64() * 1000.0,
|
total_started.elapsed().as_secs_f64() * 1000.0,
|
||||||
clone_ms,
|
|
||||||
styles_ms,
|
|
||||||
dimensions_ms,
|
|
||||||
write_started.elapsed().as_secs_f64() * 1000.0,
|
write_started.elapsed().as_secs_f64() * 1000.0,
|
||||||
bytes,
|
bytes,
|
||||||
doc.entities().count(),
|
doc.entities().count(),
|
||||||
@@ -1995,6 +2094,65 @@ pub fn save_to_bytes(
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prepare a document clone for saving: stamp the target DXF version, sync the
|
||||||
|
/// current-style markers into their header handles / dictionary variables, and
|
||||||
|
/// bake missing dimension geometry blocks. The app-side half of the save
|
||||||
|
/// pipeline — it must run where the live document lives (the worker crate
|
||||||
|
/// cannot reach the app's style/dimension modules), and it mutates only the
|
||||||
|
/// clone, leaving the live document untouched. Both [`save_to_bytes`] and the
|
||||||
|
/// web worker offload call this exactly once per save, so there is one
|
||||||
|
/// implementation of the preparation for every target.
|
||||||
|
pub fn prepare_document_for_save(
|
||||||
|
doc: &CadDocument,
|
||||||
|
version: acadrust::DxfVersion,
|
||||||
|
) -> CadDocument {
|
||||||
|
let perf = crate::perf::enabled();
|
||||||
|
let clone_started = perf.then(iced::time::Instant::now);
|
||||||
|
let mut doc = doc.clone();
|
||||||
|
let clone_ms = crate::perf::elapsed_ms(clone_started);
|
||||||
|
doc.version = version;
|
||||||
|
let styles_started = perf.then(iced::time::Instant::now);
|
||||||
|
sync_current_styles_on_save(&mut doc);
|
||||||
|
let styles_ms = crate::perf::elapsed_ms(styles_started);
|
||||||
|
let dimensions_started = perf.then(iced::time::Instant::now);
|
||||||
|
crate::modules::draw::modify::explode::bake_dimension_blocks(&mut doc);
|
||||||
|
let dimensions_ms = crate::perf::elapsed_ms(dimensions_started);
|
||||||
|
if perf {
|
||||||
|
crate::perf_record!(
|
||||||
|
"[perf] save-prepare clone={:.1} styles={:.1} dimensions={:.1} entities={} objects={}",
|
||||||
|
clone_ms,
|
||||||
|
styles_ms,
|
||||||
|
dimensions_ms,
|
||||||
|
doc.entities().count(),
|
||||||
|
doc.objects.len(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
doc
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Web SAVE/AUTOSAVE: offload the DWG/DXF writer pass to the browser worker.
|
||||||
|
/// `prepared` comes from [`prepare_document_for_save`] — the serialization
|
||||||
|
/// itself (the expensive half) runs off the UI thread and the encoded bytes
|
||||||
|
/// come back here. Falls back to inline serialization when the worker cannot
|
||||||
|
/// spawn or errors, so a save never hard-fails just because the worker is
|
||||||
|
/// unavailable.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
pub async fn serialize_prepared_document(
|
||||||
|
prepared: &CadDocument,
|
||||||
|
ext: &str,
|
||||||
|
) -> Result<Vec<u8>, String> {
|
||||||
|
let encoded = match ocs_save::encode_request(prepared, ext) {
|
||||||
|
Ok(encoded) => encoded,
|
||||||
|
// The worker cannot help if the document cannot even be encoded —
|
||||||
|
// serialize inline from the document we still hold.
|
||||||
|
Err(error) => {
|
||||||
|
log::warn!("{error}; serializing inline");
|
||||||
|
return ocs_save::serialize_document(prepared, ext);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
web_worker::serialize_document(encoded).await
|
||||||
|
}
|
||||||
|
|
||||||
// ── Post-load fixups ──────────────────────────────────────────────────────
|
// ── Post-load fixups ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
// Resolve the current text / dimension / multiline style from the handle the
|
// Resolve the current text / dimension / multiline style from the handle the
|
||||||
@@ -2695,3 +2853,110 @@ mod corrupt_guard_tests {
|
|||||||
assert!(!is_entity_corrupt(&EntityType::Insert(i)));
|
assert!(!is_entity_corrupt(&EntityType::Insert(i)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Save-pipeline prepare tests ────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// `prepare_document_for_save` is the extracted app-side half of the save
|
||||||
|
// pipeline, shared by the inline `save_to_bytes` and the web worker offload —
|
||||||
|
// these pin the behaviours both routes depend on.
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod prepare_save_tests {
|
||||||
|
use super::*;
|
||||||
|
use acadrust::entities::{Dimension, DimensionLinear};
|
||||||
|
use acadrust::tables::TextStyle;
|
||||||
|
use acadrust::types::Vector3;
|
||||||
|
|
||||||
|
// The live document must never observe the save preparation: version
|
||||||
|
// stamp, style sync and dimension baking all happen on the clone only.
|
||||||
|
#[test]
|
||||||
|
fn prepare_stamps_version_and_leaves_live_document_untouched() {
|
||||||
|
let mut doc = CadDocument::new();
|
||||||
|
doc.version = acadrust::DxfVersion::AC1027;
|
||||||
|
let prepared = prepare_document_for_save(&doc, acadrust::DxfVersion::AC1032);
|
||||||
|
assert_eq!(prepared.version, acadrust::DxfVersion::AC1032);
|
||||||
|
assert_eq!(doc.version, acadrust::DxfVersion::AC1027);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The current text style marker must resolve from its name to the table
|
||||||
|
// entry's handle on the prepared clone (the DWG header stores a handle),
|
||||||
|
// while the live document keeps its previous marker.
|
||||||
|
#[test]
|
||||||
|
fn prepare_syncs_current_style_markers() {
|
||||||
|
let mut doc = CadDocument::new();
|
||||||
|
crate::io::linetypes::populate_document(&mut doc);
|
||||||
|
let mut style = TextStyle::new("ANNOT");
|
||||||
|
style.handle = doc.allocate_handle();
|
||||||
|
let style_handle = style.handle;
|
||||||
|
doc.text_styles.add(style).unwrap();
|
||||||
|
doc.header.current_text_style_name = "ANNOT".to_string();
|
||||||
|
let live_handle = doc.header.current_text_style_handle;
|
||||||
|
let prepared = prepare_document_for_save(&doc, doc.version);
|
||||||
|
assert_eq!(prepared.header.current_text_style_handle, style_handle);
|
||||||
|
assert_eq!(doc.header.current_text_style_handle, live_handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A dimension without its anonymous geometry block must get one baked on
|
||||||
|
// the clone (the writer emits the *D block), with the group-10 definition
|
||||||
|
// point mirrored into `base` — while the live document keeps neither.
|
||||||
|
#[test]
|
||||||
|
fn prepare_bakes_missing_dimension_blocks() {
|
||||||
|
let mut doc = CadDocument::new();
|
||||||
|
crate::io::linetypes::populate_document(&mut doc);
|
||||||
|
let mut dim = DimensionLinear::new(
|
||||||
|
Vector3::new(0.0, 0.0, 0.0),
|
||||||
|
Vector3::new(10.0, 0.0, 0.0),
|
||||||
|
);
|
||||||
|
dim.definition_point = Vector3::new(5.0, 7.0, 0.0);
|
||||||
|
dim.base.definition_point = Vector3::ZERO;
|
||||||
|
dim.base.common.handle = doc.allocate_handle();
|
||||||
|
let handle = dim.base.common.handle;
|
||||||
|
doc.add_entity(EntityType::Dimension(Dimension::Linear(dim)))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let prepared = prepare_document_for_save(&doc, doc.version);
|
||||||
|
let EntityType::Dimension(Dimension::Linear(linear)) =
|
||||||
|
prepared.get_entity(handle).unwrap()
|
||||||
|
else {
|
||||||
|
panic!("dimension missing from prepared clone");
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
linear.base.block_name.starts_with("*D"),
|
||||||
|
"anonymous dimension block not baked: {}",
|
||||||
|
linear.base.block_name
|
||||||
|
);
|
||||||
|
assert!(prepared.block_records.get(&linear.base.block_name).is_some());
|
||||||
|
assert_eq!(linear.base.definition_point, linear.definition_point);
|
||||||
|
|
||||||
|
// The live document is untouched: no block name, stale group-10.
|
||||||
|
let EntityType::Dimension(Dimension::Linear(live)) =
|
||||||
|
doc.get_entity(handle).unwrap()
|
||||||
|
else {
|
||||||
|
panic!("dimension missing from live document");
|
||||||
|
};
|
||||||
|
assert!(live.base.block_name.is_empty());
|
||||||
|
assert_eq!(live.base.definition_point, Vector3::ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The whole point of the split: the inline fallback (`save_to_bytes`) and
|
||||||
|
// the worker route (`prepare_document_for_save` → `ocs_save` on the
|
||||||
|
// worker) must produce identical bytes for the same input.
|
||||||
|
#[test]
|
||||||
|
fn save_to_bytes_equals_prepare_then_shared_serialize() {
|
||||||
|
let mut doc = CadDocument::new();
|
||||||
|
crate::io::linetypes::populate_document(&mut doc);
|
||||||
|
let mut dim = DimensionLinear::new(
|
||||||
|
Vector3::new(0.0, 0.0, 0.0),
|
||||||
|
Vector3::new(10.0, 0.0, 0.0),
|
||||||
|
);
|
||||||
|
dim.base.common.handle = doc.allocate_handle();
|
||||||
|
doc.add_entity(EntityType::Dimension(Dimension::Linear(dim)))
|
||||||
|
.unwrap();
|
||||||
|
for ext in ["dwg", "dxf"] {
|
||||||
|
let inline = save_to_bytes(&doc, ext, acadrust::DxfVersion::AC1032).expect("inline");
|
||||||
|
let prepared = prepare_document_for_save(&doc, acadrust::DxfVersion::AC1032);
|
||||||
|
let shared = ocs_save::serialize_document(&prepared, ext).expect("shared");
|
||||||
|
assert_eq!(inline, shared, "inline and shared serialize diverge for {ext}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -132,8 +132,11 @@ pub fn register_session_table(name: &str, bytes: Arc<Vec<u8>>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The bytes registered under `name` this session, matched
|
/// The bytes registered under `name` this session, matched
|
||||||
/// case-insensitively the way the folder scan matches file names.
|
/// case-insensitively the way the folder scan matches file names. The web
|
||||||
fn session_table(name: &str) -> Option<Arc<Vec<u8>>> {
|
/// ARCHIVE/ETRANSMIT packages a drawing's registered plot table into the
|
||||||
|
/// transmittal; `None` means the table was not picked this session (the
|
||||||
|
/// built-ins ship with the app and need no transmittal copy).
|
||||||
|
pub fn session_table(name: &str) -> Option<Arc<Vec<u8>>> {
|
||||||
session_tables()
|
session_tables()
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||||
|
|||||||
@@ -130,6 +130,11 @@ pub async fn open_dropped(
|
|||||||
bytes: Vec<u8>,
|
bytes: Vec<u8>,
|
||||||
progress: std::sync::Arc<crate::io::OpenProgressState>,
|
progress: std::sync::Arc<crate::io::OpenProgressState>,
|
||||||
) -> crate::io::WebOpenOutcome {
|
) -> crate::io::WebOpenOutcome {
|
||||||
|
// The dropped drawing's bytes also enter the xref registry (name-keyed,
|
||||||
|
// like dropped raster images): another drawing may reference it by
|
||||||
|
// name, and the registry — not the absent filesystem — is what
|
||||||
|
// re-resolves that reference when its host is reopened or reloaded.
|
||||||
|
super::xref::register_source(&name, std::sync::Arc::new(bytes.clone()));
|
||||||
progress.set(crate::app::OPEN_PHASE_READING, 500, 1, 2);
|
progress.set(crate::app::OPEN_PHASE_READING, 500, 1, 2);
|
||||||
let bytes: std::sync::Arc<[u8]> = std::sync::Arc::from(bytes);
|
let bytes: std::sync::Arc<[u8]> = std::sync::Arc::from(bytes);
|
||||||
let size_bytes = bytes.len() as u64;
|
let size_bytes = bytes.len() as u64;
|
||||||
|
|||||||
+92
-44
@@ -7,24 +7,10 @@ use wasm_bindgen::{JsCast, JsValue};
|
|||||||
use web_sys::{ErrorEvent, MessageEvent, Worker, WorkerOptions, WorkerType};
|
use web_sys::{ErrorEvent, MessageEvent, Worker, WorkerOptions, WorkerType};
|
||||||
|
|
||||||
const HASH_MARKER: &str = "\nreport-source-sha256:";
|
const HASH_MARKER: &str = "\nreport-source-sha256:";
|
||||||
const PROTOCOL_VERSION: u16 = 4;
|
/// Version of the shared worker wire format (`ocs_save`); the `?v=` on the
|
||||||
const WORKER_URL: &str = "ocs-parse-worker.js?v=4";
|
/// worker URL cache-busts to match. v5 adds the `serialize` action.
|
||||||
|
const PROTOCOL_VERSION: u16 = ocs_save::PROTOCOL_VERSION;
|
||||||
#[derive(serde::Deserialize)]
|
const WORKER_URL: &str = "ocs-parse-worker.js?v=5";
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) async fn parse_document(
|
pub(super) async fn parse_document(
|
||||||
name: &str,
|
name: &str,
|
||||||
@@ -61,7 +47,7 @@ pub(super) async fn parse_document(
|
|||||||
>,
|
>,
|
||||||
Option<String>,
|
Option<String>,
|
||||||
bool,
|
bool,
|
||||||
Vec<EntityRuntimeFields>,
|
Vec<ocs_save::EntityRuntimeFields>,
|
||||||
) = bincode::deserialize(&bytes)
|
) = bincode::deserialize(&bytes)
|
||||||
.map_err(|error| super::OpenLoadError::from(error.to_string()))?;
|
.map_err(|error| super::OpenLoadError::from(error.to_string()))?;
|
||||||
if payload.0 != PROTOCOL_VERSION {
|
if payload.0 != PROTOCOL_VERSION {
|
||||||
@@ -72,7 +58,7 @@ pub(super) async fn parse_document(
|
|||||||
}
|
}
|
||||||
match payload.1 {
|
match payload.1 {
|
||||||
Ok(mut outcome) => {
|
Ok(mut outcome) => {
|
||||||
restore_entity_runtime_fields(
|
ocs_save::restore_entity_runtime_fields(
|
||||||
&mut outcome.document,
|
&mut outcome.document,
|
||||||
payload.4,
|
payload.4,
|
||||||
);
|
);
|
||||||
@@ -142,30 +128,6 @@ pub(super) async fn parse_document(
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn restore_entity_runtime_fields(
|
|
||||||
document: &mut acadrust::CadDocument,
|
|
||||||
fields: Vec<EntityRuntimeFields>,
|
|
||||||
) {
|
|
||||||
let handle = |value: Option<u64>| value.map(acadrust::Handle::new);
|
|
||||||
for fields in fields {
|
|
||||||
let Some(entity) = document.get_entity_mut(acadrust::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> {
|
pub(super) async fn sha256_document(bytes: &[u8]) -> Result<String, super::OpenLoadError> {
|
||||||
let options = WorkerOptions::new();
|
let options = WorkerOptions::new();
|
||||||
options.set_type(WorkerType::Module);
|
options.set_type(WorkerType::Module);
|
||||||
@@ -233,6 +195,92 @@ fn decode_worker_error(message: String) -> super::OpenLoadError {
|
|||||||
super::OpenLoadError::new(message.to_string(), source_sha256)
|
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 {
|
fn js_error(value: JsValue) -> String {
|
||||||
value
|
value
|
||||||
.as_string()
|
.as_string()
|
||||||
|
|||||||
+321
@@ -7,6 +7,7 @@ use acadrust::types::{Handle, Vector3};
|
|||||||
use acadrust::{CadDocument, EntityType};
|
use acadrust::{CadDocument, EntityType};
|
||||||
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
|
use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::{Arc, Mutex, OnceLock};
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
type SourceFingerprint = crate::io::edit_lock::FileFingerprint;
|
type SourceFingerprint = crate::io::edit_lock::FileFingerprint;
|
||||||
@@ -44,6 +45,198 @@ pub struct XrefInfo {
|
|||||||
pub source_sha256: Option<String>,
|
pub source_sha256: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Every xref whose bytes the browser session holds (picked or dropped),
|
||||||
|
/// named by its trailing file component. Used to assemble the one-line
|
||||||
|
/// "unresolved references" warning after a web open.
|
||||||
|
pub fn unresolved_xref_names(infos: &[XrefInfo]) -> Vec<String> {
|
||||||
|
infos
|
||||||
|
.iter()
|
||||||
|
.filter(|info| info.status == XrefStatus::NotFound)
|
||||||
|
.map(|info| info.name.clone())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── In-session xref byte registry (web) ────────────────────────────────
|
||||||
|
//
|
||||||
|
// The browser exposes no filesystem behind the path a drawing's XREF block
|
||||||
|
// carries, so the web build keeps the bytes of every DWG/DXF the user picked
|
||||||
|
// or dropped, keyed by file name — the xref counterpart of
|
||||||
|
// `image_model::register_source`. Resolution matches a block-record's
|
||||||
|
// `xref_path` against the registry by trailing file name (case-insensitive),
|
||||||
|
// the only component two machines can be expected to agree on. Native builds
|
||||||
|
// never consult the registry: `resolve_path` reads the real filesystem, so
|
||||||
|
// their behaviour is byte-identical to before. The registry itself compiles
|
||||||
|
// on every target so its pure matching logic stays unit-testable on the
|
||||||
|
// native host.
|
||||||
|
|
||||||
|
/// Lower-cased trailing component of a path-style xref reference. Drawings
|
||||||
|
/// store xref paths as the author wrote them (often an absolute foreign
|
||||||
|
/// path), so the file name is the only part a picked or dropped file on
|
||||||
|
/// another machine can be expected to match.
|
||||||
|
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] // unit-tested on native
|
||||||
|
pub(crate) fn file_name_key(path: &str) -> String {
|
||||||
|
path.rsplit(['/', '\\'])
|
||||||
|
.next()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_ascii_lowercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Do two xref references name the same file? Compares trailing components
|
||||||
|
/// case-insensitively (the web has no path beyond the name at all, and the
|
||||||
|
/// Windows paths most drawings were authored on are case-insensitive).
|
||||||
|
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] // unit-tested on native
|
||||||
|
pub(crate) fn same_file_name(a: &str, b: &str) -> bool {
|
||||||
|
let (a, b) = (file_name_key(a), file_name_key(b));
|
||||||
|
!a.is_empty() && a == b
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_cache() -> &'static Mutex<HashMap<String, Arc<Vec<u8>>>> {
|
||||||
|
static SOURCES: OnceLock<Mutex<HashMap<String, Arc<Vec<u8>>>>> = OnceLock::new();
|
||||||
|
SOURCES.get_or_init(|| Mutex::new(HashMap::default()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parsed_cache() -> &'static Mutex<HashMap<String, acadrust::ReadOutcome>> {
|
||||||
|
static PARSED: OnceLock<Mutex<HashMap<String, acadrust::ReadOutcome>>> = OnceLock::new();
|
||||||
|
PARSED.get_or_init(|| Mutex::new(HashMap::default()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keep a picked or dropped drawing's bytes, keyed by the file name the
|
||||||
|
/// drawing's xref references will be matched against. Registering supersedes
|
||||||
|
/// any eager parse cached for the same file, so a re-pick (or a drop that
|
||||||
|
/// finally supplies a missing reference) reloads from the new bytes.
|
||||||
|
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] // mirrors image_model::register_source
|
||||||
|
pub fn register_source(name: &str, bytes: Arc<Vec<u8>>) {
|
||||||
|
source_cache()
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||||
|
.insert(name.to_string(), bytes);
|
||||||
|
parsed_cache()
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||||
|
.retain(|parsed, _| !same_file_name(parsed, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cache an off-thread parse (web parse worker) for the next resolve to
|
||||||
|
/// merge. The entry is one-shot: a resolve consumes it, so a later reload
|
||||||
|
/// re-reads the registered bytes instead of replaying a stale document.
|
||||||
|
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
|
||||||
|
pub(crate) fn register_parsed(name: &str, outcome: acadrust::ReadOutcome) {
|
||||||
|
parsed_cache()
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||||
|
.insert(name.to_string(), outcome);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registry key whose trailing file name matches `raw` (case-insensitive) —
|
||||||
|
/// the web stand-in for `resolve_path`, which has no filesystem to consult.
|
||||||
|
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] // unit-tested on native
|
||||||
|
pub(crate) fn find_registered_source(raw: &str) -> Option<String> {
|
||||||
|
source_cache()
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||||
|
.keys()
|
||||||
|
.find(|key| same_file_name(key, raw))
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bytes registered under exactly `key`.
|
||||||
|
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
|
||||||
|
pub(crate) fn registered_bytes(key: &str) -> Option<Arc<Vec<u8>>> {
|
||||||
|
source_cache()
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||||
|
.get(key)
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Take the cached parse for `key`, if one is pending.
|
||||||
|
#[cfg(any(target_arch = "wasm32", test))]
|
||||||
|
fn take_parsed(key: &str) -> Option<acadrust::ReadOutcome> {
|
||||||
|
parsed_cache()
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||||
|
.remove(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a registered xref on the web: the cached off-thread parse when the
|
||||||
|
/// pick/open pre-pass produced one, otherwise a direct parse of the
|
||||||
|
/// registered bytes (the eager parse never caches recoverable outcomes —
|
||||||
|
/// those must keep their stats so phase 2 reports them as Failed, like the
|
||||||
|
/// native reader does).
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn read_registered_outcome(
|
||||||
|
key: &str,
|
||||||
|
progress: Option<Arc<dyn Fn(u16) + Send + Sync>>,
|
||||||
|
) -> Result<acadrust::ReadOutcome, String> {
|
||||||
|
if let Some(mut outcome) = take_parsed(key) {
|
||||||
|
finalize_xref_outcome(&mut outcome);
|
||||||
|
return Ok(outcome);
|
||||||
|
}
|
||||||
|
let Some(bytes) = registered_bytes(key) else {
|
||||||
|
return Err(format!("no registered bytes for xref \"{key}\""));
|
||||||
|
};
|
||||||
|
let mut outcome = read_outcome_from_bytes(key, bytes, progress)?;
|
||||||
|
finalize_xref_outcome(&mut outcome);
|
||||||
|
Ok(outcome)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply the same post-parse fixups the file-open tail applies, so merged
|
||||||
|
/// xref content behaves like natively loaded content.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn finalize_xref_outcome(outcome: &mut acadrust::ReadOutcome) {
|
||||||
|
let doc = &mut outcome.document;
|
||||||
|
super::normalize_block_origins(doc);
|
||||||
|
if outcome.stats.source_format == Some(acadrust::SourceFormat::Dxf) {
|
||||||
|
super::fix_dxf_dimension_rotations(doc);
|
||||||
|
super::fix_dxf_layout_plot_settings(doc);
|
||||||
|
}
|
||||||
|
super::fix_viewport_status_flags(doc);
|
||||||
|
super::fix_current_style_names(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse DWG/DXF bytes into a reader outcome — `load_file_with_progress`
|
||||||
|
/// for the browser, where the "file" is the registry entry. Recoverable
|
||||||
|
/// parse issues stay in the stats instead of failing the read, matching the
|
||||||
|
/// native xref loader's behaviour (phase 2 turns them into Failed).
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn read_outcome_from_bytes(
|
||||||
|
name: &str,
|
||||||
|
bytes: Arc<Vec<u8>>,
|
||||||
|
progress: Option<Arc<dyn Fn(u16) + Send + Sync>>,
|
||||||
|
) -> Result<acadrust::ReadOutcome, String> {
|
||||||
|
use acadrust::io::dwg::DwgReader;
|
||||||
|
use acadrust::DxfReader;
|
||||||
|
// The readers keep their stream and want an owned, 'static buffer —
|
||||||
|
// take the registry `Arc`'s contents by value (one copy; the worker
|
||||||
|
// pre-parse makes this fallback path the exception, not the rule).
|
||||||
|
let bytes = bytes.to_vec();
|
||||||
|
let ext = name.rsplit('.').next().unwrap_or_default().to_lowercase();
|
||||||
|
// A `.bak`/`.sv$` copy hides the real format behind its extension —
|
||||||
|
// sniff the leading bytes, exactly as the path-based reader does.
|
||||||
|
let effective = if matches!(ext.as_str(), "bak" | "sv$") {
|
||||||
|
super::sniff_dwg_or_dxf_bytes(&bytes).to_string()
|
||||||
|
} else {
|
||||||
|
ext.clone()
|
||||||
|
};
|
||||||
|
let outcome = match effective.as_str() {
|
||||||
|
"dwg" => {
|
||||||
|
let mut reader = DwgReader::from_stream(std::io::Cursor::new(bytes));
|
||||||
|
if let Some(progress) = progress {
|
||||||
|
reader.set_progress_callback(progress);
|
||||||
|
}
|
||||||
|
reader.read_with_stats()
|
||||||
|
}
|
||||||
|
"dxf" => DxfReader::from_reader(std::io::Cursor::new(bytes))
|
||||||
|
.and_then(|reader| reader.read_with_stats()),
|
||||||
|
_ => return Err(format!("Unsupported file format: .{ext}")),
|
||||||
|
};
|
||||||
|
let outcome = outcome.map_err(|error| error.to_string())?;
|
||||||
|
if !outcome.stats.has_usable_drawing_data() {
|
||||||
|
return Err("initial read returned no source drawing records".to_string());
|
||||||
|
}
|
||||||
|
Ok(outcome)
|
||||||
|
}
|
||||||
|
|
||||||
/// Scan `doc` for XREF block-records, resolve their paths relative to
|
/// Scan `doc` for XREF block-records, resolve their paths relative to
|
||||||
/// `base_dir`, and populate each xref block with entities from the
|
/// `base_dir`, and populate each xref block with entities from the
|
||||||
/// referenced file.
|
/// referenced file.
|
||||||
@@ -67,6 +260,10 @@ pub fn resolve_xrefs_with_progress(
|
|||||||
base_dir: &Path,
|
base_dir: &Path,
|
||||||
progress: Option<std::sync::Arc<dyn Fn(usize, usize) + Send + Sync>>,
|
progress: Option<std::sync::Arc<dyn Fn(usize, usize) + Send + Sync>>,
|
||||||
) -> (Vec<XrefInfo>, usize) {
|
) -> (Vec<XrefInfo>, usize) {
|
||||||
|
// Web: resolution is registry-by-name, so the caller's folder is only a
|
||||||
|
// native concept; reference it so the parameter stays meaningful.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
let _ = base_dir;
|
||||||
// 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
|
||||||
@@ -103,7 +300,12 @@ pub fn resolve_xrefs_with_progress(
|
|||||||
.into_par_iter()
|
.into_par_iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(xref_index, (block_name, raw_path, br_handle))| {
|
.map(|(xref_index, (block_name, raw_path, br_handle))| {
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let resolved = resolve_path(&raw_path, base_dir);
|
let resolved = resolve_path(&raw_path, base_dir);
|
||||||
|
// Web: there is no filesystem behind `raw_path` — resolve to the
|
||||||
|
// name-keyed bytes registry instead (trailing-name match).
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
let resolved = find_registered_source(&raw_path).map(PathBuf::from);
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let initial_fingerprint = resolved.as_ref().and_then(|path| {
|
let initial_fingerprint = resolved.as_ref().and_then(|path| {
|
||||||
crate::io::edit_lock::FileFingerprint::capture(path).ok()
|
crate::io::edit_lock::FileFingerprint::capture(path).ok()
|
||||||
@@ -125,9 +327,16 @@ pub fn resolve_xrefs_with_progress(
|
|||||||
});
|
});
|
||||||
callback
|
callback
|
||||||
});
|
});
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let xref_outcome = resolved
|
let xref_outcome = resolved
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|path| super::load_file_with_progress(path, nested_progress));
|
.map(|path| super::load_file_with_progress(path, nested_progress));
|
||||||
|
// Web: read the registry — the cached off-thread parse when the
|
||||||
|
// pick/open pre-pass made one, otherwise the registered bytes.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
let xref_outcome = resolved
|
||||||
|
.as_ref()
|
||||||
|
.map(|path| read_registered_outcome(&path.to_string_lossy(), nested_progress));
|
||||||
let source_sha256 = resolved.as_ref().and_then(|path| {
|
let source_sha256 = resolved.as_ref().and_then(|path| {
|
||||||
let needs_fingerprint = match &xref_outcome {
|
let needs_fingerprint = match &xref_outcome {
|
||||||
Some(Ok(outcome)) => {
|
Some(Ok(outcome)) => {
|
||||||
@@ -263,6 +472,9 @@ pub fn resolve_xrefs_with_progress(
|
|||||||
|
|
||||||
/// Try to build an absolute path from a raw xref path string.
|
/// Try to build an absolute path from a raw xref path string.
|
||||||
/// Handles absolute paths, relative paths, and Windows-style separators.
|
/// Handles absolute paths, relative paths, and Windows-style separators.
|
||||||
|
/// Web builds never get here — `resolve_xrefs_with_progress` matches the
|
||||||
|
/// bytes registry by name instead (there is no filesystem to probe).
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
fn resolve_path(raw: &str, base_dir: &Path) -> Option<PathBuf> {
|
fn resolve_path(raw: &str, base_dir: &Path) -> Option<PathBuf> {
|
||||||
let normalised = raw.replace('\\', "/");
|
let normalised = raw.replace('\\', "/");
|
||||||
let p = PathBuf::from(&normalised);
|
let p = PathBuf::from(&normalised);
|
||||||
@@ -539,3 +751,112 @@ fn is_sentinel_linetype(name: &str) -> bool {
|
|||||||
fn set_handle(entity: &mut EntityType, h: Handle) {
|
fn set_handle(entity: &mut EntityType, h: Handle) {
|
||||||
entity.common_mut().handle = h;
|
entity.common_mut().handle = h;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn outcome() -> acadrust::ReadOutcome {
|
||||||
|
acadrust::ReadOutcome::new(CadDocument::new(), acadrust::ReadStats::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn file_name_key_takes_the_trailing_component_case_folded() {
|
||||||
|
assert_eq!(file_name_key("Site Plan.dwg"), "site plan.dwg");
|
||||||
|
assert_eq!(
|
||||||
|
file_name_key(r"D:\Projects\Arch\XREFS\Elevation.DWG"),
|
||||||
|
"elevation.dwg"
|
||||||
|
);
|
||||||
|
assert_eq!(file_name_key("../consultant/model.dxf"), "model.dxf");
|
||||||
|
assert_eq!(file_name_key(""), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn same_file_name_ignores_path_and_case_but_not_empty() {
|
||||||
|
assert!(same_file_name(
|
||||||
|
r"C:\foreign\abs\PATH\Elevation.DWG",
|
||||||
|
"elevation.dwg"
|
||||||
|
));
|
||||||
|
assert!(same_file_name("a/x.dwg", r"b\X.Dwg"));
|
||||||
|
assert!(!same_file_name("elevation.dwg", "elevation-2.dwg"));
|
||||||
|
// An empty trailing component names nothing — never a match, or a
|
||||||
|
// drawing with a blank xref path would resolve to any empty key.
|
||||||
|
assert!(!same_file_name("", ""));
|
||||||
|
assert!(!same_file_name("", "x.dwg"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn registry_finds_a_registered_source_by_trailing_name() {
|
||||||
|
let registered = "Registry-Find-Site.dwg";
|
||||||
|
register_source(registered, Arc::new(vec![1, 2, 3]));
|
||||||
|
// The drawing's stored path is the author's absolute foreign path;
|
||||||
|
// only the trailing name can match, case-insensitively.
|
||||||
|
assert_eq!(
|
||||||
|
find_registered_source(r"E:\2009\Consultant\REGISTRY-FIND-SITE.DWG"),
|
||||||
|
Some(registered.to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(find_registered_source("registry-find-site.dxf"), None);
|
||||||
|
assert_eq!(
|
||||||
|
registered_bytes(registered).map(|bytes| bytes.as_ref().clone()),
|
||||||
|
Some(vec![1, 2, 3])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn register_source_supersedes_a_cached_parse_of_the_same_file() {
|
||||||
|
let name = "Registry-Supersede-Plan.dwg";
|
||||||
|
register_parsed(name, outcome());
|
||||||
|
// Re-registering the same file under a differently-cased path name
|
||||||
|
// must drop the stale parse so a reload re-reads the new bytes.
|
||||||
|
register_source("registry-supersede-plan.DWG", Arc::new(vec![9]));
|
||||||
|
assert!(take_parsed(name).is_none());
|
||||||
|
// And the fresh bytes are what the registry hands back.
|
||||||
|
assert_eq!(
|
||||||
|
find_registered_source(name).as_deref(),
|
||||||
|
Some("registry-supersede-plan.DWG")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unresolved_xref_names_lists_not_found_entries_only() {
|
||||||
|
let infos = vec![
|
||||||
|
XrefInfo {
|
||||||
|
name: "Loaded.dwg".into(),
|
||||||
|
path: "Loaded.dwg".into(),
|
||||||
|
status: XrefStatus::Loaded,
|
||||||
|
diagnostics: Vec::new(),
|
||||||
|
read_stats: None,
|
||||||
|
source_sha256: None,
|
||||||
|
},
|
||||||
|
XrefInfo {
|
||||||
|
name: "Missing A.dwg".into(),
|
||||||
|
path: r"C:\gone\Missing A.dwg".into(),
|
||||||
|
status: XrefStatus::NotFound,
|
||||||
|
diagnostics: Vec::new(),
|
||||||
|
read_stats: None,
|
||||||
|
source_sha256: None,
|
||||||
|
},
|
||||||
|
XrefInfo {
|
||||||
|
name: "Broken.dwg".into(),
|
||||||
|
path: "Broken.dwg".into(),
|
||||||
|
status: XrefStatus::Failed,
|
||||||
|
diagnostics: Vec::new(),
|
||||||
|
read_stats: None,
|
||||||
|
source_sha256: None,
|
||||||
|
},
|
||||||
|
XrefInfo {
|
||||||
|
name: "Missing B.dxf".into(),
|
||||||
|
path: "Missing B.dxf".into(),
|
||||||
|
status: XrefStatus::NotFound,
|
||||||
|
diagnostics: Vec::new(),
|
||||||
|
read_stats: None,
|
||||||
|
source_sha256: None,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
unresolved_xref_names(&infos),
|
||||||
|
vec!["Missing A.dwg".to_string(), "Missing B.dxf".to_string()]
|
||||||
|
);
|
||||||
|
assert!(unresolved_xref_names(&[]).is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
+391
@@ -0,0 +1,391 @@
|
|||||||
|
//! Minimal in-memory ZIP writer.
|
||||||
|
//!
|
||||||
|
//! The web ARCHIVE/ETRANSMIT packages a transmittal (drawing + registered
|
||||||
|
//! dependencies) into one browser download; ZIP is the container every
|
||||||
|
//! recipient can open. This writer emits the classic (pre-zip64) format
|
||||||
|
//! directly — local file headers, a central directory, and the end record —
|
||||||
|
//! with CRC32 and DEFLATE from the `flate2` crate the tree already depends
|
||||||
|
//! on, so no new dependency is pulled in for a handful of fixed-size
|
||||||
|
//! little-endian records.
|
||||||
|
//!
|
||||||
|
//! Constraints of the classic format are enforced with errors instead of
|
||||||
|
//! wrapping: more than 65535 entries, an entry over 4 GiB, or an archive
|
||||||
|
//! over 4 GiB cannot be represented and refuse to build. Browser-download
|
||||||
|
//! transmittals are far below every limit.
|
||||||
|
//!
|
||||||
|
//! Every entry is stamped with the same fixed DOS timestamp and flat,
|
||||||
|
//! separator-free name, so building the same entries twice yields
|
||||||
|
//! byte-identical output.
|
||||||
|
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use flate2::write::DeflateEncoder;
|
||||||
|
use flate2::Compression;
|
||||||
|
|
||||||
|
/// Local file header signature ("PK\x03\x04").
|
||||||
|
const LOCAL_SIG: u32 = 0x0403_4b50;
|
||||||
|
/// Central directory file header signature ("PK\x01\x02").
|
||||||
|
const CENTRAL_SIG: u32 = 0x0201_4b50;
|
||||||
|
/// End of central directory record signature ("PK\x05\x06").
|
||||||
|
const END_SIG: u32 = 0x0605_4b50;
|
||||||
|
|
||||||
|
/// Compression method 0: bytes stored verbatim.
|
||||||
|
const METHOD_STORE: u16 = 0;
|
||||||
|
/// Compression method 8: raw DEFLATE (no zlib wrapper).
|
||||||
|
const METHOD_DEFLATE: u16 = 8;
|
||||||
|
|
||||||
|
/// Version needed to extract (2.0 — the deflate minimum) and the version
|
||||||
|
/// that "made" the archive (2.0, MS-DOS host so no external-attribute
|
||||||
|
/// permissions are implied). One constant serves both fields.
|
||||||
|
const VERSION_2_0: u16 = 20;
|
||||||
|
|
||||||
|
/// General-purpose flag 0x0800: entry names are UTF-8.
|
||||||
|
const FLAG_UTF8: u16 = 0x0800;
|
||||||
|
|
||||||
|
/// Timestamp stamped on every entry: 2026-01-01 00:00:00 in DOS packed
|
||||||
|
/// form. Fixed rather than taken from the clock so archives are
|
||||||
|
/// deterministic (and testable) byte for byte.
|
||||||
|
const DOS_TIME: u16 = 0; // 00:00:00
|
||||||
|
const DOS_DATE: u16 = (2026 - 1980) << 9 | 1 << 5 | 1; // 2026-01-01
|
||||||
|
|
||||||
|
/// One file to package: a flat name and its bytes.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ZipEntry {
|
||||||
|
pub name: String,
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZipEntry {
|
||||||
|
pub fn new(name: impl Into<String>, data: Vec<u8>) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.into(),
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a complete ZIP archive from `entries`, in order.
|
||||||
|
///
|
||||||
|
/// Each entry is DEFLATE-compressed and kept only when that actually
|
||||||
|
/// shrinks it (drawings compress well; PNG/PDF payloads are stored instead
|
||||||
|
/// of being pointlessly re-deflated) — the same choice mainstream writers
|
||||||
|
/// make. Duplicate names are written as given; callers that want one file
|
||||||
|
/// per name dedupe before building.
|
||||||
|
pub fn build_zip(entries: &[ZipEntry]) -> Result<Vec<u8>, String> {
|
||||||
|
let count =
|
||||||
|
u16::try_from(entries.len()).map_err(|_| format!("too many entries: {}", entries.len()))?;
|
||||||
|
let mut out: Vec<u8> = Vec::new();
|
||||||
|
let mut central: Vec<u8> = Vec::new();
|
||||||
|
for entry in entries {
|
||||||
|
let name = validate_name(&entry.name)?;
|
||||||
|
let name_len =
|
||||||
|
u16::try_from(name.len()).map_err(|_| format!("entry name too long: {}", name.len()))?;
|
||||||
|
|
||||||
|
let mut crc = flate2::Crc::new();
|
||||||
|
crc.update(&entry.data);
|
||||||
|
let crc = crc.sum();
|
||||||
|
|
||||||
|
let deflated = deflate(&entry.data);
|
||||||
|
let (method, payload): (u16, &[u8]) = if deflated.len() < entry.data.len() {
|
||||||
|
(METHOD_DEFLATE, &deflated)
|
||||||
|
} else {
|
||||||
|
(METHOD_STORE, &entry.data)
|
||||||
|
};
|
||||||
|
let compressed_size = u32::try_from(payload.len())
|
||||||
|
.map_err(|_| format!("entry {} exceeds the 4 GiB classic-ZIP limit", name))?;
|
||||||
|
let uncompressed_size = u32::try_from(entry.data.len())
|
||||||
|
.map_err(|_| format!("entry {} exceeds the 4 GiB classic-ZIP limit", name))?;
|
||||||
|
let local_offset = u32::try_from(out.len())
|
||||||
|
.map_err(|_| "archive exceeds the 4 GiB classic-ZIP limit".to_string())?;
|
||||||
|
|
||||||
|
// Local file header — immediately followed by name and payload.
|
||||||
|
out.extend_from_slice(&LOCAL_SIG.to_le_bytes());
|
||||||
|
out.extend_from_slice(&VERSION_2_0.to_le_bytes());
|
||||||
|
out.extend_from_slice(&FLAG_UTF8.to_le_bytes());
|
||||||
|
out.extend_from_slice(&method.to_le_bytes());
|
||||||
|
out.extend_from_slice(&DOS_TIME.to_le_bytes());
|
||||||
|
out.extend_from_slice(&DOS_DATE.to_le_bytes());
|
||||||
|
out.extend_from_slice(&crc.to_le_bytes());
|
||||||
|
out.extend_from_slice(&compressed_size.to_le_bytes());
|
||||||
|
out.extend_from_slice(&uncompressed_size.to_le_bytes());
|
||||||
|
out.extend_from_slice(&name_len.to_le_bytes());
|
||||||
|
out.extend_from_slice(&0u16.to_le_bytes()); // extra field length
|
||||||
|
out.extend_from_slice(name.as_bytes());
|
||||||
|
out.extend_from_slice(payload);
|
||||||
|
|
||||||
|
// Central directory record for the entry just written.
|
||||||
|
central.extend_from_slice(&CENTRAL_SIG.to_le_bytes());
|
||||||
|
central.extend_from_slice(&VERSION_2_0.to_le_bytes()); // version made by
|
||||||
|
central.extend_from_slice(&VERSION_2_0.to_le_bytes()); // version needed
|
||||||
|
central.extend_from_slice(&FLAG_UTF8.to_le_bytes());
|
||||||
|
central.extend_from_slice(&method.to_le_bytes());
|
||||||
|
central.extend_from_slice(&DOS_TIME.to_le_bytes());
|
||||||
|
central.extend_from_slice(&DOS_DATE.to_le_bytes());
|
||||||
|
central.extend_from_slice(&crc.to_le_bytes());
|
||||||
|
central.extend_from_slice(&compressed_size.to_le_bytes());
|
||||||
|
central.extend_from_slice(&uncompressed_size.to_le_bytes());
|
||||||
|
central.extend_from_slice(&name_len.to_le_bytes());
|
||||||
|
central.extend_from_slice(&0u16.to_le_bytes()); // extra field length
|
||||||
|
central.extend_from_slice(&0u16.to_le_bytes()); // comment length
|
||||||
|
central.extend_from_slice(&0u16.to_le_bytes()); // disk number start
|
||||||
|
central.extend_from_slice(&0u16.to_le_bytes()); // internal attributes
|
||||||
|
central.extend_from_slice(&0u32.to_le_bytes()); // external attributes
|
||||||
|
central.extend_from_slice(&local_offset.to_le_bytes());
|
||||||
|
central.extend_from_slice(name.as_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
let central_offset = u32::try_from(out.len())
|
||||||
|
.map_err(|_| "archive exceeds the 4 GiB classic-ZIP limit".to_string())?;
|
||||||
|
let central_size = u32::try_from(central.len())
|
||||||
|
.map_err(|_| "archive exceeds the 4 GiB classic-ZIP limit".to_string())?;
|
||||||
|
out.extend_from_slice(¢ral);
|
||||||
|
|
||||||
|
// End of central directory record — no archive comment.
|
||||||
|
out.extend_from_slice(&END_SIG.to_le_bytes());
|
||||||
|
out.extend_from_slice(&0u16.to_le_bytes()); // this disk number
|
||||||
|
out.extend_from_slice(&0u16.to_le_bytes()); // central directory start disk
|
||||||
|
out.extend_from_slice(&count.to_le_bytes()); // entries on this disk
|
||||||
|
out.extend_from_slice(&count.to_le_bytes()); // entries total
|
||||||
|
out.extend_from_slice(¢ral_size.to_le_bytes());
|
||||||
|
out.extend_from_slice(¢ral_offset.to_le_bytes());
|
||||||
|
out.extend_from_slice(&0u16.to_le_bytes()); // comment length
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Entry names must be flat file names: non-empty, free of path separators
|
||||||
|
/// and NUL, not a directory dot-name. Keeping the archive flat mirrors the
|
||||||
|
/// native transmittal folder (drawing and dependencies side by side) and
|
||||||
|
/// rules out path traversal on extraction.
|
||||||
|
fn validate_name(name: &str) -> Result<&str, String> {
|
||||||
|
if name.is_empty()
|
||||||
|
|| name == "."
|
||||||
|
|| name == ".."
|
||||||
|
|| name.contains(['/', '\\', '\0'])
|
||||||
|
|| name.len() > u16::MAX as usize
|
||||||
|
{
|
||||||
|
return Err(format!("invalid ZIP entry name: {name:?}"));
|
||||||
|
}
|
||||||
|
Ok(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Raw DEFLATE stream (what method 8 stores — no zlib header/checksum).
|
||||||
|
/// Infallible in practice: the encoder writes into a `Vec`, which cannot
|
||||||
|
/// fail; on an impossible error the uncompressed bytes are kept so the
|
||||||
|
/// caller still receives a usable payload.
|
||||||
|
fn deflate(data: &[u8]) -> Vec<u8> {
|
||||||
|
let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
|
||||||
|
if encoder.write_all(data).is_err() {
|
||||||
|
return data.to_vec();
|
||||||
|
}
|
||||||
|
encoder.finish().unwrap_or_else(|_| data.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use flate2::read::DeflateDecoder;
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
/// The CRC-32 (IEEE 802.3) check value for "123456789" — the standard
|
||||||
|
/// reference every CRC implementation is verified against.
|
||||||
|
const CRC_CHECK_VALUE: u32 = 0xCBF4_3926;
|
||||||
|
|
||||||
|
/// One entry as read back from the central directory.
|
||||||
|
struct CentralEntry {
|
||||||
|
name: String,
|
||||||
|
method: u16,
|
||||||
|
crc: u32,
|
||||||
|
compressed_size: u32,
|
||||||
|
uncompressed_size: u32,
|
||||||
|
local_offset: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn u16_at(zip: &[u8], at: usize) -> u16 {
|
||||||
|
u16::from_le_bytes([zip[at], zip[at + 1]])
|
||||||
|
}
|
||||||
|
fn u32_at(zip: &[u8], at: usize) -> u32 {
|
||||||
|
u32::from_le_bytes([zip[at], zip[at + 1], zip[at + 2], zip[at + 3]])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the end record (always the last 22 bytes — the writer never
|
||||||
|
/// emits a comment) and walk the central directory it points at.
|
||||||
|
fn parse_central_directory(zip: &[u8]) -> (Vec<CentralEntry>, u16, u32, u32) {
|
||||||
|
assert!(zip.len() >= 22, "archive must hold an end record");
|
||||||
|
let eocd = zip.len() - 22;
|
||||||
|
assert_eq!(&zip[eocd..eocd + 4], &END_SIG.to_le_bytes());
|
||||||
|
assert_eq!(u16_at(zip, eocd + 4), 0, "disk number");
|
||||||
|
assert_eq!(u16_at(zip, eocd + 6), 0, "central directory start disk");
|
||||||
|
let on_disk = u16_at(zip, eocd + 8);
|
||||||
|
let total = u16_at(zip, eocd + 10);
|
||||||
|
let size = u32_at(zip, eocd + 12);
|
||||||
|
let offset = u32_at(zip, eocd + 16);
|
||||||
|
assert_eq!(u16_at(zip, eocd + 20), 0, "comment length");
|
||||||
|
assert_eq!(on_disk, total, "single-disk archive");
|
||||||
|
|
||||||
|
let mut entries = Vec::with_capacity(total as usize);
|
||||||
|
let mut cursor = offset as usize;
|
||||||
|
for _ in 0..total {
|
||||||
|
assert_eq!(&zip[cursor..cursor + 4], &CENTRAL_SIG.to_le_bytes());
|
||||||
|
let method = u16_at(zip, cursor + 10);
|
||||||
|
let crc = u32_at(zip, cursor + 16);
|
||||||
|
let compressed_size = u32_at(zip, cursor + 20);
|
||||||
|
let uncompressed_size = u32_at(zip, cursor + 24);
|
||||||
|
let name_len = u16_at(zip, cursor + 28) as usize;
|
||||||
|
assert_eq!(u16_at(zip, cursor + 30), 0, "extra length");
|
||||||
|
assert_eq!(u16_at(zip, cursor + 32), 0, "comment length");
|
||||||
|
assert_eq!(u16_at(zip, cursor + 34), 0, "disk number start");
|
||||||
|
let local_offset = u32_at(zip, cursor + 42);
|
||||||
|
let name = String::from_utf8(zip[cursor + 46..cursor + 46 + name_len].to_vec())
|
||||||
|
.expect("entry names are UTF-8");
|
||||||
|
entries.push(CentralEntry {
|
||||||
|
name,
|
||||||
|
method,
|
||||||
|
crc,
|
||||||
|
compressed_size,
|
||||||
|
uncompressed_size,
|
||||||
|
local_offset,
|
||||||
|
});
|
||||||
|
cursor += 46 + name_len;
|
||||||
|
}
|
||||||
|
assert_eq!(cursor - offset as usize, size as usize, "recorded size");
|
||||||
|
assert_eq!(cursor, eocd, "central directory ends at the end record");
|
||||||
|
(entries, total, size, offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read one entry's payload back through its local header, inflating
|
||||||
|
/// method-8 entries — the round trip an extractor performs.
|
||||||
|
fn read_entry_payload(zip: &[u8], entry: &CentralEntry) -> Vec<u8> {
|
||||||
|
let at = entry.local_offset as usize;
|
||||||
|
assert_eq!(&zip[at..at + 4], &LOCAL_SIG.to_le_bytes());
|
||||||
|
let name_len = u16_at(zip, at + 26) as usize;
|
||||||
|
assert_eq!(u16_at(zip, at + 28), 0, "extra length");
|
||||||
|
let name = String::from_utf8(zip[at + 30..at + 30 + name_len].to_vec()).unwrap();
|
||||||
|
assert_eq!(name, entry.name, "local and central names agree");
|
||||||
|
let start = at + 30 + name_len;
|
||||||
|
let payload = &zip[start..start + entry.compressed_size as usize];
|
||||||
|
match entry.method {
|
||||||
|
METHOD_STORE => payload.to_vec(),
|
||||||
|
METHOD_DEFLATE => {
|
||||||
|
let mut inflated = Vec::new();
|
||||||
|
DeflateDecoder::new(payload)
|
||||||
|
.read_to_end(&mut inflated)
|
||||||
|
.expect("deflate payload inflates");
|
||||||
|
inflated
|
||||||
|
}
|
||||||
|
other => panic!("unexpected method {other}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deterministic poorly-compressible bytes (an LCG keystream) so the
|
||||||
|
/// STORE branch is exercised without a fixture file.
|
||||||
|
fn incompressible(len: usize) -> Vec<u8> {
|
||||||
|
let mut state = 0x2545_F491_u32;
|
||||||
|
(0..len)
|
||||||
|
.map(|_| {
|
||||||
|
state = state.wrapping_mul(1_103_515_245).wrapping_add(12_345);
|
||||||
|
(state >> 24) as u8
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_archive_is_a_bare_end_record() {
|
||||||
|
let zip = build_zip(&[]).expect("empty archive builds");
|
||||||
|
assert_eq!(zip.len(), 22);
|
||||||
|
let (entries, total, size, offset) = parse_central_directory(&zip);
|
||||||
|
assert_eq!(entries.len(), 0);
|
||||||
|
assert_eq!(total, 0);
|
||||||
|
assert_eq!(size, 0);
|
||||||
|
assert_eq!(offset, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn central_directory_round_trips_known_contents() {
|
||||||
|
let drawing = b"0\nSECTION\n2\nENTITIES\n0\nENDSEC\n0\nEOF\n".repeat(40);
|
||||||
|
let entries = vec![
|
||||||
|
ZipEntry::new("plan.dwg", drawing.clone()),
|
||||||
|
ZipEntry::new("site.png", incompressible(512)),
|
||||||
|
ZipEntry::new("notes.txt", Vec::new()),
|
||||||
|
];
|
||||||
|
let zip = build_zip(&entries).expect("archive builds");
|
||||||
|
|
||||||
|
let (central, total, size, offset) = parse_central_directory(&zip);
|
||||||
|
assert_eq!(total as usize, entries.len());
|
||||||
|
assert_eq!(central.len(), entries.len());
|
||||||
|
assert!(size > 0 && (offset as usize) < zip.len());
|
||||||
|
|
||||||
|
let names: Vec<&str> = central.iter().map(|e| e.name.as_str()).collect();
|
||||||
|
assert_eq!(names, ["plan.dwg", "site.png", "notes.txt"]);
|
||||||
|
|
||||||
|
for (entry, parsed) in entries.iter().zip(¢ral) {
|
||||||
|
let payload = read_entry_payload(&zip, parsed);
|
||||||
|
assert_eq!(payload, entry.data, "{} round-trips", entry.name);
|
||||||
|
assert_eq!(parsed.uncompressed_size as usize, entry.data.len());
|
||||||
|
let mut crc = flate2::Crc::new();
|
||||||
|
crc.update(&entry.data);
|
||||||
|
assert_eq!(parsed.crc, crc.sum(), "{} CRC matches flate2", entry.name);
|
||||||
|
assert!(parsed.compressed_size as usize <= entry.data.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The repetitive drawing DEFLATES; the keystream image does not.
|
||||||
|
assert_eq!(central[0].method, METHOD_DEFLATE);
|
||||||
|
assert!(central[0].compressed_size < central[0].uncompressed_size);
|
||||||
|
assert_eq!(central[1].method, METHOD_STORE);
|
||||||
|
assert_eq!(central[1].compressed_size, central[1].uncompressed_size);
|
||||||
|
// An empty entry has nothing to shrink: stored, zero sizes.
|
||||||
|
assert_eq!(central[2].method, METHOD_STORE);
|
||||||
|
assert_eq!(central[2].compressed_size, 0);
|
||||||
|
assert_eq!(central[2].uncompressed_size, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stored_crc_matches_the_reference_check_value() {
|
||||||
|
let zip = build_zip(&[ZipEntry::new("check.bin", b"123456789".to_vec())])
|
||||||
|
.expect("archive builds");
|
||||||
|
let (central, _, _, _) = parse_central_directory(&zip);
|
||||||
|
assert_eq!(central[0].crc, CRC_CHECK_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn local_offsets_and_determinism_hold() {
|
||||||
|
let entries = vec![
|
||||||
|
ZipEntry::new("a.dwg", incompressible(64)),
|
||||||
|
ZipEntry::new("b.png", b"same bytes".to_vec()),
|
||||||
|
];
|
||||||
|
let zip = build_zip(&entries).expect("archive builds");
|
||||||
|
let (central, _, _, _) = parse_central_directory(&zip);
|
||||||
|
// Offsets advance past each header+payload, first entry at zero.
|
||||||
|
assert_eq!(central[0].local_offset, 0);
|
||||||
|
assert!(central[1].local_offset > central[0].local_offset);
|
||||||
|
// Identical input rebuilds byte-identical output (fixed timestamp).
|
||||||
|
assert_eq!(build_zip(&entries).unwrap(), zip);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn utf8_names_round_trip() {
|
||||||
|
let entries = vec![ZipEntry::new("πλάνη.dwg", "γ".repeat(100).into_bytes())];
|
||||||
|
let zip = build_zip(&entries).expect("archive builds");
|
||||||
|
let (central, _, _, _) = parse_central_directory(&zip);
|
||||||
|
assert_eq!(central[0].name, "πλάνη.dwg");
|
||||||
|
assert_eq!(read_entry_payload(&zip, ¢ral[0]), entries[0].data);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_names_are_rejected() {
|
||||||
|
let long = "x".repeat(u16::MAX as usize + 1);
|
||||||
|
for name in ["", ".", "..", "a/b", "a\\b", "a\0b", &long] {
|
||||||
|
let err = build_zip(&[ZipEntry::new(name, Vec::new())])
|
||||||
|
.expect_err("invalid name must be rejected");
|
||||||
|
assert!(err.contains("invalid ZIP entry name"), "{name:?}: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn entry_count_over_the_classic_limit_is_rejected() {
|
||||||
|
let entries: Vec<ZipEntry> = (0..=u16::MAX as usize + 1)
|
||||||
|
.map(|i| ZipEntry::new(format!("f{i}"), Vec::new()))
|
||||||
|
.collect();
|
||||||
|
let err = build_zip(&entries).expect_err("too many entries must be rejected");
|
||||||
|
assert!(err.contains("too many entries"), "{err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3153,6 +3153,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||||||
"XREF Reloaded \"{}\"" => Some(("insert", "xref-reloaded")),
|
"XREF Reloaded \"{}\"" => Some(("insert", "xref-reloaded")),
|
||||||
"XREF Save the drawing first to resolve relative XREF paths." => Some(("insert", "xref-save-the-drawing-first-to-resolve-relativ")),
|
"XREF Save the drawing first to resolve relative XREF paths." => Some(("insert", "xref-save-the-drawing-first-to-resolve-relativ")),
|
||||||
"XREF Unloaded (skipped): \"{}\"" => Some(("common", "xref-unloaded-skipped")),
|
"XREF Unloaded (skipped): \"{}\"" => Some(("common", "xref-unloaded-skipped")),
|
||||||
|
"XREF Unresolved external references: {names} — drop the referenced DWG/DXF files on the page to register them, then reopen this drawing or run XRELOAD." => Some(("insert", "xref-unresolved-external-references")),
|
||||||
"Y (mm)" => Some(("plot", "y-mm")),
|
"Y (mm)" => Some(("plot", "y-mm")),
|
||||||
"Yards" => Some(("common", "yards")),
|
"Yards" => Some(("common", "yards")),
|
||||||
"Yellow" => Some(("common", "yellow")),
|
"Yellow" => Some(("common", "yellow")),
|
||||||
@@ -3190,11 +3191,14 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||||||
"that object" => Some(("modal", "that-object")),
|
"that object" => Some(("modal", "that-object")),
|
||||||
"those objects" => Some(("modal", "those-objects")),
|
"those objects" => Some(("modal", "those-objects")),
|
||||||
"units" => Some(("common", "units")),
|
"units" => Some(("common", "units")),
|
||||||
|
"{cmd}: cannot build the transmittal ZIP ({e})." => Some(("file", "cmd-cannot-build-the-transmittal-zip-e")),
|
||||||
"{cmd}: cannot create folder ({e})." => Some(("file", "cmd-cannot-create-folder-e")),
|
"{cmd}: cannot create folder ({e})." => Some(("file", "cmd-cannot-create-folder-e")),
|
||||||
|
"{cmd}: cannot package the drawing ({e})." => Some(("file", "cmd-cannot-package-the-drawing-e")),
|
||||||
"{cmd}: no hatch objects." => Some(("view", "cmd-no-hatch-objects")),
|
"{cmd}: no hatch objects." => Some(("view", "cmd-no-hatch-objects")),
|
||||||
"{cmd}: no text or dimension objects." => Some(("view", "cmd-no-text-or-dimension-objects")),
|
"{cmd}: no text or dimension objects." => Some(("view", "cmd-no-text-or-dimension-objects")),
|
||||||
"{cmd}: not yet implemented." => Some(("view", "cmd-not-yet-implemented")),
|
"{cmd}: not yet implemented." => Some(("view", "cmd-not-yet-implemented")),
|
||||||
"{cmd}: packaged {copied} file(s) into {}" => Some(("file", "cmd-packaged-copied-file-s-into")),
|
"{cmd}: packaged {copied} file(s) into {}" => Some(("file", "cmd-packaged-copied-file-s-into")),
|
||||||
|
"{cmd}: skipped {n} referenced file(s) not loaded in this session: {names}" => Some(("file", "cmd-skipped-n-referenced-file-s-not-loaded-in")),
|
||||||
"{count} object(s) added to selection." => Some(("common", "count-object-s-added-to-selection")),
|
"{count} object(s) added to selection." => Some(("common", "count-object-s-added-to-selection")),
|
||||||
"{count} object(s) pasted." => Some(("common", "count-object-s-pasted")),
|
"{count} object(s) pasted." => Some(("common", "count-object-s-pasted")),
|
||||||
"{error}. External-change checks remain active." => Some(("file", "error-external-change-checks-remain-active")),
|
"{error}. External-change checks remain active." => Some(("file", "error-external-change-checks-remain-active")),
|
||||||
@@ -3543,6 +3547,9 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||||||
"Save Block As" => Some(("common", "save-block-as")),
|
"Save Block As" => Some(("common", "save-block-as")),
|
||||||
"Save Data Extraction" => Some(("common", "save-data-extraction")),
|
"Save Data Extraction" => Some(("common", "save-data-extraction")),
|
||||||
"CSV" => Some(("common", "csv")),
|
"CSV" => Some(("common", "csv")),
|
||||||
|
"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")),
|
||||||
"Export STL" => Some(("common", "export-stl")),
|
"Export STL" => Some(("common", "export-stl")),
|
||||||
"STL Files" => Some(("common", "stl-files")),
|
"STL Files" => Some(("common", "stl-files")),
|
||||||
"Export STEP AP203" => Some(("common", "export-step-ap203")),
|
"Export STEP AP203" => Some(("common", "export-step-ap203")),
|
||||||
|
|||||||
@@ -465,6 +465,19 @@ fn registered_bytes_by_name(path: &str) -> Option<Arc<Vec<u8>>> {
|
|||||||
.map(|(_, bytes)| Arc::clone(bytes))
|
.map(|(_, bytes)| Arc::clone(bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Original image file bytes registered for a reference, resolved the way
|
||||||
|
/// `decode_reference` resolves pixels: exact reference first, then any
|
||||||
|
/// registration that names the same file. The web ARCHIVE/ETRANSMIT
|
||||||
|
/// packages these bytes into the transmittal; `None` means nothing was
|
||||||
|
/// picked or dropped for the reference this session.
|
||||||
|
pub fn registered_source_bytes(path: &str) -> Option<Arc<Vec<u8>>> {
|
||||||
|
let path = path.trim();
|
||||||
|
if path.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
registered_bytes(path).or_else(|| registered_bytes_by_name(path))
|
||||||
|
}
|
||||||
|
|
||||||
/// Drop every memoised image so the next resolve re-reads / re-fetches. Called
|
/// Drop every memoised image so the next resolve re-reads / re-fetches. Called
|
||||||
/// when a new document is opened — a fresh drawing shouldn't inherit a prior
|
/// when a new document is opened — a fresh drawing shouldn't inherit a prior
|
||||||
/// one's stale successes or offline failures.
|
/// one's stale successes or offline failures.
|
||||||
|
|||||||
@@ -30,6 +30,18 @@ fn source_cache() -> &'static Mutex<HashMap<String, Arc<Vec<u8>>>> {
|
|||||||
SOURCES.get_or_init(|| Mutex::new(HashMap::new()))
|
SOURCES.get_or_init(|| Mutex::new(HashMap::new()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bytes registered for exactly this PDF reference (the attach flow keys by
|
||||||
|
/// the same path the underlay definition stores). The web ARCHIVE/ETRANSMIT
|
||||||
|
/// packages the underlay into the transmittal; `None` means no PDF was
|
||||||
|
/// picked for this reference this session.
|
||||||
|
pub fn registered_source_bytes(path: &str) -> Option<Arc<Vec<u8>>> {
|
||||||
|
source_cache()
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(|error| error.into_inner())
|
||||||
|
.get(path)
|
||||||
|
.cloned()
|
||||||
|
}
|
||||||
|
|
||||||
/// Keep bytes selected through a file dialog. Browsers expose no reusable path,
|
/// Keep bytes selected through a file dialog. Browsers expose no reusable path,
|
||||||
/// while native builds also benefit by avoiding a second disk read.
|
/// while native builds also benefit by avoiding a second disk read.
|
||||||
pub fn register_source(path: &str, bytes: Arc<Vec<u8>>) {
|
pub fn register_source(path: &str, bytes: Arc<Vec<u8>>) {
|
||||||
|
|||||||
@@ -564,6 +564,17 @@ pub fn data_extraction_view(state: &DataExtractionState, sizing: crate::ui::moda
|
|||||||
let add_folder = button(text(t!("Add folder…")).size(12)).padding([5, 10]);
|
let add_folder = button(text(t!("Add folder…")).size(12)).padding([5, 10]);
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let add_folder = add_folder.on_press(Message::DataExtractionAddFolder);
|
let add_folder = add_folder.on_press(Message::DataExtractionAddFolder);
|
||||||
|
// The web has no folder picker and no directory to walk, so the button
|
||||||
|
// stays disabled there — with a tooltip that says why and what to do.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
let add_folder = iced::widget::tooltip(
|
||||||
|
add_folder,
|
||||||
|
container(text(t!("Adding folders is not available in the browser. Add drawing files instead.")).size(11))
|
||||||
|
.padding([3, 6])
|
||||||
|
.style(container::bordered_box),
|
||||||
|
iced::widget::tooltip::Position::Top,
|
||||||
|
)
|
||||||
|
.gap(4);
|
||||||
|
|
||||||
let body: Element<'_, Message> = match state.page {
|
let body: Element<'_, Message> = match state.page {
|
||||||
ExtractionPage::Begin => group(t!("Begin"), column![
|
ExtractionPage::Begin => group(t!("Begin"), column![
|
||||||
|
|||||||
+14
-2
@@ -1,7 +1,7 @@
|
|||||||
import init, { parse_document, sha256_document } from "./worker_pkg/ocs_web_worker.js?v=4";
|
import init, { parse_document, serialize_document, sha256_document } from "./worker_pkg/ocs_web_worker.js?v=5";
|
||||||
|
|
||||||
const ready = init(
|
const ready = init(
|
||||||
new URL("./worker_pkg/ocs_web_worker_bg.wasm?v=4", import.meta.url),
|
new URL("./worker_pkg/ocs_web_worker_bg.wasm?v=5", import.meta.url),
|
||||||
);
|
);
|
||||||
|
|
||||||
self.onmessage = async ({ data }) => {
|
self.onmessage = async ({ data }) => {
|
||||||
@@ -13,6 +13,18 @@ self.onmessage = async ({ data }) => {
|
|||||||
self.postMessage({ ok: true, digest });
|
self.postMessage({ ok: true, digest });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (data.action === "serialize") {
|
||||||
|
// SAVE/AUTOSAVE: decode the prepared document, run the DWG/DXF writer
|
||||||
|
// here, and hand the encoded result buffer back. Mirrors the parse
|
||||||
|
// branch's copy-before-transfer (wasm-bindgen returns a view into
|
||||||
|
// WebAssembly.Memory, which must not detach).
|
||||||
|
const encoded = serialize_document(new Uint8Array(data.document), (next) => {
|
||||||
|
stage = next;
|
||||||
|
});
|
||||||
|
const transferable = encoded.slice();
|
||||||
|
self.postMessage({ ok: true, data: transferable.buffer }, [transferable.buffer]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const encoded = parse_document(
|
const encoded = parse_document(
|
||||||
data.name,
|
data.name,
|
||||||
new Uint8Array(data.bytes),
|
new Uint8Array(data.bytes),
|
||||||
|
|||||||
Reference in new issue
Block a user