forked from 1b-agent/opencadstudio-upstream
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:
1 parent
84b093748f
commit
2c38de53e2
41 files changed
+1026
-92
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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).
|
||||
@@ -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 = يجري فتح صفحة الملاحظات...
|
||||
|
||||
@@ -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 = Отваряне на страницата за обратна връзка...
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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 = Άνοιγμα της σελίδας σχολίων...
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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 = प्रतिक्रिया पृष्ठ खोलना
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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 = フィードバックページを開いています...
|
||||
|
||||
@@ -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 = 피드백 페이지를 여는 중...
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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 = Открываем страницу обратной связи...
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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 = 打开反馈页面...
|
||||
|
||||
@@ -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 = 開啟反饋頁面...
|
||||
|
||||
@@ -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
@@ -1595,49 +1595,25 @@ impl OpenCADStudio {
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
// Web: nothing is behind a typed path — pick the .csv through
|
||||
// the browser, read its bytes at pick time, and register them
|
||||
// in the picked-source registry; the same table-insert path
|
||||
// then runs from `DataLinkCsvPicked`.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let _ = path;
|
||||
return Some(Task::perform(
|
||||
crate::io::pick_datalink_csv_web(),
|
||||
Message::DataLinkCsvPicked,
|
||||
));
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(text) => {
|
||||
let rows_data = parse_csv_table(&text);
|
||||
let nrows = rows_data.len();
|
||||
let ncols = rows_data.iter().map(|r| r.len()).max().unwrap_or(0);
|
||||
if nrows == 0 || ncols == 0 {
|
||||
self.command_line
|
||||
.push_error(crate::t!("DATALINK: the CSV file is empty.").as_ref());
|
||||
return Some(Task::none());
|
||||
}
|
||||
use acadrust::entities::TableBuilder;
|
||||
use acadrust::types::Vector3;
|
||||
let mut table = TableBuilder::new(nrows, ncols)
|
||||
.at(Vector3::new(0.0, 0.0, 0.0))
|
||||
.row_height(0.5)
|
||||
.column_width(2.0)
|
||||
.build();
|
||||
for (r, row) in rows_data.iter().enumerate() {
|
||||
for (c, cell) in row.iter().enumerate() {
|
||||
table.set_cell_text(r, c, cell);
|
||||
}
|
||||
}
|
||||
let doc = &self.tabs[i].scene.document;
|
||||
let current_style = doc.header.current_table_style_name.clone();
|
||||
table.table_style_handle = doc.objects.iter().find_map(|(handle, object)| {
|
||||
match object {
|
||||
acadrust::objects::ObjectType::TableStyle(style)
|
||||
if style.name.eq_ignore_ascii_case(¤t_style) =>
|
||||
{
|
||||
Some(*handle)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
let link_path = std::fs::canonicalize(path)
|
||||
.map(|value| value.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|_| path.to_string());
|
||||
let command = crate::modules::annotate::data_link::DataLinkPlaceCommand::new(
|
||||
table, &link_path,
|
||||
);
|
||||
self.command_line
|
||||
.push_info(&crate::command::CadCommand::prompt(&command));
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
self.place_data_link_table(&text, &link_path);
|
||||
}
|
||||
Err(e) => {
|
||||
self.command_line
|
||||
@@ -1849,6 +1825,51 @@ impl OpenCADStudio {
|
||||
}
|
||||
Some(self.finish_dispatch(cmd))
|
||||
}
|
||||
|
||||
/// Build the linked table from CSV text and start the placement command —
|
||||
/// the shared tail of the native typed-path `DATALINK` read and the web
|
||||
/// picker's `DataLinkCsvPicked` (which registers the picked bytes in the
|
||||
/// picked-source registry first so the link re-reads in-session).
|
||||
pub(in crate::app) fn place_data_link_table(&mut self, csv_text: &str, link_path: &str) {
|
||||
let i = self.active_tab;
|
||||
let rows_data = parse_csv_table(csv_text);
|
||||
let nrows = rows_data.len();
|
||||
let ncols = rows_data.iter().map(|r| r.len()).max().unwrap_or(0);
|
||||
if nrows == 0 || ncols == 0 {
|
||||
self.command_line
|
||||
.push_error(crate::t!("DATALINK: the CSV file is empty.").as_ref());
|
||||
return;
|
||||
}
|
||||
use acadrust::entities::TableBuilder;
|
||||
use acadrust::types::Vector3;
|
||||
let mut table = TableBuilder::new(nrows, ncols)
|
||||
.at(Vector3::new(0.0, 0.0, 0.0))
|
||||
.row_height(0.5)
|
||||
.column_width(2.0)
|
||||
.build();
|
||||
for (r, row) in rows_data.iter().enumerate() {
|
||||
for (c, cell) in row.iter().enumerate() {
|
||||
table.set_cell_text(r, c, cell);
|
||||
}
|
||||
}
|
||||
let doc = &self.tabs[i].scene.document;
|
||||
let current_style = doc.header.current_table_style_name.clone();
|
||||
table.table_style_handle = doc.objects.iter().find_map(|(handle, object)| {
|
||||
match object {
|
||||
acadrust::objects::ObjectType::TableStyle(style)
|
||||
if style.name.eq_ignore_ascii_case(¤t_style) =>
|
||||
{
|
||||
Some(*handle)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
let command =
|
||||
crate::modules::annotate::data_link::DataLinkPlaceCommand::new(table, link_path);
|
||||
self.command_line
|
||||
.push_info(&crate::command::CadCommand::prompt(&command));
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
}
|
||||
|
||||
// Scan LandXML text for <CgPoint> survey points. Each element's text content is
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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")),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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};
|
||||
})();
|
||||
Reference in new issue
Block a user