feat: hatch PDF draw order (#1345), numeric CAS (#1348), web automation (#1349), DATALINK table, System theme; honest decode accounting

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).
This commit is contained in:
Afonsop committed 2026-09-18 13:21:18 +01:00
1 parent 84b093748f
commit 2c38de53e2
41 files changed
+1026 -92

No files matched your search

+3
View File
@@ -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",
+42
View File
@@ -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).
+2 -1
View File
@@ -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 = يجري فتح صفحة الملاحظات...
+2 -1
View File
@@ -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 = Отваряне на страницата за обратна връзка...
+2 -1
View File
@@ -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...
+2 -1
View File
@@ -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...
+2 -1
View File
@@ -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 = Άνοιγμα της σελίδας σχολίων...
+2 -1
View File
@@ -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...
+2 -1
View File
@@ -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...
+2 -1
View File
@@ -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...
+2 -1
View File
@@ -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...
+2 -1
View File
@@ -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 = प्रतिक्रिया पृष्ठ खोलना
+2 -1
View File
@@ -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...
+2 -1
View File
@@ -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...
+2 -1
View File
@@ -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 = フィードバックページを開いています...
+2 -1
View File
@@ -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 = 피드백 페이지를 여는 중...
+2 -1
View File
@@ -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...
+2 -1
View File
@@ -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...
+2 -1
View File
@@ -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...
+2 -1
View File
@@ -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 = Открываем страницу обратной связи...
+2 -1
View File
@@ -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...
+2 -1
View File
@@ -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 = 打开反馈页面...
+2 -1
View File
@@ -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 = 開啟反饋頁面...
+28
View File
@@ -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<u8>)>,
) -> Task<Message> {
let Some((name, bytes)) = picked else {
return Task::none();
};
let text = String::from_utf8_lossy(&bytes).into_owned();
if !register_picked_source(&name, Arc::new(bytes)) {
if let Some(name) = take_last_collision().take() {
self.command_line.push_warning(
crate::tf!(
"\"{name}\" is already loaded 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<Message> {
Task::perform(
async {
+59 -38
View File
@@ -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(&current_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(&current_style) =>
{
Some(*handle)
}
_ => None,
}
});
let command =
crate::modules::annotate::data_link::DataLinkPlaceCommand::new(table, link_path);
self.command_line
.push_info(&crate::command::CadCommand::prompt(&command));
self.tabs[i].active_cmd = Some(Box::new(command));
}
}
// Scan LandXML text for <CgPoint> survey points. Each element's text content is
+28 -3
View File
@@ -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" {
+84
View File
@@ -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<u16> = "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"
.encode_utf16()
.chain(std::iter::once(0))
.collect();
let value: Vec<u16> = "AppsUseLightTheme"
.encode_utf16()
.chain(std::iter::once(0))
.collect();
let mut data: u32 = 0;
let mut size = std::mem::size_of::<u32>() as u32;
// ERROR_SUCCESS (0) with data 1 means the apps theme is Light; a
// missing value (older Windows, wiped personalization) is Dark.
let light = unsafe {
RegGetValueW(
HKEY_CURRENT_USER,
sub_key.as_ptr(),
value.as_ptr(),
RRF_RT_REG_DWORD,
std::ptr::null_mut(),
&mut data as *mut u32 as *mut core::ffi::c_void,
&mut size,
) == 0
&& data == 1
};
if light {
iced::Theme::Light
} else {
iced::Theme::Dark
}
}
#[cfg(not(any(target_arch = "wasm32", target_os = "windows")))]
{
iced::Theme::Dark
}
}
#[derive(Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct UiThemePalette {
@@ -503,6 +568,25 @@ fn config_path() -> Option<std::path::PathBuf> {
mod tests {
use super::*;
#[test]
fn system_scheme_resolves_to_a_concrete_theme_and_keeps_its_name() {
// Fresh configs keep the existing default; SYSTEM is opt-in.
assert_ne!(UiThemeConfig::default().name, "System");
let mut cfg = UiThemeConfig::default();
cfg.name = "System".to_string();
let resolved = cfg.to_iced();
// Case-insensitive like the other stored names.
cfg.name = "system".to_string();
assert_eq!(cfg.to_iced(), resolved);
assert!(
matches!(resolved, iced::Theme::Light | iced::Theme::Dark),
"SYSTEM must resolve through the platform preference"
);
// The stored name survives resolution; re-runs re-resolve.
assert_eq!(cfg.name, "system");
assert_eq!(resolve_system_theme(), resolved);
}
#[test]
fn test_model_space_defaults() {
let cfg = ModelSpaceThemeConfig::default();
+93 -7
View File
@@ -106,6 +106,10 @@ pub(super) struct State {
completed: VecDeque<(String, Value, Value)>,
owner: Option<(u64, String)>,
pub enabled: bool,
/// Set by the first control request of the session (or by a web host's
/// boot-time opt-in): while control is enabled, startup prompts that
/// would block automation — the donation prompt — are suppressed (#1349).
pub automation_seen: bool,
serial: u64,
events: VecDeque<Value>,
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<Message>) {
// First control contact marks the session automated (#1349): a
// DonationPrompt that is still queued (not yet shown) is dropped so
// automation never waits behind it, and `queue_startup_prompts`
// stops queueing one. A prompt already on screen stays — a human
// may be reading it; clients close it with `close_modal`.
if !self.control.automation_seen && self.control.enabled {
self.control.automation_seen = true;
if self.active_modal != Some(super::ModalKind::DonationPrompt) {
self.pending_startup_modals
.retain(|kind| kind != &super::ModalKind::DonationPrompt);
}
}
let op = req["op"].as_str().unwrap_or("");
let query = matches!(
op,
@@ -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
+15
View File
@@ -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<u8>)>),
/// Append one typed character to the command-line input from the
/// global key-press subscription. Used when the text-input widget
/// itself isn't focused (focus parked on viewport / button / etc.)
@@ -4403,6 +4408,16 @@ impl OpenCADStudio {
fn boot_web() -> (Self, Task<Message>) {
#[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
let mut s = Self::new();
// A web host that intends to automate can opt in before the first
// control request by setting `opencadstudio.automation` to "1" in
// localStorage (the same pre-load mechanism the settings use): the
// startup donation prompt then never queues (#1349).
if web_sys::window().and_then(|window| window.local_storage().ok().flatten()).and_then(
|storage| storage.get_item("opencadstudio.automation").ok().flatten(),
) == Some("1".to_string())
{
s.control.automation_seen = true;
}
s.queue_startup_prompts();
let focus = s.focus_cmd_input();
let primary_font =
+96 -1
View File
@@ -1104,6 +1104,37 @@ fn requested_handle(request: &Value) -> Result<acadrust::Handle, String> {
.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}");
}
}
+40 -1
View File
@@ -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();
+8
View File
@@ -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;
+6
View File
@@ -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
+21
View File
@@ -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<u8>)> {
let Some(handle) = crate::sys::file_dialog()
.set_title(crate::t!("Link a CSV data source").as_ref())
.add_filter(crate::t!("CSV Files").as_ref(), &["csv", "txt", "CSV", "TXT"])
.add_filter(crate::t!("All Files").as_ref(), &["*"])
.pick_file()
.await
else {
return None;
};
let name = handle.file_name();
let bytes = handle.read().await;
Some((name, bytes))
}
/// Load a CAD file from a known path. Parsing and cache building run on a
/// dedicated OS thread so the async executor stays free for rendering during
/// the load. Writes phase markers into `phase` so the UI can show
+288 -14
View File
@@ -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<u8> {
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<u8> {
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 {
+1
View File
@@ -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")),
+73
View File
@@ -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();
+18 -7
View File
@@ -664,19 +664,27 @@ fn native_footprint(format: Option<image::ImageFormat>, color: Option<image::Col
image::ColorType::Rgba32F => 16,
_ => 4,
});
// A 16-bit frame and its RGBA conversion can coexist; an 8-bit one is
// converted with at most a small row buffer in flight.
let peak_bpp = if native_bpp > 4 { native_bpp + 4 } else { 4 };
let codec_factor = match format {
Some(image::ImageFormat::Tiff) => 3,
_ => 1,
// `DynamicImage::into_rgba8` only MOVES for Rgba8; every other layout
// allocates the RGBA destination while the native buffer is still
// alive, so both coexist (image 0.25 buffer.rs cast_in_color_space).
let peak_bpp = if native_bpp == 4 { 4 } else { native_bpp + 4 };
// GIF decodes the frame into its own buffer while the screen-sized
// output buffer is live — two RGBA-sized allocations coexist.
let peak_bpp = match format {
Some(image::ImageFormat::Gif) => peak_bpp.max(8),
_ => peak_bpp,
};
// tiff 0.11 splits the given budget between the frame buffer and the
// chunk scratch (a TOTAL cap), so no multiplier is needed: the honest
// peak is the coexisting native+RGBA pair, already in peak_bpp.
let codec_factor = 1;
NativeFootprint { peak_bpp, codec_factor }
}
/// Budget decision for probed pixel dimensions. Pure and cfg-independent
/// when the budgets are passed explicitly, so tests can probe arbitrary
/// budgets; the decode path always passes this target's constants.
#[cfg_attr(not(test), allow(dead_code))]
fn tier_for(
width: u32,
height: u32,
@@ -807,6 +815,10 @@ fn decode_with_budgets<R: std::io::BufRead + std::io::Seek>(
// 0.25); the tier already folded that factor in, and the allowance
// matches it so an in-tier TIFF never codec-refuses.
let mut limits = image::Limits::default();
// tiff 0.11 SPLITS this cap between the frame buffer and chunk scratch
// (a total ceiling), so the plain budget is the right allowance; the
// ×3 multiplier that image's older tiff codec needed would instead
// admit hostile chunk transients up to 3× the budget.
limits.max_alloc = Some(match reader.format() {
Some(image::ImageFormat::Gif) => probed
.map(|(width, height, _, _)| {
@@ -814,7 +826,6 @@ fn decode_with_budgets<R: std::io::BufRead + std::io::Seek>(
})
.unwrap_or(max_decode_bytes)
.min(max_decode_bytes),
Some(image::ImageFormat::Tiff) => max_decode_bytes.saturating_mul(3),
_ => max_decode_bytes,
});
reader.limits(limits);
+7
View File
@@ -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() {
+2
View File
@@ -44,6 +44,7 @@
<link data-trunk rel="copy-file" href="web/discussions.json" />
<link data-trunk rel="copy-file" href="web/videos.json" />
<link data-trunk rel="copy-file" href="web/locale-labels.json" />
<link data-trunk rel="copy-file" href="web/ocs-control.js" />
</head>
<body>
<div id="loading">
@@ -51,6 +52,7 @@
<div class="title">Open <span>CAD</span> Studio</div>
<div class="sub">Loading…</div>
</div>
<script src="ocs-control.js" defer></script>
<script>
(() => {
const splash = document.getElementById("loading");
+72
View File
@@ -0,0 +1,72 @@
// Web automation convenience API (#1349).
//
// The wasm module exposes the control bridge through `window.wasmBindings`
// (ocs_control_submit / ocs_control_take). This module wraps it in promises:
//
// await window.ocsControl.ready resolves once the bridge exists
// window.addEventListener("ocs-ready", …) fired once, at the same moment
// await window.ocsControl.request({...}) one control request -> final reply
//
// Hosts that intend to automate from the start should set
// localStorage["opencadstudio.automation"] = "1" before the page loads so the
// startup donation prompt never appears (see docs/automation/README.md).
(() => {
if (window.ocsControl) return;
const bridge = () => {
const api = window.wasmBindings;
return api && api.ocs_control_submit && api.ocs_control_take ? api : null;
};
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
let readyResolve;
const ready = new Promise(resolve => { readyResolve = resolve; });
const settle = () => {
readyResolve(window.ocsControl);
window.dispatchEvent(new Event("ocs-ready"));
};
const poll = setInterval(() => {
if (bridge()) {
clearInterval(poll);
settle();
}
}, 50);
if (bridge()) {
clearInterval(poll);
settle();
}
// Submit one request and poll take() until the app stores its reply.
async function exchange(api, payload, pollMs, deadline) {
const ticket = api.ocs_control_submit(JSON.stringify(payload));
for (;;) {
const raw = api.ocs_control_take(ticket);
if (raw) return JSON.parse(raw);
if (performance.now() >= deadline) {
throw new Error("ocs-control: timed out: " + JSON.stringify(payload));
}
await sleep(pollMs);
}
}
// One control request. Replies that are still `accepted`/`running` (an
// async open, capture or save) are followed through `op:"operation"` until
// a final status arrives. Resolves with the final reply object; rejects
// only on timeout or a missing bridge.
async function request(payload, options = {}) {
const pollMs = options.pollMs ?? 50;
const deadline = performance.now() + (options.timeoutMs ?? 120000);
const api = bridge();
if (!api) {
throw new Error("ocs-control: bridge not ready (await ocsControl.ready)");
}
let reply = await exchange(api, payload, pollMs, deadline);
while (["accepted", "running"].includes(reply.status) && reply.request_id) {
await sleep(pollMs);
reply = await exchange(
api, {op: "operation", request_id: reply.request_id}, pollMs, deadline);
}
return reply;
}
window.ocsControl = {ready, request};
})();