From 2c38de53e279b87dd2cb487107f50361dfb0b131 Mon Sep 17 00:00:00 2001 From: Afonsop Date: Fri, 18 Sep 2026 13:21:18 +0100 Subject: [PATCH] feat: hatch PDF draw order (#1345), numeric CAS (#1348), web automation (#1349), DATALINK table, System theme; honest decode accounting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream issues, fixed: #1345 — hatches plotted at draw_depth 0.0: the plot path cloned models from the hatch cache, which bakes the depth at seed time; the canvas re-reads the live depth map every frame, which is why the editor was right and every export (PDF and native printing — both route through the exporter) was wrong, with hatch-vs-hatch ties broken by HashMap order. The plot path now re-reads the depth map per entity, and the exporter's tie-break ranks follow the editor's own pass order (hatch < image < wire fill < wire < text < wipeout) instead of pinning hatches between fills and wires. Pinned by content-stream operator- order tests (back/front hatches, overlapping hatches, interleaved wire-between-hatches) and a scene test that fails pre-fix. #1348 — set_properties' optimistic-concurrency compare now matches numbers by value (10.0 == 10) through json_values_match, compatible with the shape of upstream PR #1350; nested objects/arrays recurse. #1349 — web automation contract: control protocol 2 (v1 clients still accepted) adds {op:'open', name, data_base64} routed through the guarded open queue; window.ocsControl.ready/request promise wrapper with an ocs-ready event (web/ocs-control.js); the startup donation prompt is suppressed once a control request is seen (opt-in pre-boot via localStorage); docs/automation documents the whole web contract. Also: DATALINK CSV-to-table works on web (picker + bytes through the session registry — was std::fs, silently dead); COLORSCHEME accepts System (prefers-color-scheme on web, AppsUseLightTheme on Windows). Decode accounting, made honest per the fix-of-fix review: into_rgba8 allocates the RGBA destination while the native buffer is alive for every non-Rgba8 layout (L8 5, La8 6, Rgb8 7, L16 6, La16 8 B/px peaks — the formula now says so), GIF bills the screen+frame coexistence at 8 B/px, and TIFF drops the 3x multipliers both ways — tiff 0.11 splits the given budget between frame and scratch (a total ceiling), so the old double-billing refused honest 16-bit GeoTIFFs ~2.25x early while admitting hostile chunk transients at 3x. The image-refusal-unknown-size locale key moves to the group the catalog always claimed (file). --- Cargo.toml | 3 + docs/automation/README.md | 42 +++++ locales/ar-SA/opencadstudio.ftl | 3 +- locales/bg-BG/opencadstudio.ftl | 3 +- locales/cs-CZ/opencadstudio.ftl | 3 +- locales/de-DE/opencadstudio.ftl | 3 +- locales/el-GR/opencadstudio.ftl | 3 +- locales/en-US/opencadstudio.ftl | 3 +- locales/es-ES/opencadstudio.ftl | 3 +- locales/fi-FI/opencadstudio.ftl | 3 +- locales/fr-FR/opencadstudio.ftl | 3 +- locales/hi-IN/opencadstudio.ftl | 3 +- locales/hu-HU/opencadstudio.ftl | 3 +- locales/it-IT/opencadstudio.ftl | 3 +- locales/ja-JP/opencadstudio.ftl | 3 +- locales/ko-KR/opencadstudio.ftl | 3 +- locales/nl-NL/opencadstudio.ftl | 3 +- locales/pl-PL/opencadstudio.ftl | 3 +- locales/pt-BR/opencadstudio.ftl | 3 +- locales/ru-RU/opencadstudio.ftl | 3 +- locales/tr-TR/opencadstudio.ftl | 3 +- locales/zh-CN/opencadstudio.ftl | 3 +- locales/zh-TW/opencadstudio.ftl | 3 +- src/app/annotation_data.rs | 28 +++ src/app/commands/display.rs | 97 ++++++---- src/app/commands/view.rs | 31 +++- src/app/config.rs | 84 +++++++++ src/app/control/mod.rs | 100 ++++++++++- src/app/mod.rs | 15 ++ src/app/record_api.rs | 97 +++++++++- src/app/startup.rs | 41 ++++- src/app/update/file.rs | 8 + src/app/update/mod.rs | 6 + src/io/mod.rs | 21 +++ src/io/pdf_export.rs | 302 ++++++++++++++++++++++++++++++-- src/locale_catalog.rs | 1 + src/scene/mod.rs | 73 ++++++++ src/scene/model/image_model.rs | 25 ++- src/scene/paper.rs | 7 + web-app.html | 2 + web/ocs-control.js | 72 ++++++++ 41 files changed, 1026 insertions(+), 92 deletions(-) create mode 100644 web/ocs-control.js diff --git a/Cargo.toml b/Cargo.toml index 93422e416..3c2fc87a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,6 +149,9 @@ web-sys = { version = "=0.3.85", features = [ "EventInit", "EventTarget", "BeforeUnloadEvent", + # COLORSCHEME SYSTEM resolves prefers-color-scheme through a media + # query listener read at boot and on every re-run. + "MediaQueryList", "console", "Node", "HtmlElement", diff --git a/docs/automation/README.md b/docs/automation/README.md index 1dcf4a90e..eeb87efad 100644 --- a/docs/automation/README.md +++ b/docs/automation/README.md @@ -156,3 +156,45 @@ The repeatable live-editor evaluation draws three isolated entities in one batch ```sh python3 docs/automation/mcp_eval.py target/debug/OpenCADStudio ``` + +## Web automation + +The web build exposes the same control protocol (protocol 2; protocol 1 requests are still accepted unchanged) through two wasm exports on `window.wasmBindings`: + +- `ocs_control_submit(requestJson)` returns a ticket id immediately. +- `ocs_control_take(ticketId)` returns the JSON reply once the app has settled the request, or `null` while it runs. + +`web/ocs-control.js` (loaded by `web-app.html`) wraps that pair in a small promise API: + +```js +await window.ocsControl.ready; // resolves when the bridge exists +window.addEventListener("ocs-ready", () => {}); // ...or listen for the one-shot event +const state = await window.ocsControl.request({op: "state"}); +const reply = await window.ocsControl.request({ + op: "run", request_id: "line-1", + document_id: state.document_id, revision: state.revision, + cmd: "LINE 0,0 10,0", +}); +``` + +`request` resolves with the final reply, following an `accepted`/`running` operation through `op: "operation"` until it completes, fails, or waits for input. It rejects on timeout (default 120 s, override with `{timeoutMs, pollMs}`) or when the bridge is missing. + +### Opening an in-memory drawing (web only) + +The `open` operation accepts base64 bytes instead of a path, so automation can open drawings it already holds in memory — no OPFS copy, no file picker. The bytes go through the same guarded open queue drag-and-drop uses: + +```js +const bytes = new Uint8Array(await file.arrayBuffer()); +let binary = ""; +for (const byte of bytes) binary += String.fromCharCode(byte); +await window.ocsControl.request({ + op: "open", request_id: "open-1", name: "plan.dwg", + data_base64: btoa(binary), +}); +``` + +The operation stays pending until the open settles; if another open holds the slot, the reply reports `result.queued` and the poll opens the drawing as soon as the slot frees. `state.capabilities` lists `open_bytes` on the web build. Native builds keep the path-based `open` and reject `data_base64` with `invalid_request`. + +### Startup prompts during automation + +The first control request of a session marks it automated: a donation prompt that has not been shown yet is dropped, and no new one is queued while control stays enabled. A prompt already on screen is left for its reader — close it with `{"op":"action","name":"close_modal"}`. A web host that intends to automate from the first page load can set `localStorage["opencadstudio.automation"] = "1"` before loading the app so the prompt never appears (the same pre-load mechanism the settings use). diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 93173e577..a0fc9d203 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -2009,6 +2009,7 @@ file = .adding-folders-is-not-available-in-the-browser = إضافة المجلدات غير متاحة في المتصفح. أضف ملفات الرسومات بدلاً من ذلك. .drawing-not-loaded-in-the-browser = رسم غير محمّل في المتصفح: __ocs_fmt_0__. .image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" بأبعاد __ocs_fmt_1__×__ocs_fmt_2__ بكسل ويتطلب فك ترميزه __ocs_fmt_3__ ميغابايت، وهو فوق حد __ocs_fmt_4__ ميغابايت للصور في هذا الإصدار — لا يمكن إرفاقه. تُرفَق الصور ضمن الحد تلقائيًا بدقة عرض مخفَّضة عند الحاجة. + .image-refusal-unknown-size = تعذّر قياس أبعاد "__ocs_fmt_0__" من ترويسته وسيتجاوز فك ترميزه حد الصور __ocs_fmt_1__ ميغابايت في هذا الإصدار — لا يمكن إرفاقه. selection = .cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ حدد الكائنات (__ocs_arg_count__ محدد، Enter للتطبيق): @@ -3542,6 +3543,7 @@ view = .cannot-delete-the-current-annotation-scale-arg = لا يمكن حذف مقياس التعليق التوضيحي الحالي (__ocs_fmt_0__). .cascade = متتالي .color-scheme-set-to-name = عُيّن نظام الألوان إلى '__ocs_fmt_0__'. + .color-scheme-set-to-system-currently-name = تم تعيين نظام الألوان على "النظام" (حاليًا __ocs_fmt_0__). .current-annotation-scale-name = مقياس التعليق التوضيحي الحالي: __ocs_fmt_0__ .datalink-path-to-the-csv-file = DATALINK مسار ملف ‎.csv: .datalink-cannot-read-path-e = DATALINK: تعذرت قراءة "__ocs_fmt_0__": ‏__ocs_fmt_1__ @@ -3589,7 +3591,6 @@ view = .opening-ocs-discussions-for-help-and-questions = يجري فتح مناقشات OCS للمساعدة والأسئلة... .opening-ocs-web = يجري فتح OCS Web... .opening-the-command-reference = فتح مرجع الأوامر... - .image-refusal-unknown-size = تعذّر قياس أبعاد "__ocs_fmt_0__" من ترويسته وسيتجاوز فك ترميزه حد الصور __ocs_fmt_1__ ميغابايت في هذا الإصدار — لا يمكن إرفاقه. .already-loaded-different-contents-kept = "__ocs_fmt_0__" محمّل بالفعل بمحتوى مختلف؛ تم الاحتفاظ بالملف الموجود. .opening-patreon-page = يجري فتح صفحة Patreon... .opening-feedback-page = يجري فتح صفحة الملاحظات... diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 5815f9d6c..e25d96e9c 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -1996,6 +1996,7 @@ file = .adding-folders-is-not-available-in-the-browser = Добавянето на папки не е налично в браузъра. Вместо това добавете файлове с чертежи. .drawing-not-loaded-in-the-browser = Чертежът не е зареден в браузъра: __ocs_fmt_0__. .image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" е __ocs_fmt_1__×__ocs_fmt_2__ px и при декодиране би заемала __ocs_fmt_3__ MB — над __ocs_fmt_4__ MB лимита за изображения на тази версия, затова не може да бъде прикачена. Изображенията в рамките на лимита се прикачват автоматично с намалена разделителна способност при нужда. + .image-refusal-unknown-size = Размерът на „__ocs_fmt_0__“ не можа да се измери от заглавната му част, а декодирането му би надвишило __ocs_fmt_1__ MB лимита на тази сборка — не може да се прикачи. selection = .cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Избери обекти (__ocs_arg_count__ избрани, Enter за прилагане): @@ -3529,6 +3530,7 @@ view = .cannot-delete-the-current-annotation-scale-arg = Не може да се изтрие текущият анотационен мащаб (__ocs_fmt_0__). .cascade = Каскада .color-scheme-set-to-name = Цветовата схема е зададена на "__ocs_fmt_0__". + .color-scheme-set-to-system-currently-name = Цветовата схема е зададена на „Системна“ (в момента __ocs_fmt_0__). .current-annotation-scale-name = Текущ анотационен мащаб: __ocs_fmt_0__ .datalink-path-to-the-csv-file = DATALINK път до .csv файла: .datalink-cannot-read-path-e = DATALINK: не може да се прочете "__ocs_fmt_0__": __ocs_fmt_1__ @@ -3576,7 +3578,6 @@ view = .opening-ocs-discussions-for-help-and-questions = Отваряне на OCS дискусии за помощ и въпроси... .opening-ocs-web = Отваряне на OCS уебсайта... .opening-the-command-reference = Отваряне на справочника за команди... - .image-refusal-unknown-size = Размерът на „__ocs_fmt_0__“ не можа да се измери от заглавната му част, а декодирането му би надвишило __ocs_fmt_1__ MB лимита на тази сборка — не може да се прикачи. .already-loaded-different-contents-kept = "__ocs_fmt_0__" вече е зареден с различно съдържание; съществуващият файл е запазен. .opening-patreon-page = Отваряне на страницата в Patreon... .opening-feedback-page = Отваряне на страницата за обратна връзка... diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index afcbf319f..362059822 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -1996,6 +1996,7 @@ file = .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__. .image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" má __ocs_fmt_1__×__ocs_fmt_2__ px a při dekódování by zabrala __ocs_fmt_3__ MB, přes limit __ocs_fmt_4__ MB pro obrázky v této verzi — nelze ji připojit. Obrázky v rámci limitu se podle potřeby připojují automaticky se sníženým rozlišením zobrazení. + .image-refusal-unknown-size = „__ocs_fmt_0__“ nešlo změřit ze záhlaví a jeho dekódování by překročilo limit __ocs_fmt_1__ MB tohoto sestavení — nelze připojit. selection = .cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Vyberte objekty (vybráno __ocs_arg_count__, použít Enter): @@ -3527,6 +3528,7 @@ view = .cannot-delete-the-current-annotation-scale-arg = Nelze odstranit aktuální měřítko anotací (__ocs_fmt_0__). .cascade = Kaskáda .color-scheme-set-to-name = Barevné schéma nastaveno na „__ocs_fmt_0__“. + .color-scheme-set-to-system-currently-name = Barevné schéma nastaveno na „Systém“ (aktuálně __ocs_fmt_0__). .current-annotation-scale-name = Aktuální měřítko anotace: __ocs_fmt_0__ .datalink-path-to-the-csv-file = Cesta DATALINK k souboru .csv: .datalink-cannot-read-path-e = DATALINK: nelze přečíst „__ocs_fmt_0__“: __ocs_fmt_1__ @@ -3574,7 +3576,6 @@ view = .opening-ocs-discussions-for-help-and-questions = Otevírání diskuzí OCS pro nápovědu a dotazy... .opening-ocs-web = Otevírání webu OCS... .opening-the-command-reference = Otevírání referenční příručky příkazů... - .image-refusal-unknown-size = „__ocs_fmt_0__“ nešlo změřit ze záhlaví a jeho dekódování by překročilo limit __ocs_fmt_1__ MB tohoto sestavení — nelze připojit. .already-loaded-different-contents-kept = "__ocs_fmt_0__" je již načten s jiným obsahem; existující soubor byl zachován. .opening-patreon-page = Otevírání stránky Patreon... .opening-feedback-page = Otevírá se stránka zpětné vazby... diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 379047b6a..b86aa4d78 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -1997,6 +1997,7 @@ file = .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__. .image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" hat __ocs_fmt_1__×__ocs_fmt_2__ px und würde beim Dekodieren __ocs_fmt_3__ MB belegen, über dem __ocs_fmt_4__ MB Bildlimit dieses Builds — es kann nicht angehängt werden. Bilder innerhalb des Limits werden bei Bedarf automatisch mit reduzierter Anzeigeanflösung angehängt. + .image-refusal-unknown-size = „__ocs_fmt_0__“ konnte anhand des Headers nicht vermessen werden, und das Dekodieren überschreitet das __ocs_fmt_1__-MB-Limit dieses Builds — kann nicht angehängt werden. selection = .cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Objekte auswählen (__ocs_arg_count__ ausgewählt, Eingabe zum Anwenden): @@ -3528,6 +3529,7 @@ view = .cannot-delete-the-current-annotation-scale-arg = Die aktuelle Annotationsskala (__ocs_fmt_0__) kann nicht gelöscht werden. .cascade = Kaskade .color-scheme-set-to-name = Farbschema eingestellt auf '__ocs_fmt_0__'. + .color-scheme-set-to-system-currently-name = Farbschema auf „System“ eingestellt (derzeit __ocs_fmt_0__). .current-annotation-scale-name = Aktuelle Annotationsskala: __ocs_fmt_0__ .datalink-path-to-the-csv-file = DATALINK Pfad zur .csv-Datei: .datalink-cannot-read-path-e = DATALINK: kann nicht "__ocs_fmt_0__" lesen: __ocs_fmt_1__ @@ -3575,7 +3577,6 @@ view = .opening-ocs-discussions-for-help-and-questions = Eröffnung OCS Diskussionen für Hilfe und Fragen... .opening-ocs-web = Öffnen OCS Web... .opening-the-command-reference = Öffnen der Befehlsreferenz... - .image-refusal-unknown-size = „__ocs_fmt_0__“ konnte anhand des Headers nicht vermessen werden, und das Dekodieren überschreitet das __ocs_fmt_1__-MB-Limit dieses Builds — kann nicht angehängt werden. .already-loaded-different-contents-kept = "__ocs_fmt_0__" ist bereits mit anderem Inhalt geladen; die vorhandene Datei wurde behalten. .opening-patreon-page = Eröffnungsseite Patreon... .opening-feedback-page = Feedbackseite öffnen... diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 04bd58c4e..831613a02 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -1996,6 +1996,7 @@ file = .adding-folders-is-not-available-in-the-browser = Η προσθήκη φακέλων δεν είναι διαθέσιμη στο πρόγραμμα περιήγησης. Προσθέστε αρχεία σχεδίων αντί για αυτό. .drawing-not-loaded-in-the-browser = Το σχέδιο δεν έχει φορτωθεί στο πρόγραμμα περιήγησης: __ocs_fmt_0__. .image-over-the-decode-limit-cannot-be-attached = Το "__ocs_fmt_0__" είναι __ocs_fmt_1__×__ocs_fmt_2__ px και η αποκωδικοποίησή του θα απαιτούσε __ocs_fmt_3__ MB, πάνω από το όριο __ocs_fmt_4__ MB για εικόνες σε αυτή την έκδοση — δεν μπορεί να επισυναφθεί. Οι εικόνες εντός του ορίου επισυνάπτονται κατά ανάγκη αυτόματα σε μειωμένη ανάλυση εμφάνισης. + .image-refusal-unknown-size = Το "__ocs_fmt_0__" δεν μπόρεσε να μετρηθεί από την κεφαλίδα του και η αποκωδικοποίησή του θα ξεπερνούσε το όριο __ocs_fmt_1__ MB αυτής της build — δεν μπορεί να επισυναφθεί. selection = .cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Επιλέξτε αντικείμενα (__ocs_arg_count__ επιλεγμένα, Enter για εφαρμογή): @@ -3523,6 +3524,7 @@ view = .cannot-delete-the-current-annotation-scale-arg = Δεν είναι δυνατή η διαγραφή της τρέχουσας κλίμακας επισημείωσης (__ocs_fmt_0__). .cascade = Διαδοχική διάταξη .color-scheme-set-to-name = Ο συνδυασμός χρωμάτων ορίστηκε σε '__ocs_fmt_0__'. + .color-scheme-set-to-system-currently-name = Ο χρωματικός συνδυασμός ρυθμίστηκε σε „Σύστημα“ (αυτή τη στιγμή __ocs_fmt_0__). .current-annotation-scale-name = Τρέχουσα κλίμακα επισημείωσης: __ocs_fmt_0__ .datalink-path-to-the-csv-file = DATALINK διαδρομή προς το αρχείο .csv: .datalink-specify-insertion-point = DATALINK Ορίστε σημείο εισαγωγής: @@ -3571,7 +3573,6 @@ view = .opening-ocs-discussions-for-help-and-questions = Άνοιγμα των συζητήσεων OCS για βοήθεια και ερωτήσεις... .opening-ocs-web = Άνοιγμα του OCS Web... .opening-the-command-reference = Άνοιγμα της αναφοράς εντολών... - .image-refusal-unknown-size = Το "__ocs_fmt_0__" δεν μπόρεσε να μετρηθεί από την κεφαλίδα του και η αποκωδικοποίησή του θα ξεπερνούσε το όριο __ocs_fmt_1__ MB αυτής της build — δεν μπορεί να επισυναφθεί. .already-loaded-different-contents-kept = Το "__ocs_fmt_0__" είναι ήδη φορτωμένο με διαφορετικό περιεχόμενο· διατηρήθηκε το υπάρχον αρχείο. .opening-patreon-page = Άνοιγμα της σελίδας Patreon... .opening-feedback-page = Άνοιγμα της σελίδας σχολίων... diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 80daa1b83..c89652f51 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -1997,6 +1997,7 @@ file = .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__. .image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" is __ocs_fmt_1__×__ocs_fmt_2__ px and would decode to __ocs_fmt_3__ MB, over the __ocs_fmt_4__ MB image limit of this build — it cannot be attached. Images within the limit are attached at a reduced display resolution when needed. + .image-refusal-unknown-size = "__ocs_fmt_0__" could not be sized from its header and decoding it exceeded the __ocs_fmt_1__ MB image limit of this build — it cannot be attached. selection = .cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Select objects (__ocs_arg_count__ selected, Enter to apply): @@ -3524,6 +3525,7 @@ view = .cannot-delete-the-current-annotation-scale-arg = Cannot delete the current annotation scale (__ocs_fmt_0__). .cascade = Cascade .color-scheme-set-to-name = Color scheme set to '__ocs_fmt_0__'. + .color-scheme-set-to-system-currently-name = Color scheme set to 'System' (currently __ocs_fmt_0__). .current-annotation-scale-name = Current annotation scale: __ocs_fmt_0__ .datalink-path-to-the-csv-file = DATALINK path to the .csv file: .datalink-specify-insertion-point = DATALINK Specify insertion point: @@ -3572,7 +3574,6 @@ view = .opening-ocs-discussions-for-help-and-questions = Opening OCS Discussions for help and questions... .opening-ocs-web = Opening OCS Web... .opening-the-command-reference = Opening the command reference... - .image-refusal-unknown-size = "__ocs_fmt_0__" could not be sized from its header and decoding it exceeded the __ocs_fmt_1__ MB image limit of this build — it cannot be attached. .already-loaded-different-contents-kept = "__ocs_fmt_0__" is already loaded with different contents; the existing file was kept. .opening-patreon-page = Opening Patreon page... .opening-feedback-page = Opening feedback page... diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 993e199b4..1822f48fd 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -1997,6 +1997,7 @@ file = .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__. .image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" tiene __ocs_fmt_1__×__ocs_fmt_2__ px y al decodificarse ocuparía __ocs_fmt_3__ MB, por encima del límite de __ocs_fmt_4__ MB por imagen de esta versión; no se puede adjuntar. Las imágenes dentro del límite se adjuntan automáticamente con una resolución de visualización reducida cuando es necesario. + .image-refusal-unknown-size = "__ocs_fmt_0__" no pudo medirse por su cabecera y decodificarlo excedería el límite de __ocs_fmt_1__ MB de esta build — no se puede adjuntar. selection = .cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Seleccione objetos (__ocs_arg_count__ seleccionados, Enter para aplicar): @@ -3530,6 +3531,7 @@ view = .cannot-delete-the-current-annotation-scale-arg = No se puede eliminar la escala anotativa actual (__ocs_fmt_0__). .cascade = Cascada .color-scheme-set-to-name = Esquema de color definido como «__ocs_fmt_0__». + .color-scheme-set-to-system-currently-name = Esquema de color establecido en «Sistema» (actualmente __ocs_fmt_0__). .current-annotation-scale-name = Escala anotativa actual: __ocs_fmt_0__ .datalink-path-to-the-csv-file = DATALINK Ruta del archivo .csv: .datalink-cannot-read-path-e = DATALINK: no se puede leer «__ocs_fmt_0__»: __ocs_fmt_1__ @@ -3577,7 +3579,6 @@ view = .opening-ocs-discussions-for-help-and-questions = Abriendo Debates de OCS para obtener ayuda y hacer preguntas... .opening-ocs-web = Abriendo OCS Web... .opening-the-command-reference = Abriendo la referencia de comandos... - .image-refusal-unknown-size = "__ocs_fmt_0__" no pudo medirse por su cabecera y decodificarlo excedería el límite de __ocs_fmt_1__ MB de esta build — no se puede adjuntar. .already-loaded-different-contents-kept = "__ocs_fmt_0__" ya está cargado con contenido diferente; se conservó el archivo existente. .opening-patreon-page = Abriendo la página de Patreon... .opening-feedback-page = Abriendo la página de comentarios... diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index c7a5a0051..7c48dcf57 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -1996,6 +1996,7 @@ file = .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__. .image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" on __ocs_fmt_1__×__ocs_fmt_2__ px ja sen purkaminen veisi __ocs_fmt_3__ MB, yli tämän version __ocs_fmt_4__ MB:n kuvarajan — sitä ei voi liittää. Rajan sisällä olevat kuvat liitetään tarvittaessa automaattisesti pienemmällä näyttötarkkuudella. + .image-refusal-unknown-size = Kohteen "__ocs_fmt_0__" kokoa ei voitu mitata otsakkeesta ja sen purkaminen ylittäisi tämän käännöksen __ocs_fmt_1__ MB kuvarajan — sitä ei voi liittää. selection = .cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Valitse objektit (__ocs_arg_count__ valittu, Enter käytettäväksi): @@ -3529,6 +3530,7 @@ view = .cannot-delete-the-current-annotation-scale-arg = Nykyistä huomautusasteikkoa (__ocs_fmt_0__) ei voi poistaa. .cascade = Kaskadi .color-scheme-set-to-name = Väriteemaksi asetettu '__ocs_fmt_0__'. + .color-scheme-set-to-system-currently-name = Väriteema asetettu arvoon "Järjestelmä" (tällä hetkellä __ocs_fmt_0__). .current-annotation-scale-name = Nykyinen merkintäasteikko: __ocs_fmt_0__ .datalink-path-to-the-csv-file = DATALINK polku .csv-tiedostoon: .datalink-cannot-read-path-e = DATALINK: ei voi lukea "__ocs_fmt_0__": __ocs_fmt_1__ @@ -3576,7 +3578,6 @@ view = .opening-ocs-discussions-for-help-and-questions = OCS-keskustelujen avaaminen apua ja kysymyksiä varten... .opening-ocs-web = Avataan OCS Web... .opening-the-command-reference = Avataan komentoviittausta... - .image-refusal-unknown-size = Kohteen "__ocs_fmt_0__" kokoa ei voitu mitata otsakkeesta ja sen purkaminen ylittäisi tämän käännöksen __ocs_fmt_1__ MB kuvarajan — sitä ei voi liittää. .already-loaded-different-contents-kept = "__ocs_fmt_0__" on jo ladattu eri sisällöllä; olemassa oleva tiedosto säilytettiin. .opening-patreon-page = Avataan Patreon-sivua... .opening-feedback-page = Palautesivua avataan... diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 6f43b0559..bdd70e550 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -1997,6 +1997,7 @@ file = .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__. .image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" fait __ocs_fmt_1__×__ocs_fmt_2__ px et son décodage nécessiterait __ocs_fmt_3__ Mo, au-delà de la limite de __ocs_fmt_4__ Mo par image de cette version — elle ne peut pas être jointe. Les images dans la limite sont jointes automatiquement à une résolution d'affichage réduite si nécessaire. + .image-refusal-unknown-size = "__ocs_fmt_0__" n’a pas pu être mesuré par son en-tête et son décodage dépasserait la limite de __ocs_fmt_1__ Mo de cette version — impossible de l’attacher. 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) : @@ -3530,6 +3531,7 @@ view = .cannot-delete-the-current-annotation-scale-arg = Impossible de supprimer l'échelle d'annotation actuelle (__ocs_fmt_0__). .cascade = Cascade .color-scheme-set-to-name = Schéma de couleur défini à '__ocs_fmt_0__'. + .color-scheme-set-to-system-currently-name = Jeu de couleurs défini sur « Système » (actuellement __ocs_fmt_0__). .current-annotation-scale-name = Échelle d'annotation actuelle : __ocs_fmt_0__ .datalink-path-to-the-csv-file = DATALINK chemin vers le fichier .csv : .datalink-cannot-read-path-e = DATALINK: ne peut pas lire "__ocs_fmt_0__": __ocs_fmt_1__ @@ -3577,7 +3579,6 @@ view = .opening-ocs-discussions-for-help-and-questions = Ouverture OCS Discussions pour l'aide et les questions... .opening-ocs-web = Ouverture du site Web OCS... .opening-the-command-reference = Ouverture de la référence des commandes... - .image-refusal-unknown-size = "__ocs_fmt_0__" n’a pas pu être mesuré par son en-tête et son décodage dépasserait la limite de __ocs_fmt_1__ Mo de cette version — impossible de l’attacher. .already-loaded-different-contents-kept = "__ocs_fmt_0__" est déjà chargé avec un contenu différent ; le fichier existant a été conservé. .opening-patreon-page = Ouverture de la page Patreon... .opening-feedback-page = Ouverture de la page de rétroaction... diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 11138781a..42f3ea823 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -1985,6 +1985,7 @@ file = .adding-folders-is-not-available-in-the-browser = ब्राउज़र में फ़ोल्डर जोड़ना उपलब्ध नहीं है। इसके बजाय ड्रॉइंग फ़ाइलें जोड़ें। .drawing-not-loaded-in-the-browser = ड्रॉइंग ब्राउज़र में लोड नहीं है: __ocs_fmt_0__। .image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" __ocs_fmt_1__×__ocs_fmt_2__ px है और इसे डिकोड करने पर __ocs_fmt_3__ MB लगेगा, जो इस बिल्ड की __ocs_fmt_4__ MB छवि सीमा से अधिक है — इसे संलग्न नहीं किया जा सकता। सीमा के भीतर की छवियाँ आवश्यकता पड़ने पर स्वचालित रूप से घटी हुई डिस्प्ले रिज़ॉल्यूशन पर संलग्न की जाती हैं। + .image-refusal-unknown-size = "__ocs_fmt_0__" को उसके हेडर से मापा नहीं जा सका और उसे डिकोड करना इस बिल्ड की __ocs_fmt_1__ MB छवि सीमा पार कर देगा — जोड़ा नहीं जा सकता। selection = .cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ ऑब्जेक्ट चुनें (__ocs_arg_count__ चयनित, लागू करने के लिए Enter दबाएँ): @@ -3518,6 +3519,7 @@ view = .cannot-delete-the-current-annotation-scale-arg = वर्तमान एनोटेशन स्केल (__ocs_fmt_0__) हटाया नहीं जा सकता। .cascade = कास्केड .color-scheme-set-to-name = रंग योजना '__ocs_fmt_0__' के लिए सेट की गई है। + .color-scheme-set-to-system-currently-name = रंग योजना 'सिस्टम' पर सेट की गई (वर्तमान में __ocs_fmt_0__)। .current-annotation-scale-name = वर्तमान एनोटेशन स्केल: __ocs_fmt_0__ .datalink-path-to-the-csv-file = DATALINK .csv फ़ाइल का पथ: .datalink-cannot-read-path-e = DATALINK: “__ocs_fmt_0__” पढ़ा नहीं जा सका: __ocs_fmt_1__ @@ -3565,7 +3567,6 @@ view = .opening-ocs-discussions-for-help-and-questions = OCS मदद और प्रश्नों के लिए चर्चा खोलना। .opening-ocs-web = OCS वेब खोलना ... .opening-the-command-reference = कमांड संदर्भ खोला जा रहा है... - .image-refusal-unknown-size = "__ocs_fmt_0__" को उसके हेडर से मापा नहीं जा सका और उसे डिकोड करना इस बिल्ड की __ocs_fmt_1__ MB छवि सीमा पार कर देगा — जोड़ा नहीं जा सकता। .already-loaded-different-contents-kept = "__ocs_fmt_0__" अलग सामग्री के साथ पहले से लोड है; मौजूदा फ़ाइल रखी गई। .opening-patreon-page = Patreon पृष्ठ खोला जा रहा है… .opening-feedback-page = प्रतिक्रिया पृष्ठ खोलना diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 4603a9f3f..36ac51541 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -1996,6 +1996,7 @@ file = .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__. .image-over-the-decode-limit-cannot-be-attached = A(z) "__ocs_fmt_0__" __ocs_fmt_1__×__ocs_fmt_2__ px felbontású, és a dekódolása __ocs_fmt_3__ MB-ot igényelne — ez meghaladja e build __ocs_fmt_4__ MB-os képhatárát, ezért nem csatolható. A határon belüli képek szükség esetén automatikusan csökkentett megjelenítési felbontással csatolódnak. + .image-refusal-unknown-size = A(z) "__ocs_fmt_0__" nem mérhető a fejlécéből, és dekódolása meghaladná ezen build __ocs_fmt_1__ MB-os képhatárát — nem csatolható. selection = .cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Objektumok kiválasztása (__ocs_arg_count__ kiválasztva, Enter alkalmazni): @@ -3529,6 +3530,7 @@ view = .cannot-delete-the-current-annotation-scale-arg = Nem lehet törölni az aktuális megjegyzésskálát (__ocs_fmt_0__). .cascade = Vízesés .color-scheme-set-to-name = A színséma „__ocs_fmt_0__” értékre van állítva. + .color-scheme-set-to-system-currently-name = Színséma beállítva: „Rendszer” (jelenleg __ocs_fmt_0__). .current-annotation-scale-name = Jelenlegi annotációs skála: __ocs_fmt_0__ .datalink-path-to-the-csv-file = DATALINK elérési út a .csv fájlhoz: .datalink-cannot-read-path-e = DATALINK: nem olvasható "__ocs_fmt_0__": __ocs_fmt_1__ @@ -3576,7 +3578,6 @@ view = .opening-ocs-discussions-for-help-and-questions = A OCS beszélgetések megnyitása segítségért és kérdésekért... .opening-ocs-web = A OCS web megnyitása... .opening-the-command-reference = A parancsreferencia megnyitása... - .image-refusal-unknown-size = A(z) "__ocs_fmt_0__" nem mérhető a fejlécéből, és dekódolása meghaladná ezen build __ocs_fmt_1__ MB-os képhatárát — nem csatolható. .already-loaded-different-contents-kept = A(z) "__ocs_fmt_0__" már betöltve eltérő tartalommal; a meglévő fájl megőrizve. .opening-patreon-page = Patreon oldal megnyitása... .opening-feedback-page = Visszajelzési oldal megnyitása... diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index de23dea57..3a4184327 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -1996,6 +1996,7 @@ file = .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__. .image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" è di __ocs_fmt_1__×__ocs_fmt_2__ px e la decodifica richiederebbe __ocs_fmt_3__ MB, oltre il limite di __ocs_fmt_4__ MB per immagine di questa build: non può essere allegata. Le immagini entro il limite vengono allegate automaticamente a risoluzione di visualizzazione ridotta quando necessario. + .image-refusal-unknown-size = "__ocs_fmt_0__" non è stato misurabile dall’intestazione e decodificarlo supererebbe il limite di __ocs_fmt_1__ MB di questa build — impossibile allegarlo. selection = .cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Seleziona oggetti (__ocs_arg_count__ selezionato, Enter da applicare): @@ -3529,6 +3530,7 @@ view = .cannot-delete-the-current-annotation-scale-arg = Impossibile eliminare la scala di annotazione corrente (__ocs_fmt_0__). .cascade = Cascata .color-scheme-set-to-name = Combinazione di colori impostata su "__ocs_fmt_0__". + .color-scheme-set-to-system-currently-name = Combinazione colori impostata su "Sistema" (attualmente __ocs_fmt_0__). .current-annotation-scale-name = Scala di annotazione corrente: __ocs_fmt_0__ .datalink-path-to-the-csv-file = DATALINK percorso del file .csv: .datalink-cannot-read-path-e = DATALINK: impossibile leggere "__ocs_fmt_0__": __ocs_fmt_1__ @@ -3576,7 +3578,6 @@ view = .opening-ocs-discussions-for-help-and-questions = Apertura delle discussioni OCS per assistenza e domande... .opening-ocs-web = Apertura del Web OCS... .opening-the-command-reference = Apertura del riferimento dei comandi... - .image-refusal-unknown-size = "__ocs_fmt_0__" non è stato misurabile dall’intestazione e decodificarlo supererebbe il limite di __ocs_fmt_1__ MB di questa build — impossibile allegarlo. .already-loaded-different-contents-kept = "__ocs_fmt_0__" è già caricato con contenuti diversi; il file esistente è stato mantenuto. .opening-patreon-page = Apertura della pagina Patreon... .opening-feedback-page = Apertura della pagina di feedback... diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index ff76117ab..db8a03a10 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -1987,6 +1987,7 @@ file = .adding-folders-is-not-available-in-the-browser = ブラウザーではフォルダーを追加できません。代わりに図面ファイルを追加してください。 .drawing-not-loaded-in-the-browser = 図面がブラウザーに読み込まれていません: __ocs_fmt_0__。 .image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" は __ocs_fmt_1__×__ocs_fmt_2__ px で、デコードすると __ocs_fmt_3__ MB 必要となり、このビルドの __ocs_fmt_4__ MB の画像上限を超えるため添付できません。上限内の画像は、必要に応じて自動的に低下させた表示解像度で添付されます。 + .image-refusal-unknown-size = "__ocs_fmt_0__" のサイズをヘッダーから測定できず、デコードするとこのビルドの __ocs_fmt_1__ MB 画像制限を超えます — 添付できません。 selection = .cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ オブジェクトを選択(__ocs_arg_count__ 個選択、Enter で適用): @@ -3518,6 +3519,7 @@ view = .cannot-delete-the-current-annotation-scale-arg = 現在の注釈尺度 (__ocs_fmt_0__) は削除できません。 .cascade = 重ねて表示 .color-scheme-set-to-name = 配色を '__ocs_fmt_0__' に設定しました。 + .color-scheme-set-to-system-currently-name = カラースキームを「システム」に設定しました(現在: __ocs_fmt_0__)。 .current-annotation-scale-name = 現在の注釈尺度: __ocs_fmt_0__ .datalink-path-to-the-csv-file = DATALINK .csv ファイルのパス: .datalink-cannot-read-path-e = DATALINK: "__ocs_fmt_0__" を読み込めません: __ocs_fmt_1__ @@ -3565,7 +3567,6 @@ view = .opening-ocs-discussions-for-help-and-questions = ヘルプと質問のため OCS ディスカッションを開いています... .opening-ocs-web = OCS Web を開いています... .opening-the-command-reference = コマンドリファレンスを開いています... - .image-refusal-unknown-size = "__ocs_fmt_0__" のサイズをヘッダーから測定できず、デコードするとこのビルドの __ocs_fmt_1__ MB 画像制限を超えます — 添付できません。 .already-loaded-different-contents-kept = "__ocs_fmt_0__" は既に別の内容で読み込まれています。既存のファイルを保持しました。 .opening-patreon-page = Patreon ページを開いています... .opening-feedback-page = フィードバックページを開いています... diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 23243f623..3d8dc8530 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -1996,6 +1996,7 @@ file = .adding-folders-is-not-available-in-the-browser = 브라우저에서는 폴더를 추가할 수 없습니다. 대신 도면 파일을 추가하세요. .drawing-not-loaded-in-the-browser = 도면이 브라우저에 로드되지 않았습니다: __ocs_fmt_0__. .image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__"은(는) __ocs_fmt_1__×__ocs_fmt_2__ px이며 디코딩 시 __ocs_fmt_3__ MB가 필요해 이 빌드의 __ocs_fmt_4__ MB 이미지 한도를 초과하므로 첨부할 수 없습니다. 한도 이내의 이미지는 필요할 때 자동으로 낮춘 표시 해상도로 첨부됩니다. + .image-refusal-unknown-size = "__ocs_fmt_0__"의 크기를 헤더에서 측정할 수 없고 디코딩 시 이 빌드의 __ocs_fmt_1__ MB 이미지 한도를 초과합니다 — 첨부할 수 없습니다. selection = .cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ 객체 선택(__ocs_arg_count__ 선택, Enter 적용): @@ -3529,6 +3530,7 @@ view = .cannot-delete-the-current-annotation-scale-arg = 현재 주석 축척(__ocs_fmt_0__)을 삭제할 수 없습니다. .cascade = 캐스케이드 .color-scheme-set-to-name = 색 구성표가 '__ocs_fmt_0__'로 설정되었습니다. + .color-scheme-set-to-system-currently-name = 색 구성표가 '시스템'으로 설정되었습니다(현재 __ocs_fmt_0__). .current-annotation-scale-name = 현재 주석 축척: __ocs_fmt_0__ .datalink-path-to-the-csv-file = .csv 파일에 대한 DATALINK 경로: .datalink-cannot-read-path-e = DATALINK: "__ocs_fmt_0__"를 읽을 수 없습니다: __ocs_fmt_1__ @@ -3576,7 +3578,6 @@ view = .opening-ocs-discussions-for-help-and-questions = OCS 열기 도움말 및 질문에 대한 토론... .opening-ocs-web = OCS 웹을 여는 중... .opening-the-command-reference = 명령 참조 열기... - .image-refusal-unknown-size = "__ocs_fmt_0__"의 크기를 헤더에서 측정할 수 없고 디코딩 시 이 빌드의 __ocs_fmt_1__ MB 이미지 한도를 초과합니다 — 첨부할 수 없습니다. .already-loaded-different-contents-kept = "__ocs_fmt_0__"이(가) 이미 다른 내용으로 로드되었습니다. 기존 파일을 유지했습니다. .opening-patreon-page = Patreon 페이지를 여는 중... .opening-feedback-page = 피드백 페이지를 여는 중... diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 3122049b1..a4440d24f 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -1997,6 +1997,7 @@ file = .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__. .image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" is __ocs_fmt_1__×__ocs_fmt_2__ px en zou bij het decoderen __ocs_fmt_3__ MB nodig hebben, boven de __ocs_fmt_4__ MB afbeeldingslimiet van deze build — het kan niet worden bijgevoegd. Afbeeldingen binnen de limiet worden zo nodig automatisch met een verlaagde weergaveresolutie bijgevoegd. + .image-refusal-unknown-size = "__ocs_fmt_0__" kon niet via de header gemeten worden en decoderen overschrijdt de __ocs_fmt_1__ MB-limiet van deze build — kan niet worden toegevoegd. selection = .cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Selecteer objecten (__ocs_arg_count__ geselecteerd, Voer in om toe te passen): @@ -3528,6 +3529,7 @@ view = .cannot-delete-the-current-annotation-scale-arg = Kan de huidige annotatieschaal (__ocs_fmt_0__) niet verwijderen. .cascade = Cascade .color-scheme-set-to-name = Kleurschema ingesteld op '__ocs_fmt_0__'. + .color-scheme-set-to-system-currently-name = Kleurenschema ingesteld op 'Systeem' (momenteel __ocs_fmt_0__). .current-annotation-scale-name = Huidige annotatieschaal: __ocs_fmt_0__ .datalink-path-to-the-csv-file = DATALINK pad naar het .csv bestand: .datalink-cannot-read-path-e = DATALINK: kan "__ocs_fmt_0__" niet lezen: __ocs_fmt_1__ @@ -3575,7 +3577,6 @@ view = .opening-ocs-discussions-for-help-and-questions = Openen OCS Discussie voor hulp en vragen... .opening-ocs-web = Openen OCS Web... .opening-the-command-reference = Commandoreferentie openen... - .image-refusal-unknown-size = "__ocs_fmt_0__" kon niet via de header gemeten worden en decoderen overschrijdt de __ocs_fmt_1__ MB-limiet van deze build — kan niet worden toegevoegd. .already-loaded-different-contents-kept = "__ocs_fmt_0__" is al geladen met andere inhoud; het bestaande bestand is bewaard. .opening-patreon-page = Patreon pagina openen... .opening-feedback-page = Herinneringspagina openen... diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 12617c25e..ae42e91bc 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -1996,6 +1996,7 @@ file = .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__. .image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" ma __ocs_fmt_1__×__ocs_fmt_2__ px, a jej dekodowanie wymagałoby __ocs_fmt_3__ MB — ponad __ocs_fmt_4__ MB limit obrazu w tej wersji — więc nie może być dołączona. Obrazy w ramach limitu są w razie potrzeby dołączane automatycznie w obniżonej rozdzielczości wyświetlania. + .image-refusal-unknown-size = „__ocs_fmt_0__” nie dał się zmierzyć z nagłówka, a jego dekodowanie przekroczy limit __ocs_fmt_1__ MB tej kompilacji — nie można go dołączyć. selection = .cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Wybierz obiekty (wybrano __ocs_arg_count__, zastosowano Enter): @@ -3529,6 +3530,7 @@ view = .cannot-delete-the-current-annotation-scale-arg = Nie można usunąć bieżącej skali adnotacji (__ocs_fmt_0__). .cascade = Kaskada .color-scheme-set-to-name = Schemat kolorów ustawiony na „__ocs_fmt_0__”. + .color-scheme-set-to-system-currently-name = Schemat kolorów ustawiono na „System” (obecnie __ocs_fmt_0__). .current-annotation-scale-name = Aktualna skala adnotacji: __ocs_fmt_0__ .datalink-path-to-the-csv-file = Ścieżka DATALINK do pliku .csv: .datalink-cannot-read-path-e = DATALINK: nie można odczytać „__ocs_fmt_0__”: __ocs_fmt_1__ @@ -3576,7 +3578,6 @@ view = .opening-ocs-discussions-for-help-and-questions = Otwieranie dyskusji OCS w celu uzyskania pomocy i pytań... .opening-ocs-web = Otwieranie OCS Web... .opening-the-command-reference = Otwieranie dokumentacji poleceń... - .image-refusal-unknown-size = „__ocs_fmt_0__” nie dał się zmierzyć z nagłówka, a jego dekodowanie przekroczy limit __ocs_fmt_1__ MB tej kompilacji — nie można go dołączyć. .already-loaded-different-contents-kept = "__ocs_fmt_0__" jest już wczytany z inną zawartością; zachowano istniejący plik. .opening-patreon-page = Otwieranie strony Patreon... .opening-feedback-page = Otwieram stronę z opiniami... diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index a68c91621..2f69511a1 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -1997,6 +1997,7 @@ file = .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__. .image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" tem __ocs_fmt_1__×__ocs_fmt_2__ px e a decodificação exigiria __ocs_fmt_3__ MB, acima do limite de __ocs_fmt_4__ MB por imagem desta versão — não é possível anexá-la. Imagens dentro do limite são anexadas automaticamente com resolução de exibição reduzida quando necessário. + .image-refusal-unknown-size = "__ocs_fmt_0__" não pôde ser medido pelo cabeçalho e a sua descodificação excedeu o limite de __ocs_fmt_1__ MB desta build — não pode ser anexado. selection = .cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Selecione objetos (__ocs_arg_count__ selecionados, Enter para aplicar): @@ -3530,6 +3531,7 @@ view = .cannot-delete-the-current-annotation-scale-arg = Não é possível excluir a escala de anotação atual (__ocs_fmt_0__). .cascade = Cascata .color-scheme-set-to-name = Esquema de cores definido como “__ocs_fmt_0__”. + .color-scheme-set-to-system-currently-name = Esquema de cores definido como "Sistema" (atualmente __ocs_fmt_0__). .current-annotation-scale-name = Escala de anotação atual: __ocs_fmt_0__ .datalink-path-to-the-csv-file = DATALINK caminho do arquivo .csv: .datalink-cannot-read-path-e = DATALINK: não é possível ler “__ocs_fmt_0__”: __ocs_fmt_1__ @@ -3577,7 +3579,6 @@ view = .opening-ocs-discussions-for-help-and-questions = Abrindo Discussões do OCS para ajuda e perguntas... .opening-ocs-web = Abrindo OCS Web... .opening-the-command-reference = Abrindo a referência de comandos... - .image-refusal-unknown-size = "__ocs_fmt_0__" não pôde ser medido pelo cabeçalho e a sua descodificação excedeu o limite de __ocs_fmt_1__ MB desta build — não pode ser anexado. .already-loaded-different-contents-kept = "__ocs_fmt_0__" já está carregado com conteúdo diferente; o ficheiro existente foi mantido. .opening-patreon-page = Abrindo página do Patreon... .opening-feedback-page = Abrindo página de comentários... diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index 800fb2fea..a386e0e85 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -2000,6 +2000,7 @@ file = .adding-folders-is-not-available-in-the-browser = Добавление папок в браузере недоступно. Вместо этого добавьте файлы чертежей. .drawing-not-loaded-in-the-browser = Чертёж не загружен в браузере: __ocs_fmt_0__. .image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" — __ocs_fmt_1__×__ocs_fmt_2__ px; для декодирования потребовалось бы __ocs_fmt_3__ МБ, что превышает ограничение __ocs_fmt_4__ МБ на изображение в этой сборке, поэтому вложить его нельзя. Изображения в пределах ограничения при необходимости вкладываются автоматически с пониженным разрешением отображения. + .image-refusal-unknown-size = «__ocs_fmt_0__» не удалось измерить по заголовку, а его декодирование превысит лимит __ocs_fmt_1__ МБ этой сборки — вложить нельзя. selection = .cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Выберите объекты (__ocs_arg_count__ Выберите, Введите для применения): @@ -3531,6 +3532,7 @@ view = .cannot-delete-the-current-annotation-scale-arg = Невозможно удалить текущий масштаб аннотаций (__ocs_fmt_0__). .cascade = каскад .color-scheme-set-to-name = Цветовая схема, установленная на '__ocs_fmt_0__'. + .color-scheme-set-to-system-currently-name = Цветовая схема установлена на «Системную» (сейчас __ocs_fmt_0__). .current-annotation-scale-name = Текущий масштаб аннотаций: __ocs_fmt_0__ .datalink-path-to-the-csv-file = DATALINK Путь к файлу .csv: .datalink-cannot-read-path-e = DATALINK: не может читать "__ocs_fmt_0__": __ocs_fmt_1__ @@ -3578,7 +3580,6 @@ view = .opening-ocs-discussions-for-help-and-questions = Открытие OCS Дискуссии о помощи и вопросах .opening-ocs-web = Открытие OCS Веб-сайт .opening-the-command-reference = Открытие справочника команд... - .image-refusal-unknown-size = «__ocs_fmt_0__» не удалось измерить по заголовку, а его декодирование превысит лимит __ocs_fmt_1__ МБ этой сборки — вложить нельзя. .already-loaded-different-contents-kept = "__ocs_fmt_0__" уже загружен с другим содержимым; существующий файл сохранён. .opening-patreon-page = Открытие страницы Patreon... .opening-feedback-page = Открываем страницу обратной связи... diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 9a407ea40..71c3720ff 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -1980,6 +1980,7 @@ file = .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__. .image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__" __ocs_fmt_1__×__ocs_fmt_2__ px boyutunda ve çözümlendiğinde __ocs_fmt_3__ MB gerektirir; bu sürümün __ocs_fmt_4__ MB görsel sınırını aştığından eklenemez. Sınır içindeki görseller gerektiğinde otomatik olarak düşürülmüş görüntü çözünürlüğüyle eklenir. + .image-refusal-unknown-size = "__ocs_fmt_0__" başlığından ölçülemedi ve kodu çözülürse bu sürümün __ocs_fmt_1__ MB görüntü limiti aşılır — eklenemez. selection = .cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ Nesneleri seçin (__ocs_arg_count__ seçili, uygulamak için Enter): @@ -3483,6 +3484,7 @@ view = .cannot-delete-the-current-annotation-scale-arg = Geçerli açıklama ölçeği (__ocs_fmt_0__) silinemez. .cascade = Basamakla .color-scheme-set-to-name = Renk düzeni '__ocs_fmt_0__' olarak ayarlandı. + .color-scheme-set-to-system-currently-name = Renk düzeni 'Sistem' olarak ayarlandı (şu anda __ocs_fmt_0__). .current-annotation-scale-name = Geçerli açıklama ölçeği: __ocs_fmt_0__ .datalink-path-to-the-csv-file = DATALINK .csv dosyasının yolu: .datalink-specify-insertion-point = DATALINK Ekleme noktasını belirtin: @@ -3527,7 +3529,6 @@ view = .opening-ocs-discussions-for-help-and-questions = Yardım ve sorular için OCS Tartışmaları açılıyor... .opening-ocs-web = OCS Web açılıyor... .opening-the-command-reference = Komut başvurusu açılıyor... - .image-refusal-unknown-size = "__ocs_fmt_0__" başlığından ölçülemedi ve kodu çözülürse bu sürümün __ocs_fmt_1__ MB görüntü limiti aşılır — eklenemez. .already-loaded-different-contents-kept = "__ocs_fmt_0__" zaten farklı içerikle yüklü; mevcut dosya tutuldu. .opening-patreon-page = Patreon sayfası açılıyor... .opening-feedback-page = Geri bildirim sayfası açılıyor... diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 709b3fdd2..4e8995f33 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -1984,6 +1984,7 @@ file = .adding-folders-is-not-available-in-the-browser = 浏览器中无法添加文件夹。请改为添加图形文件。 .drawing-not-loaded-in-the-browser = 图形未加载到浏览器中:__ocs_fmt_0__。 .image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__"为 __ocs_fmt_1__×__ocs_fmt_2__ 像素,解码需要 __ocs_fmt_3__ MB,超过此版本的 __ocs_fmt_4__ MB 图像上限,因此无法附着。上限内的图像在需要时会自动以降低的显示分辨率附着。 + .image-refusal-unknown-size = 无法从文件头测得“__ocs_fmt_0__”的尺寸,解码将超过本版本 __ocs_fmt_1__ MB 的图像上限——无法附着。 selection = .cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ 选择对象(已选择 __ocs_arg_count__ 个,按 Enter 应用): @@ -3513,6 +3514,7 @@ view = .cannot-delete-the-current-annotation-scale-arg = 无法删除当前注解缩放 (__ocs_fmt_0__) 。 .cascade = 层叠 .color-scheme-set-to-name = 设置为“ __ocs_fmt_0__ ” 的颜色方案 。 + .color-scheme-set-to-system-currently-name = 颜色方案已设为“系统”(当前:__ocs_fmt_0__)。 .current-annotation-scale-name = 当前注释比例 : __ocs_fmt_0__ .datalink-path-to-the-csv-file = DATALINK 路径到.csv 文件 : .datalink-cannot-read-path-e = DATALINK: 无法读取 “__ocs_fmt_0__ ” : __ocs_fmt_1__ @@ -3560,7 +3562,6 @@ view = .opening-ocs-discussions-for-help-and-questions = 开幕OCS 讨论帮助和问题... .opening-ocs-web = 打开OCS网络... .opening-the-command-reference = 正在打开命令参考... - .image-refusal-unknown-size = 无法从文件头测得“__ocs_fmt_0__”的尺寸,解码将超过本版本 __ocs_fmt_1__ MB 的图像上限——无法附着。 .already-loaded-different-contents-kept = "__ocs_fmt_0__"已以不同内容加载;保留了现有文件。 .opening-patreon-page = 打开帕特伦的页面... .opening-feedback-page = 打开反馈页面... diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index 3c8225c23..0acf500ad 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -1996,6 +1996,7 @@ file = .adding-folders-is-not-available-in-the-browser = 瀏覽器中無法新增資料夾。請改為新增圖面檔案。 .drawing-not-loaded-in-the-browser = 圖面未載入瀏覽器:__ocs_fmt_0__。 .image-over-the-decode-limit-cannot-be-attached = "__ocs_fmt_0__"為 __ocs_fmt_1__×__ocs_fmt_2__ 像素,解碼需要 __ocs_fmt_3__ MB,超過此組建的 __ocs_fmt_4__ MB 影像上限,因此無法貼附。上限內的影像會在需要時自動以降低的顯示解析度貼附。 + .image-refusal-unknown-size = 無法從檔頭測得「__ocs_fmt_0__」的尺寸,解碼將超過本版本 __ocs_fmt_1__ MB 的影像上限——無法附加。 selection = .cmd-select-objects-count-selected-enter-to-app = __ocs_arg_cmd__ 選擇物件(已選擇 __ocs_arg_count__ 個,按 Enter 套用): @@ -3525,6 +3526,7 @@ view = .cannot-delete-the-current-annotation-scale-arg = 無法刪除目前注解縮放 (__ocs_fmt_0__) 。 .cascade = 層疊 .color-scheme-set-to-name = 設定為“ __ocs_fmt_0__ ” 的顏色方案 。 + .color-scheme-set-to-system-currently-name = 配色方案已設為「系統」(目前:__ocs_fmt_0__)。 .current-annotation-scale-name = 目前註解比例 : __ocs_fmt_0__ .datalink-path-to-the-csv-file = DATALINK 路徑到.csv 檔案 : .datalink-cannot-read-path-e = DATALINK: 無法讀取 “__ocs_fmt_0__ ” : __ocs_fmt_1__ @@ -3572,7 +3574,6 @@ view = .opening-ocs-discussions-for-help-and-questions = 開幕OCS 討論幫助和問題... .opening-ocs-web = 開啟OCS網路... .opening-the-command-reference = 正在開啟命令參考... - .image-refusal-unknown-size = 無法從檔頭測得「__ocs_fmt_0__」的尺寸,解碼將超過本版本 __ocs_fmt_1__ MB 的影像上限——無法附加。 .already-loaded-different-contents-kept = "__ocs_fmt_0__"已以不同內容載入;保留了現有檔案。 .opening-patreon-page = 開啟帕特倫的頁面... .opening-feedback-page = 開啟反饋頁面... diff --git a/src/app/annotation_data.rs b/src/app/annotation_data.rs index 997dd0465..2bb457883 100644 --- a/src/app/annotation_data.rs +++ b/src/app/annotation_data.rs @@ -1108,6 +1108,34 @@ impl OpenCADStudio { Task::none() } + /// Web DATALINK: the picked .csv's bytes arrived. They are registered in + /// the picked-source registry — so `DATALINKUPDATE` re-reads them + /// in-session instead of failing on the absent filesystem — and fed to + /// the same table-insert path the native typed-path command uses. + #[cfg(target_arch = "wasm32")] + pub(super) fn on_datalink_csv_picked( + &mut self, + picked: Option<(String, Vec)>, + ) -> Task { + let Some((name, bytes)) = picked else { + return Task::none(); + }; + let text = String::from_utf8_lossy(&bytes).into_owned(); + if !register_picked_source(&name, Arc::new(bytes)) { + if let Some(name) = take_last_collision().take() { + self.command_line.push_warning( + crate::tf!( + "\"{name}\" is already loaded from another folder with different contents; re-picking it was ignored to keep the existing link." + ) + .as_ref(), + ); + } + return Task::none(); + } + self.place_data_link_table(&text, &name); + Task::none() + } + pub(super) fn on_data_link_browse(&mut self) -> Task { Task::perform( async { diff --git a/src/app/commands/display.rs b/src/app/commands/display.rs index a7604403e..b020e0d15 100644 --- a/src/app/commands/display.rs +++ b/src/app/commands/display.rs @@ -1595,49 +1595,25 @@ impl OpenCADStudio { ); return Some(Task::none()); } + // Web: nothing is behind a typed path — pick the .csv through + // the browser, read its bytes at pick time, and register them + // in the picked-source registry; the same table-insert path + // then runs from `DataLinkCsvPicked`. + #[cfg(target_arch = "wasm32")] + { + let _ = path; + return Some(Task::perform( + crate::io::pick_datalink_csv_web(), + Message::DataLinkCsvPicked, + )); + } + #[cfg(not(target_arch = "wasm32"))] match std::fs::read_to_string(path) { Ok(text) => { - let rows_data = parse_csv_table(&text); - let nrows = rows_data.len(); - let ncols = rows_data.iter().map(|r| r.len()).max().unwrap_or(0); - if nrows == 0 || ncols == 0 { - self.command_line - .push_error(crate::t!("DATALINK: the CSV file is empty.").as_ref()); - return Some(Task::none()); - } - use acadrust::entities::TableBuilder; - use acadrust::types::Vector3; - let mut table = TableBuilder::new(nrows, ncols) - .at(Vector3::new(0.0, 0.0, 0.0)) - .row_height(0.5) - .column_width(2.0) - .build(); - for (r, row) in rows_data.iter().enumerate() { - for (c, cell) in row.iter().enumerate() { - table.set_cell_text(r, c, cell); - } - } - let doc = &self.tabs[i].scene.document; - let current_style = doc.header.current_table_style_name.clone(); - table.table_style_handle = doc.objects.iter().find_map(|(handle, object)| { - match object { - acadrust::objects::ObjectType::TableStyle(style) - if style.name.eq_ignore_ascii_case(¤t_style) => - { - Some(*handle) - } - _ => None, - } - }); let link_path = std::fs::canonicalize(path) .map(|value| value.to_string_lossy().into_owned()) .unwrap_or_else(|_| path.to_string()); - let command = crate::modules::annotate::data_link::DataLinkPlaceCommand::new( - table, &link_path, - ); - self.command_line - .push_info(&crate::command::CadCommand::prompt(&command)); - self.tabs[i].active_cmd = Some(Box::new(command)); + self.place_data_link_table(&text, &link_path); } Err(e) => { self.command_line @@ -1849,6 +1825,51 @@ impl OpenCADStudio { } Some(self.finish_dispatch(cmd)) } + + /// Build the linked table from CSV text and start the placement command — + /// the shared tail of the native typed-path `DATALINK` read and the web + /// picker's `DataLinkCsvPicked` (which registers the picked bytes in the + /// picked-source registry first so the link re-reads in-session). + pub(in crate::app) fn place_data_link_table(&mut self, csv_text: &str, link_path: &str) { + let i = self.active_tab; + let rows_data = parse_csv_table(csv_text); + let nrows = rows_data.len(); + let ncols = rows_data.iter().map(|r| r.len()).max().unwrap_or(0); + if nrows == 0 || ncols == 0 { + self.command_line + .push_error(crate::t!("DATALINK: the CSV file is empty.").as_ref()); + return; + } + use acadrust::entities::TableBuilder; + use acadrust::types::Vector3; + let mut table = TableBuilder::new(nrows, ncols) + .at(Vector3::new(0.0, 0.0, 0.0)) + .row_height(0.5) + .column_width(2.0) + .build(); + for (r, row) in rows_data.iter().enumerate() { + for (c, cell) in row.iter().enumerate() { + table.set_cell_text(r, c, cell); + } + } + let doc = &self.tabs[i].scene.document; + let current_style = doc.header.current_table_style_name.clone(); + table.table_style_handle = doc.objects.iter().find_map(|(handle, object)| { + match object { + acadrust::objects::ObjectType::TableStyle(style) + if style.name.eq_ignore_ascii_case(¤t_style) => + { + Some(*handle) + } + _ => None, + } + }); + let command = + crate::modules::annotate::data_link::DataLinkPlaceCommand::new(table, link_path); + self.command_line + .push_info(&crate::command::CadCommand::prompt(&command)); + self.tabs[i].active_cmd = Some(Box::new(command)); + } } // Scan LandXML text for survey points. Each element's text content is diff --git a/src/app/commands/view.rs b/src/app/commands/view.rs index 2428a1e2c..25e2d49e7 100644 --- a/src/app/commands/view.rs +++ b/src/app/commands/view.rs @@ -458,7 +458,8 @@ impl OpenCADStudio { self.command_line.push_output(&crate::tf!("Available themes: {}", "DARK LIGHT DRACULA NORD SOLARIZED_LIGHT SOLARIZED_DARK \ GRUVBOX_LIGHT GRUVBOX_DARK TOKYONIGHT TOKYONIGHTSTORM TOKYONIGHTLIGHT \ - KANAGAWAWAVE KANAGAWADRAGON KANAGAWALOTUS MOONFLY NIGHTFLY OXOCARBON FERRA", + KANAGAWAWAVE KANAGAWADRAGON KANAGAWALOTUS MOONFLY NIGHTFLY OXOCARBON FERRA \ + SYSTEM (follows the OS or browser color scheme)", )); self.command_line.push_info(&crate::tf!( "Current color scheme: <{}>", @@ -482,16 +483,40 @@ impl OpenCADStudio { self.command_line.push_output(&crate::tf!("Available themes: {}", "DARK LIGHT DRACULA NORD SOLARIZED_LIGHT SOLARIZED_DARK \ GRUVBOX_LIGHT GRUVBOX_DARK TOKYONIGHT TOKYONIGHTSTORM TOKYONIGHTLIGHT \ - KANAGAWAWAVE KANAGAWADRAGON KANAGAWALOTUS MOONFLY NIGHTFLY OXOCARBON FERRA", + KANAGAWAWAVE KANAGAWADRAGON KANAGAWALOTUS MOONFLY NIGHTFLY OXOCARBON FERRA \ + SYSTEM (follows the OS or browser color scheme)", )); return Some(Task::none()); } if sub == "0" || sub == "1" { self.command_line.push_error( - crate::t!("COLORSCHEME: 0 and 1 are COLORTHEME options. Enter a theme name (e.g. DARK, LIGHT, DRACULA) or use COLORTHEME.").as_ref(), + crate::t!("COLORSCHEME: 0 and 1 are COLORTHEME options. Enter a theme name (e.g. DARK, LIGHT, SYSTEM) or use COLORTHEME.").as_ref(), ); return Some(Task::none()); } + // SYSTEM keeps its own name in the saved config and resolves + // to a concrete Light/Dark theme at run time — the browser's + // prefers-color-scheme on the web, the Windows personalization + // setting natively. Re-running the command re-resolves; there + // is no live preference listener. + if upper_sub == "SYSTEM" { + if self.ui_theme.name == "Custom" { + self.saved_custom_palette = Some(self.ui_theme.palette); + } + let resolved = crate::app::config::resolve_system_theme(); + let name = format!("{resolved:?}"); + self.ui_theme.name = "System".to_string(); + self.ui_theme.palette = + crate::app::config::UiThemePalette::from_iced(resolved.seed()); + self.theme_color_inputs = self.ui_theme.palette.hex_values(); + self.active_theme = resolved; + self.sync_model_space_theme(true); + self.persist_settings_if_changed(); + self.command_line.push_output(crate::tf!( + "Color scheme set to 'System' (currently {name})." + ).as_ref()); + return Some(Task::none()); + } let theme = crate::app::config::parse_theme_name(sub); if let Some(t) = theme { if self.ui_theme.name == "Custom" { diff --git a/src/app/config.rs b/src/app/config.rs index 6d1c8f0eb..60ef4ff2c 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -102,12 +102,77 @@ impl UiThemeConfig { pub fn to_iced(&self) -> iced::Theme { if self.name == "Custom" { iced::Theme::custom("Custom", self.palette.to_iced()) + } else if self.name.eq_ignore_ascii_case("System") { + resolve_system_theme() } else { builtin_theme(&self.name).unwrap_or(iced::Theme::Oxocarbon) } } } +/// Resolve the `System` color scheme (COLORSCHEME SYSTEM) to a concrete +/// theme: the browser `prefers-color-scheme` media query on the web, the +/// Windows `AppsUseLightTheme` personalization value natively, and the dark +/// default on the remaining native platforms (no cheap OS query there). +/// Read at boot (`apply_config`) and re-read whenever COLORSCHEME SYSTEM +/// runs again — there is no live preference listener by design. +pub fn resolve_system_theme() -> iced::Theme { + #[cfg(target_arch = "wasm32")] + { + let light = web_sys::window().is_some_and(|window| { + window + .match_media("(prefers-color-scheme: light)") + .ok() + .flatten() + .is_some_and(|query| query.matches()) + }); + if light { + iced::Theme::Light + } else { + iced::Theme::Dark + } + } + #[cfg(all(not(target_arch = "wasm32"), target_os = "windows"))] + { + use windows_sys::Win32::System::Registry::{ + RegGetValueW, HKEY_CURRENT_USER, RRF_RT_REG_DWORD, + }; + let sub_key: Vec = "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize" + .encode_utf16() + .chain(std::iter::once(0)) + .collect(); + let value: Vec = "AppsUseLightTheme" + .encode_utf16() + .chain(std::iter::once(0)) + .collect(); + let mut data: u32 = 0; + let mut size = std::mem::size_of::() as u32; + // ERROR_SUCCESS (0) with data 1 means the apps theme is Light; a + // missing value (older Windows, wiped personalization) is Dark. + let light = unsafe { + RegGetValueW( + HKEY_CURRENT_USER, + sub_key.as_ptr(), + value.as_ptr(), + RRF_RT_REG_DWORD, + std::ptr::null_mut(), + &mut data as *mut u32 as *mut core::ffi::c_void, + &mut size, + ) == 0 + && data == 1 + }; + if light { + iced::Theme::Light + } else { + iced::Theme::Dark + } + } + #[cfg(not(any(target_arch = "wasm32", target_os = "windows")))] + { + iced::Theme::Dark + } +} + #[derive(Clone, Copy, PartialEq, Serialize, Deserialize)] #[serde(default)] pub struct UiThemePalette { @@ -503,6 +568,25 @@ fn config_path() -> Option { mod tests { use super::*; + #[test] + fn system_scheme_resolves_to_a_concrete_theme_and_keeps_its_name() { + // Fresh configs keep the existing default; SYSTEM is opt-in. + assert_ne!(UiThemeConfig::default().name, "System"); + let mut cfg = UiThemeConfig::default(); + cfg.name = "System".to_string(); + let resolved = cfg.to_iced(); + // Case-insensitive like the other stored names. + cfg.name = "system".to_string(); + assert_eq!(cfg.to_iced(), resolved); + assert!( + matches!(resolved, iced::Theme::Light | iced::Theme::Dark), + "SYSTEM must resolve through the platform preference" + ); + // The stored name survives resolution; re-runs re-resolve. + assert_eq!(cfg.name, "system"); + assert_eq!(resolve_system_theme(), resolved); + } + #[test] fn test_model_space_defaults() { let cfg = ModelSpaceThemeConfig::default(); diff --git a/src/app/control/mod.rs b/src/app/control/mod.rs index c255d9faf..6262cf732 100644 --- a/src/app/control/mod.rs +++ b/src/app/control/mod.rs @@ -106,6 +106,10 @@ pub(super) struct State { completed: VecDeque<(String, Value, Value)>, owner: Option<(u64, String)>, pub enabled: bool, + /// Set by the first control request of the session (or by a web host's + /// boot-time opt-in): while control is enabled, startup prompts that + /// would block automation — the donation prompt — are suppressed (#1349). + pub automation_seen: bool, serial: u64, events: VecDeque, pub(super) routing: bool, @@ -346,7 +350,37 @@ impl OpenCADStudio { pub(super) fn control_state(&self) -> Value { let tab = &self.tabs[self.active_tab]; let command = tab.active_cmd.as_deref().map(active_command_metadata); - json!({"ok":true,"protocol":1,"session_id":session_id(),"version":env!("OCS_APP_VERSION"), + // Protocol 2 adds the web in-memory `open` (base64 bytes); protocol 1 + // requests remain accepted unchanged. + // The web-only `open_bytes` push below is the sole mutation, so + // native builds see the binding as immutable. + #[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut))] + let mut capabilities = vec![ + "commands", + "command_manifest", + "step_input", + "batch", + "compact_results", + "entity_pick", + "structure_pick", + "selection", + "properties", + "records", + "record_schemas", + "record_filters", + "atomic_record_updates", + "layers", + "history", + "documents", + "events", + "capture", + "viewport_capture", + "measure", + "spatial_query", + ]; + #[cfg(target_arch = "wasm32")] + capabilities.push("open_bytes"); + json!({"ok":true,"protocol":2,"session_id":session_id(),"version":env!("OCS_APP_VERSION"), "mode":if self.main_window.is_some(){"gui"}else{"headless"},"enabled":self.control.enabled, "document_id":tab.id,"revision":tab.edit_revision,"geometry_revision":tab.scene.geometry_epoch,"camera_revision":tab.scene.camera_generation, "plugins":plugin_ids(), @@ -361,11 +395,23 @@ impl OpenCADStudio { "mtext_editor":self.mtext_editor.as_ref().map(|e|json!({"text":e.content.text(),"height":e.height,"style":e.style})), "text_editor":self.text_inline.is_some(),"event_cursor":self.control.serial, "operation":self.control.pending.as_ref().map(|p| &p.id), - "capabilities":["commands","command_manifest","step_input","batch","compact_results","entity_pick","structure_pick","selection","properties","records","record_schemas","record_filters","atomic_record_updates","layers","history","documents","events","capture","viewport_capture","measure","spatial_query"] + "capabilities":capabilities }) } pub(super) fn control_request(&mut self, req: Value) -> (Value, Task) { + // First control contact marks the session automated (#1349): a + // DonationPrompt that is still queued (not yet shown) is dropped so + // automation never waits behind it, and `queue_startup_prompts` + // stops queueing one. A prompt already on screen stays — a human + // may be reading it; clients close it with `close_modal`. + if !self.control.automation_seen && self.control.enabled { + self.control.automation_seen = true; + if self.active_modal != Some(super::ModalKind::DonationPrompt) { + self.pending_startup_modals + .retain(|kind| kind != &super::ModalKind::DonationPrompt); + } + } let op = req["op"].as_str().unwrap_or(""); let query = matches!( op, @@ -391,9 +437,12 @@ impl OpenCADStudio { Task::none(), ); } - if req["protocol"].as_u64().is_some_and(|v| v != 1) { + if req["protocol"] + .as_u64() + .is_some_and(|v| !(1..=2).contains(&v)) + { return ( - failure("protocol_mismatch", "Protocol 1 required"), + failure("protocol_mismatch", "Protocol 1 or 2 required"), Task::none(), ); } @@ -684,9 +733,46 @@ impl OpenCADStudio { let i = self.active_tab; Ok(match req["op"].as_str().unwrap_or("") { "new" => self.update(Message::TabNew), - "open" => self.update(Message::OpenExternal(std::path::PathBuf::from(string( - req, "path", - )?))), + "open" => { + // Web automation opens in-memory drawings (#1349): decode the + // base64 payload and route the bytes through the guarded open + // queue drag-and-drop uses, so no OPFS copy or file picker is + // involved. + #[cfg(target_arch = "wasm32")] + if req["data_base64"].as_str().is_some() { + let name = string(req, "name")?.to_owned(); + let encoded = string(req, "data_base64")?; + let bytes = base64::Engine::decode( + &base64::engine::general_purpose::STANDARD, + encoded, + ) + .map_err(|_| failure("invalid_data", "data_base64 is not valid base64"))?; + if !crate::io::web_drop::enqueue_open(name.clone(), bytes) { + return Err(failure("busy", "Web open queue full")); + } + // The queue is normally drained by the control poll; drain + // it now through the same guarded path so this operation + // stays pending until the open settles instead of + // completing before the drawing exists. + let task = self.update(Message::PollWebControl); + if self.opening.as_ref().is_none_or(|opening| opening.name != name) { + // Another open holds the slot; the poll opens this + // entry once it frees, after this operation settles. + self.set_control_result(json!({"queued": true, "name": name})); + } + return Ok(task); + } + #[cfg(not(target_arch = "wasm32"))] + if req["data_base64"].as_str().is_some() { + return Err(failure( + "invalid_request", + "data_base64 open is web-only; supply a native path", + )); + } + self.update(Message::OpenExternal(std::path::PathBuf::from(string( + req, "path", + )?))) + } "activate" => { let i = self .tabs diff --git a/src/app/mod.rs b/src/app/mod.rs index 76c3d8252..e1f0883ff 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2411,6 +2411,11 @@ pub enum Message { /// `None` = cancelled. #[cfg(target_arch = "wasm32")] CenterlineLinetypeFilePicked(Option<(String, String)>), + /// Web DATALINK: the picked .csv file's name and raw bytes — registered + /// in the picked-source registry, then fed to the same table-insert path + /// the native typed-path command uses. `None` = cancelled. + #[cfg(target_arch = "wasm32")] + DataLinkCsvPicked(Option<(String, Vec)>), /// Append one typed character to the command-line input from the /// global key-press subscription. Used when the text-input widget /// itself isn't focused (focus parked on viewport / button / etc.) @@ -4403,6 +4408,16 @@ impl OpenCADStudio { fn boot_web() -> (Self, Task) { #[cfg_attr(target_arch = "wasm32", allow(unused_mut))] let mut s = Self::new(); + // A web host that intends to automate can opt in before the first + // control request by setting `opencadstudio.automation` to "1" in + // localStorage (the same pre-load mechanism the settings use): the + // startup donation prompt then never queues (#1349). + if web_sys::window().and_then(|window| window.local_storage().ok().flatten()).and_then( + |storage| storage.get_item("opencadstudio.automation").ok().flatten(), + ) == Some("1".to_string()) + { + s.control.automation_seen = true; + } s.queue_startup_prompts(); let focus = s.focus_cmd_input(); let primary_font = diff --git a/src/app/record_api.rs b/src/app/record_api.rs index 843c04802..a87fcf47c 100644 --- a/src/app/record_api.rs +++ b/src/app/record_api.rs @@ -1104,6 +1104,37 @@ fn requested_handle(request: &Value) -> Result { .ok_or_else(|| "set_properties requires a hexadecimal handle".to_string()) } +/// Compare-and-set equality for `set_properties` (#1348): serde_json keeps +/// `10` and `10.0` as distinct numbers, and a JavaScript client cannot even +/// send `10.0` (`JSON.stringify(10.0) === "10"`), so plain `Value` equality +/// never matches a whole-number float. Numbers compare by value; strings, +/// bools and null stay exact; objects and arrays recurse. +fn json_values_match(stored: &Value, expected: &Value) -> bool { + match (stored, expected) { + (Value::Number(stored), Value::Number(expected)) => match (stored.as_f64(), expected.as_f64()) { + (Some(stored), Some(expected)) => stored == expected, + // Out-of-range or arbitrary-precision numbers keep exact equality. + _ => stored == expected, + }, + (Value::Object(stored), Value::Object(expected)) => { + stored.len() == expected.len() + && stored.iter().all(|(key, value)| { + expected + .get(key) + .is_some_and(|expected| json_values_match(value, expected)) + }) + } + (Value::Array(stored), Value::Array(expected)) => { + stored.len() == expected.len() + && stored + .iter() + .zip(expected) + .all(|(stored, expected)| json_values_match(stored, expected)) + } + _ => stored == expected, + } +} + fn apply_updates( properties: &mut Value, request: &Value, @@ -1136,7 +1167,7 @@ fn apply_updates( .pointer_mut(path) .ok_or_else(|| format!("property does not exist: {path}"))?; if let Some(expected) = update.get("expected") { - if target != expected { + if !json_values_match(target, expected) { return Err(format!("property changed before update: {path}")); } } @@ -1872,4 +1903,68 @@ mod tests { ); assert_eq!(compare["code"], "invalid_update", "{compare}"); } + + #[test] + fn set_properties_expected_matches_whole_number_floats_by_value() { + // Both directions of the #1348 mismatch: serde_json keeps 10 and 10.0 + // as different numbers, and a JS client can only send the integer. + assert!(super::json_values_match(&json!(10.0), &json!(10))); + assert!(super::json_values_match(&json!(10), &json!(10.0))); + // A stale value must still be rejected: value comparison, not a + // number-type blind spot. + assert!(!super::json_values_match(&json!(10.0), &json!(10.5))); + assert!(!super::json_values_match(&json!(10), &json!(10.5))); + // Non-numerics keep exact structural equality. + assert!(!super::json_values_match(&json!(10), &json!("10"))); + assert!(!super::json_values_match(&json!(10), &json!(true))); + assert!(!super::json_values_match(&json!(null), &json!(10))); + // Nested objects and arrays recurse by key/index. + assert!(super::json_values_match( + &json!({"x":12.0,"y":34.0,"z":[5.0]}), + &json!({"x":12,"y":34,"z":[5]}), + )); + assert!(!super::json_values_match( + &json!({"x":12.0,"y":34.0}), + &json!({"x":12,"y":34.5}), + )); + assert!(!super::json_values_match( + &json!({"x":12.0,"y":34.0}), + &json!({"x":12.0}), + )); + + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + let i = app.active_tab; + let entity = app.tabs[i] + .scene + .add_entity(EntityType::Insert(Insert::new("A", Vector3::new(12.0, 34.0, 5.0)))); + let handle = format!("{:X}", entity.value()); + + // A scalar whole-number float matches an integer expected value. + let moved = execute( + &mut app, + json!({"op":"set_properties","collection":"entities","handle":handle, + "updates":[{"path":"/insert_point/x","expected":12,"value":20.0}]}), + "scalar", + ); + assert_eq!(moved["status"], "completed", "{moved}"); + + // A nested expected object matches the stored whole-number floats. + let nested = execute( + &mut app, + json!({"op":"set_properties","collection":"entities","handle":handle, + "updates":[{"path":"/insert_point","expected":{"x":20,"y":34,"z":5},"value":{"x":20,"y":34,"z":6}}]}), + "nested", + ); + assert_eq!(nested["status"], "completed", "{nested}"); + + // A genuinely stale nested value is still rejected. + let stale = execute( + &mut app, + json!({"op":"set_properties","collection":"entities","handle":handle, + "updates":[{"path":"/insert_point","expected":{"x":20,"y":34,"z":5},"value":{"x":0,"y":0,"z":0}}]}), + "stale", + ); + assert_eq!(stale["code"], "invalid_update", "{stale}"); + } } diff --git a/src/app/startup.rs b/src/app/startup.rs index d1e334f4a..5f1658dab 100644 --- a/src/app/startup.rs +++ b/src/app/startup.rs @@ -8,7 +8,13 @@ impl OpenCADStudio { self.pending_startup_modals .push_back(ModalKind::AssocPrompt); } - if self.donation_prompt_version != env!("OCS_APP_VERSION") { + // Automation sessions (#1349): once control traffic has arrived (or a + // web host opted in at boot) while control is enabled, the donation + // prompt is suppressed — a modal would block every automated + // operation until a client closed it by hand. + if self.donation_prompt_version != env!("OCS_APP_VERSION") + && !(self.control.enabled && self.control.automation_seen) + { self.pending_startup_modals .push_back(ModalKind::DonationPrompt); } @@ -226,6 +232,39 @@ mod tests { assert!(restarted.active_modal.is_none()); } + #[test] + fn automation_sessions_skip_the_donation_prompt() { + // First control contact drops a still-queued donation prompt and + // stops later queueing while control stays enabled (#1349). + let mut app = OpenCADStudio::new_for_test(); + app.default_assoc_prompted = true; + app.queue_startup_prompts(); + assert_eq!(app.active_modal, Some(ModalKind::DonationPrompt)); + let _ = app.update(Message::CloseModal); + app.donation_prompt_version = "previous-release".to_string(); + app.pending_startup_modals.push_back(ModalKind::DonationPrompt); + let _ = app.control_request(serde_json::json!({"op":"state"})); + assert!(app.pending_startup_modals.is_empty()); + app.queue_startup_prompts(); + assert_ne!(app.active_modal, Some(ModalKind::DonationPrompt)); + + // A still-queued (not yet shown) prompt is dropped on first contact. + let mut queued = OpenCADStudio::new_for_test(); + queued.default_assoc_prompted = true; + queued.donation_prompt_version = "previous-release".to_string(); + queued.pending_startup_modals.push_back(ModalKind::DonationPrompt); + let _ = queued.control_request(serde_json::json!({"op":"state"})); + queued.queue_startup_prompts(); + assert_ne!(queued.active_modal, Some(ModalKind::DonationPrompt)); + + // A prompt already on screen is left for its reader; clients close + // it themselves, and a session with no control traffic still prompts. + let mut human = OpenCADStudio::new_for_test(); + human.default_assoc_prompted = true; + human.queue_startup_prompts(); + assert_eq!(human.active_modal, Some(ModalKind::DonationPrompt)); + } + #[test] fn startup_dialogs_wait_their_turn_and_preserve_unseen_prompts() { let mut app = OpenCADStudio::new_for_test(); diff --git a/src/app/update/file.rs b/src/app/update/file.rs index e422ee380..57b4c9844 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -883,6 +883,14 @@ impl OpenCADStudio { pub(in crate::app) fn apply_config(&mut self, cfg: crate::app::config::AppConfig) { self.apply_settings(&cfg.settings); self.ui_theme = cfg.theme.clone(); + // A saved `System` scheme re-resolves at boot: the stored palette is + // whatever the last resolution produced, so refresh it alongside the + // live theme (COLORSCHEME SYSTEM documents this boot-time read). + if self.ui_theme.name.eq_ignore_ascii_case("System") { + self.ui_theme.palette = crate::app::config::UiThemePalette::from_iced( + crate::app::config::resolve_system_theme().seed(), + ); + } self.active_theme = self.ui_theme.to_iced(); self.theme_color_inputs = self.ui_theme.palette.hex_values(); self.model_space = cfg.model_space; diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index c3227c3aa..d3f699d06 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -2453,6 +2453,12 @@ impl OpenCADStudio { #[cfg(target_arch = "wasm32")] Message::ScriptFilePicked(None) => Task::none(), + // Web DATALINK: the picked .csv's bytes arrived — register them + // as the link's in-session source, then run the same table-insert + // path the native typed-path command feeds. + #[cfg(target_arch = "wasm32")] + Message::DataLinkCsvPicked(picked) => self.on_datalink_csv_picked(picked), + // Web CENTERLTYPEFILE: the picked .lin/.dnl file's text arrived — // load its linetypes, then record the setting under the file's // name (the native command reads the typed path inside diff --git a/src/io/mod.rs b/src/io/mod.rs index 679cd711f..8b97a4071 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -264,6 +264,27 @@ pub async fn pick_linetype_file_web() -> Option<(String, String)> { Some((name, String::from_utf8_lossy(&bytes).into_owned())) } +/// Web DATALINK: pick the .csv through the browser and read its raw bytes — +/// the stand-in for the typed path the native command reads with +/// `std::fs::read_to_string`. The caller registers the bytes in the +/// picked-source registry so the link re-reads in-session. `None` = the user +/// cancelled. +#[cfg(target_arch = "wasm32")] +pub async fn pick_datalink_csv_web() -> Option<(String, Vec)> { + let Some(handle) = crate::sys::file_dialog() + .set_title(crate::t!("Link a CSV data source").as_ref()) + .add_filter(crate::t!("CSV Files").as_ref(), &["csv", "txt", "CSV", "TXT"]) + .add_filter(crate::t!("All Files").as_ref(), &["*"]) + .pick_file() + .await + else { + return None; + }; + let name = handle.file_name(); + let bytes = handle.read().await; + Some((name, bytes)) +} + /// Load a CAD file from a known path. Parsing and cache building run on a /// dedicated OS thread so the async executor stays free for rendering during /// the load. Writes phase markers into `phase` so the UI can show diff --git a/src/io/pdf_export.rs b/src/io/pdf_export.rs index 235126071..90607f297 100644 --- a/src/io/pdf_export.rs +++ b/src/io/pdf_export.rs @@ -720,34 +720,47 @@ fn append_pdf_page( Text(&'a PlotWire), } + // Stacking contract (#1345): the entity's draw-order depth — the same + // per-entity label the editor feeds its clip-z bias — decides the + // z order, so a hatch DRAWORDER-to-back/front interleaves with wires + // exactly like the viewport. The kind rank only breaks *exact* depth + // ties, and mirrors the editor's own pass order there: hatch fills → + // raster images → mesh/wire fills → wire strokes → text → wipeout + // masks last (the editor's `wipeout.render_pass` runs after wires to + // mask them). Never pin a whole kind between others regardless of + // depth. let mut draw_items = Vec::with_capacity(wires.len() * 2 + hatches.len() + wipeouts.len() + images.len()); let mut sequence = 0usize; - for wire in wires { - if !wire.fill_tris.is_empty() { - draw_items.push((wire.draw_depth, 0u8, sequence, DrawItem::WireFill(wire))); - sequence += 1; - } - draw_items.push((wire.draw_depth, 2u8, sequence, DrawItem::Wire(wire))); - sequence += 1; - if !wire.text_verts.is_empty() { - draw_items.push((wire.draw_depth, 3u8, sequence, DrawItem::Text(wire))); - sequence += 1; - } - } - for hatch in wipeouts.iter().chain(hatches.iter()) { + for hatch in hatches { draw_items.push((hatch.draw_depth, 1u8, sequence, DrawItem::Hatch(hatch))); sequence += 1; } for image in images { draw_items.push(( image.image.draw_depth, - 1u8, + 2u8, sequence, DrawItem::Image(image), )); sequence += 1; } + for wire in wires { + if !wire.fill_tris.is_empty() { + draw_items.push((wire.draw_depth, 3u8, sequence, DrawItem::WireFill(wire))); + sequence += 1; + } + draw_items.push((wire.draw_depth, 4u8, sequence, DrawItem::Wire(wire))); + sequence += 1; + if !wire.text_verts.is_empty() { + draw_items.push((wire.draw_depth, 5u8, sequence, DrawItem::Text(wire))); + sequence += 1; + } + } + for wipeout in wipeouts { + draw_items.push((wipeout.draw_depth, 6u8, sequence, DrawItem::Hatch(wipeout))); + sequence += 1; + } draw_items.sort_by(|a, b| { a.0.total_cmp(&b.0) .then_with(|| a.1.cmp(&b.1)) @@ -1259,6 +1272,8 @@ fn emit_wire_fills( line_weight_px: wire.line_weight_px, angle_offset: 0.0, scale: 1.0 / scale.max(1.0e-6), + // Ordering was decided by the DrawItem sort outside; this + // model only carries pattern/style data for emit_hatch. draw_depth: wire.depth_override.unwrap_or(0.0), }; emit_hatch( @@ -2335,6 +2350,265 @@ mod tests { ); } + // ── draw-order stacking tests (#1345) ─────────────────────────────── + // The PDF z order must follow the per-entity draw-order depth exactly like + // the editor viewport (SORTENTS / DRAWORDER): kind grouping may only break + // exact depth ties, in the editor's own pass order. Each test asserts the + // SEQUENCE of drawing operators inside the decoded page content stream. + + fn solid_hatch(name: &str, depth: f32, color: [f32; 4]) -> HatchModel { + HatchModel { + pattern_origin: None, + render_instance: None, + boundary: std::sync::Arc::new(vec![ + [10.0, 10.0], + [40.0, 10.0], + [40.0, 40.0], + [10.0, 40.0], + ]), + boundary_wcs: None, + world_origin: [0.0, 0.0], + fill_plane: None, + fill_plane_boundary: None, + boundary_exterior: None, + boundary_sources: None, + boundary_paths: None, + style: acadrust::entities::HatchStyleType::Normal, + pattern: crate::scene::model::hatch_model::HatchPattern::Solid, + name: name.into(), + color, + aci: 0, + line_weight_px: 1.0, + angle_offset: 0.0, + scale: 1.0, + draw_depth: depth, + } + } + + fn line_wire(name: &str, depth: f32, color: [f32; 4]) -> PlotWire { + PlotWire { + wire: WireModel::solid( + name.into(), + vec![[5.0, 5.0, 0.0], [45.0, 45.0, 0.0]], + color, + false, + ), + draw_depth: depth, + } + } + + /// Decode the page's drawing operators out of the page content stream. + /// printpdf zlib-compresses streams, but very short ones can be stored + /// raw — so each `stream…endstream` body is inflated when possible and + /// identified by the white page-background op every plotted page starts + /// with (`1 1 1 rg`). + fn page_content_stream(bytes: &[u8]) -> Vec { + use std::io::Read; + let mut search = 0; + while let Some(rel) = bytes[search..].windows(6).position(|w| w == b"stream") { + let start = search + rel; + let mut body = start + 6; + while body < bytes.len() && (bytes[body] == b'\r' || bytes[body] == b'\n') { + body += 1; + } + let end = bytes[body..] + .windows(9) + .position(|w| w == b"endstream") + .map(|i| body + i) + .unwrap_or(bytes.len()); + let raw = &bytes[body..end]; + let mut inflated = Vec::new(); + let decoded = flate2::read::ZlibDecoder::new(raw) + .read_to_end(&mut inflated) + .ok() + .filter(|_| !inflated.is_empty()) + .map(|_| inflated) + .unwrap_or_else(|| raw.to_vec()); + if decoded.windows(8).any(|w| w == b"1 1 1 rg") { + return decoded; + } + search = end; + } + panic!("no page content stream found in the PDF"); + } + + /// Byte offset of the paint operator for a fill of `color` (e.g. + /// `1 0 0 rg` … `f*`), searched from `from`. + fn fill_op_pos(stream: &[u8], color: &str, from: usize) -> usize { + let token = color.as_bytes(); + let start = stream[from..] + .windows(token.len()) + .position(|w| w == token) + .map(|i| from + i) + .unwrap_or_else(|| panic!("no {color:?} fill color op in stream")); + stream[start..] + .windows(2) + .position(|w| w == b"f*") + .map(|i| start + i) + .unwrap_or_else(|| panic!("fill color {color:?} has no f* paint op")) + } + + /// Byte offset of the stroke operator for a stroke color (e.g. + /// `1 0 0 RG` … `S`), searched from `from`. + fn stroke_op_pos(stream: &[u8], color: &str, from: usize) -> usize { + let token = color.as_bytes(); + let start = stream[from..] + .windows(token.len()) + .position(|w| w == token) + .map(|i| from + i) + .unwrap_or_else(|| panic!("no {color:?} stroke color op in stream")); + stream[start..] + .windows(3) + .position(|w| w == b"\nS\n") + .map(|i| start + i) + .unwrap_or_else(|| panic!("stroke color {color:?} has no S paint op")) + } + + fn render_stream(page: PdfPageInput) -> Vec { + let bytes = render_pdf_bytes(&[page], None).expect("page renders"); + assert!(bytes.starts_with(b"%PDF")); + page_content_stream(&bytes) + } + + const RED: [f32; 4] = [1.0, 0.0, 0.0, 1.0]; + const GREEN: [f32; 4] = [0.0, 1.0, 0.0, 1.0]; + const BLUE: [f32; 4] = [0.0, 0.0, 1.0, 1.0]; + const BLACK: [f32; 4] = [0.0, 0.0, 0.0, 1.0]; + + #[test] + fn hatch_draworder_back_plots_under_lines() { + let mut page = test_page(vec![ + line_wire("70001", 0.10, BLUE), + line_wire("70002", 0.20, GREEN), + ]); + page.content.hatches = vec![solid_hatch("70003", -0.50, RED)]; + let stream = render_stream(page); + let hatch = fill_op_pos(&stream, "1 0 0 rg", 0); + let blue = stroke_op_pos(&stream, "0 0 1 RG", 0); + let green = stroke_op_pos(&stream, "0 1 0 RG", 0); + assert!( + hatch < blue && hatch < green, + "DRAWORDER-to-back hatch must paint before the lines: {hatch} !< {blue}/{green}" + ); + assert!(blue < green, "wire depths 0.10 < 0.20 must stack in order"); + } + + #[test] + fn hatch_draworder_front_plots_over_lines() { + let mut page = test_page(vec![ + line_wire("70001", 0.10, BLUE), + line_wire("70002", 0.20, GREEN), + ]); + page.content.hatches = vec![solid_hatch("70003", 0.50, RED)]; + let stream = render_stream(page); + let hatch = fill_op_pos(&stream, "1 0 0 rg", 0); + let blue = stroke_op_pos(&stream, "0 0 1 RG", 0); + let green = stroke_op_pos(&stream, "0 1 0 RG", 0); + assert!( + blue < hatch && green < hatch, + "DRAWORDER-to-front hatch must paint after the lines: {blue}/{green} !< {hatch}" + ); + } + + #[test] + fn overlapping_hatches_stack_by_draw_order() { + // Red above blue… + let mut page = test_page(Vec::new()); + page.content.hatches = vec![ + solid_hatch("70010", 0.40, RED), + solid_hatch("70011", -0.40, BLUE), + ]; + let stream = render_stream(page); + let blue = fill_op_pos(&stream, "0 0 1 rg", 0); + let red = fill_op_pos(&stream, "1 0 0 rg", 0); + assert!(blue < red, "deeper hatch paints first: {blue} !< {red}"); + // …and the mirrored DRAWORDER flips them. + let mut page = test_page(Vec::new()); + page.content.hatches = vec![ + solid_hatch("70010", -0.40, RED), + solid_hatch("70011", 0.40, BLUE), + ]; + let stream = render_stream(page); + let blue = fill_op_pos(&stream, "0 0 1 rg", 0); + let red = fill_op_pos(&stream, "1 0 0 rg", 0); + assert!( + red < blue, + "mirrored draw order must flip the paint order: {red} !< {blue}" + ); + } + + #[test] + fn stacking_interleaves_hatches_and_wires_by_depth() { + // Hatch below the wire, hatch above the wire — the wire must sit + // BETWEEN the two fills. Kind pinning (all hatches under or over all + // wires) is exactly the bug: entity depth decides. + let mut page = test_page(vec![line_wire("70020", 0.00, BLACK)]); + page.content.hatches = vec![ + solid_hatch("70021", 0.30, GREEN), + solid_hatch("70022", -0.30, BLUE), + ]; + let stream = render_stream(page); + let blue = fill_op_pos(&stream, "0 0 1 rg", 0); + let black = stroke_op_pos(&stream, "0 0 0 RG", 0); + let green = fill_op_pos(&stream, "0 1 0 rg", 0); + assert!( + blue < black && black < green, + "wire must interleave: blue hatch {blue} < line {black} < green hatch {green}" + ); + } + + #[test] + fn equal_depth_keeps_linework_over_hatch_fill() { + // Exact tie → the editor's pass order: hatch fill first, wire stroke + // after, so linework stays visible over an equal-depth fill. + let mut page = test_page(vec![line_wire("70030", 0.0, BLACK)]); + page.content.hatches = vec![solid_hatch("70031", 0.0, RED)]; + let stream = render_stream(page); + let hatch = fill_op_pos(&stream, "1 0 0 rg", 0); + let black = stroke_op_pos(&stream, "0 0 0 RG", 0); + assert!( + hatch < black, + "tied wire must paint after the hatch: {hatch} !< {black}" + ); + } + + #[test] + fn wipeout_masks_tied_wires_but_honors_depth() { + let wipeout = |depth: f32| solid_hatch("WIPEOUT_FILL", depth, [1.0; 4]); + // Tie: wipeouts paint after wires (the editor's wipeout pass runs + // last so the mask covers tied linework). + let mut page = test_page(vec![line_wire("70040", 0.0, BLACK)]); + page.content.wipeouts = vec![wipeout(0.0)]; + let stream = render_stream(page); + // Wipeout fills white — take the LAST white fill (the page background + // rectangle is white too, but it paints first). + let mut wipe = None; + let mut search = 0; + while let Some(rel) = stream[search..].windows(8).position(|w| w == b"1 1 1 rg") { + let color_at = search + rel; + let paint = fill_op_pos(&stream, "1 1 1 rg", color_at); + wipe = Some(paint); + search = paint + 1; + } + let last_wipe = wipe.expect("a white wipeout fill reaches the stream"); + let black = stroke_op_pos(&stream, "0 0 0 RG", 0); + assert!( + black < last_wipe, + "tied wipeout must mask the wire: {black} !< {last_wipe}" + ); + // Below the wire (DRAWORDER under): depth wins and the wire paints + // back on top of the mask. + let mut page = test_page(vec![line_wire("70040", 0.0, BLACK)]); + page.content.wipeouts = vec![wipeout(-0.5)]; + let stream = render_stream(page); + let wipe = fill_op_pos(&stream, "1 1 1 rg", 0); + let black = stroke_op_pos(&stream, "0 0 0 RG", 0); + assert!( + wipe < black, + "deeper wipeout must paint before the wire: {wipe} !< {black}" + ); + } + // Build a WireModel carrying the SDF glyph quads for `text` in the embedded // "txt" stroke font, laid out into the process-wide atlas emit_text reads. fn text_wire(text: &str, origin: [f64; 3]) -> PlotWire { diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 682da08de..bb082a2c1 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -645,6 +645,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Color Temperature" => Some(("properties", "color-temperature")), "Color override (#RRGGBB):" => Some(("styles", "color-override-rrggbb")), "Color scheme set to '{name}'." => Some(("view", "color-scheme-set-to-name")), + "Color scheme set to 'System' (currently {name})." => Some(("view", "color-scheme-set-to-system-currently-name")), "Color {}" => Some(("ui", "color")), "Column count" => Some(("properties", "column-count")), "Column gutter" => Some(("properties", "column-gutter")), diff --git a/src/scene/mod.rs b/src/scene/mod.rs index a0f631e9a..1ecbe5b62 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -11982,6 +11982,79 @@ mod journal_tests { assert!(!after_erase.contains_key(&h2.value())); } + // #1345: the plot path must re-read each hatch's draw-order label from + // the depth map — the cached hatch model bakes 0.0 at seed time, and a + // stale value made every exported hatch stack at depth 0 regardless of + // DRAWORDER (SORTENTS). + #[test] + fn plot_hatches_carry_current_draw_order_depth() { + use acadrust::entities::{BoundaryEdge, BoundaryPath, Hatch, Line, PolylineEdge}; + use acadrust::objects::{ObjectType, SortEntitiesTable}; + use acadrust::types::{Vector2, Vector3}; + + let mut hatch = Hatch::solid(); + let mut path = BoundaryPath::new(); + path.edges.push(BoundaryEdge::Polyline(PolylineEdge::new( + vec![ + Vector2::new(0.0, 0.0), + Vector2::new(10.0, 0.0), + Vector2::new(10.0, 10.0), + Vector2::new(0.0, 10.0), + ], + true, + ))); + hatch.add_path(path); + + let mut s = Scene::new(); + let line = s.add_entity(EntityType::Line(Line::from_points( + Vector3::new(0.0, 5.0, 0.0), + Vector3::new(10.0, 5.0, 0.0), + ))); + let hatch_handle = s.add_entity(EntityType::Hatch(hatch)); + + // Model-tab export path: hatches must carry their live depth label. + let plotted = s.paper_plot_hatches(); + assert_eq!(plotted.len(), 1, "the hatch must reach the plot list"); + let depth_map = s.draw_depth_map(); + let expected = depth_map[&hatch_handle.value()][0]; + assert_eq!( + plotted[0].draw_depth, expected, + "plot hatch depth must equal the live depth-map label" + ); + assert!( + plotted[0].draw_depth > depth_map[&line.value()][0], + "later entity (hatch) starts in front of the line" + ); + drop(depth_map); + + // DRAWORDER-to-back the hatch: sort key below every effective key, + // exactly like the command writes into the block's SortEntitiesTable. + let block = s.model_space_block_handle(); + let table_handle = s.document.allocate_handle(); + let mut table = SortEntitiesTable::for_block(block); + table.handle = table_handle; + table.add_entry(hatch_handle, Handle::new(1)); + s.document + .objects + .insert(table_handle, ObjectType::SortEntitiesTable(table)); + s.bump_entities(&[(hatch_handle, ChangeKind::Modified)]); + s.invalidate_draw_depth(); + + let depth_map = s.draw_depth_map(); + let back = depth_map[&hatch_handle.value()][0]; + let line_depth = depth_map[&line.value()][0]; + drop(depth_map); + assert!( + back < line_depth, + "sort table must move the hatch behind the line ({back} !< {line_depth})" + ); + let plotted = s.paper_plot_hatches(); + assert_eq!( + plotted[0].draw_depth, back, + "plot hatch depth must follow DRAWORDER (stale 0.0 = the #1345 bug)" + ); + } + #[test] fn category_gate_keeps_unrelated_caches_warm() { let mut s = Scene::new(); diff --git a/src/scene/model/image_model.rs b/src/scene/model/image_model.rs index 2e6ab9602..1a267826d 100644 --- a/src/scene/model/image_model.rs +++ b/src/scene/model/image_model.rs @@ -664,19 +664,27 @@ fn native_footprint(format: Option, color: Option 16, _ => 4, }); - // A 16-bit frame and its RGBA conversion can coexist; an 8-bit one is - // converted with at most a small row buffer in flight. - let peak_bpp = if native_bpp > 4 { native_bpp + 4 } else { 4 }; - let codec_factor = match format { - Some(image::ImageFormat::Tiff) => 3, - _ => 1, + // `DynamicImage::into_rgba8` only MOVES for Rgba8; every other layout + // allocates the RGBA destination while the native buffer is still + // alive, so both coexist (image 0.25 buffer.rs cast_in_color_space). + let peak_bpp = if native_bpp == 4 { 4 } else { native_bpp + 4 }; + // GIF decodes the frame into its own buffer while the screen-sized + // output buffer is live — two RGBA-sized allocations coexist. + let peak_bpp = match format { + Some(image::ImageFormat::Gif) => peak_bpp.max(8), + _ => peak_bpp, }; + // tiff 0.11 splits the given budget between the frame buffer and the + // chunk scratch (a TOTAL cap), so no multiplier is needed: the honest + // peak is the coexisting native+RGBA pair, already in peak_bpp. + let codec_factor = 1; NativeFootprint { peak_bpp, codec_factor } } /// Budget decision for probed pixel dimensions. Pure and cfg-independent /// when the budgets are passed explicitly, so tests can probe arbitrary /// budgets; the decode path always passes this target's constants. +#[cfg_attr(not(test), allow(dead_code))] fn tier_for( width: u32, height: u32, @@ -807,6 +815,10 @@ fn decode_with_budgets( // 0.25); the tier already folded that factor in, and the allowance // matches it so an in-tier TIFF never codec-refuses. let mut limits = image::Limits::default(); + // tiff 0.11 SPLITS this cap between the frame buffer and chunk scratch + // (a total ceiling), so the plain budget is the right allowance; the + // ×3 multiplier that image's older tiff codec needed would instead + // admit hostile chunk transients up to 3× the budget. limits.max_alloc = Some(match reader.format() { Some(image::ImageFormat::Gif) => probed .map(|(width, height, _, _)| { @@ -814,7 +826,6 @@ fn decode_with_budgets( }) .unwrap_or(max_decode_bytes) .min(max_decode_bytes), - Some(image::ImageFormat::Tiff) => max_decode_bytes.saturating_mul(3), _ => max_decode_bytes, }); reader.limits(limits); diff --git a/src/scene/paper.rs b/src/scene/paper.rs index 43f21a996..2ca274a1e 100644 --- a/src/scene/paper.rs +++ b/src/scene/paper.rs @@ -562,6 +562,12 @@ impl Scene { .unwrap_or(true) }; let mut models = Vec::new(); + // Cached hatch models bake `draw_depth: 0.0` at document load; the + // canvas path re-reads the label per frame but this plot path used to + // clone the stale value, so every top-level hatch exported at depth 0 + // and DRAWORDER (SORTENTS) never reached the PDF (#1345). Refresh the + // per-entity label here exactly like `hatch_models_for_view`. + let depths = self.draw_depth_map(); for (&handle, model) in self.hatches.iter() { let Some(source) = self.document.get_entity(handle) else { continue; @@ -609,6 +615,7 @@ impl Scene { hatch.color = style.0; hatch.aci = style.4; hatch.line_weight_px = style.3; + hatch.draw_depth = depths.get(&handle.value()).map_or(0.0, |d| d[0]); if let EntityType::Hatch(dxf) = entity { if let model::hatch_model::HatchPattern::Pattern(_) = &hatch.pattern { if dxf.pattern.lines.is_empty() { diff --git a/web-app.html b/web-app.html index eee071025..612262827 100644 --- a/web-app.html +++ b/web-app.html @@ -44,6 +44,7 @@ +
@@ -51,6 +52,7 @@
Open CAD Studio
Loading…
+