feat(plugin): harden V4 host integration

Preserve legacy wire indices and route concurrent V4 responses and background requests by correlation and stable tab ID.
This commit is contained in:
Hakan Seven 2026-08-15 20:17:43 +03:00
commit f247d15f6e
110 changed files with 32160 additions and 1255 deletions

6
.gitattributes vendored Normal file
View file

@ -0,0 +1,6 @@
# Line endings: always LF, never CRLF, on every platform and clone.
* text=auto eol=lf
# Binary files must never be line-ending normalized.
*.png binary
*.ttf binary

View file

@ -50,10 +50,8 @@ jobs:
with:
tool: wasm-bindgen@${{ steps.wasm-bindgen-version.outputs.version }}
# `--public-url` matches the project page sub-path:
# https://<user>.github.io/OpenCADStudio/.
- name: Build web bundle
run: trunk build --release --public-url /OpenCADStudio/
run: trunk build --release --public-url /
# GitHub Pages runs Jekyll, which ignores files; opt out so the
# wasm-bindgen output is served verbatim.
@ -177,10 +175,8 @@ jobs:
print(f"supporters: {len(supporters)}")
PY
# Publish an OpenCADStudio-branded star-history chart for the README.
# The timestamp media type returns when each current stargazer starred
# the repository; the script follows every API page and emits both themes.
- name: Generate star history charts
# Publish one themed growth chart for stars and release downloads.
- name: Generate project growth charts
env:
GITHUB_TOKEN: ${{ secrets.OCS_GITHUB_TOKEN }}
run: python3 scripts/generate-star-history.py --output-dir dist

278
Cargo.lock generated
View file

@ -85,7 +85,7 @@ dependencies = [
[[package]]
name = "acadrust"
version = "0.4.1"
source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=4736846#473684644e8bbacd1310a7eccdb4b3180d7013db"
source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=c28b2cd#c28b2cd25851ca0ee555b7cfac673927456ae04e"
dependencies = [
"ahash 0.8.12",
"anyhow",
@ -891,7 +891,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cadkernel"
version = "0.1.0"
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=1b84d6c#1b84d6c926d8053690a9975ccad6814800790008"
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=617bdec#617bdecabe97c0140ffdecef3d2af2e2b5e612b1"
dependencies = [
"acadrust",
"cavalier_contours",
@ -925,6 +925,18 @@ dependencies = [
"wayland-client",
]
[[package]]
name = "cargo-lock"
version = "11.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50524592e6bfbb1bf6f94e8184a786f637faeaf1cf37bbe65502b9a2c5c48939"
dependencies = [
"semver",
"serde",
"toml 1.1.3+spec-1.1.0",
"url",
]
[[package]]
name = "cavalier_contours"
version = "0.7.0"
@ -1564,6 +1576,15 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "erased-discriminant"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1a6df962265a53221f29081896c412ef325c17fa7d638cd9578febe53d3c82c"
dependencies = [
"typeid",
]
[[package]]
name = "errno"
version = "0.3.14"
@ -1835,6 +1856,15 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
name = "form_urlencoded"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
"percent-encoding",
]
[[package]]
name = "funty"
version = "2.0.0"
@ -2510,6 +2540,109 @@ dependencies = [
"winit",
]
[[package]]
name = "icu_collections"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
dependencies = [
"displaydoc",
"potential_utf",
"utf8_iter",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locale_core"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_normalizer"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
dependencies = [
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
[[package]]
name = "icu_properties"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
dependencies = [
"icu_collections",
"icu_locale_core",
"icu_properties_data",
"icu_provider",
"zerotrie",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
[[package]]
name = "icu_provider"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
dependencies = [
"displaydoc",
"icu_locale_core",
"writeable",
"yoke",
"zerofrom",
"zerotrie",
"zerovec",
]
[[package]]
name = "idna"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [
"idna_adapter",
"smallvec",
"utf8_iter",
]
[[package]]
name = "idna_adapter"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
dependencies = [
"icu_normalizer",
"icu_properties",
]
[[package]]
name = "image"
version = "0.25.10"
@ -2928,6 +3061,12 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litemap"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
[[package]]
name = "litrs"
version = "1.0.0"
@ -3760,16 +3899,19 @@ dependencies = [
[[package]]
name = "ocs_plugin_api"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"acadrust",
"bincode",
"cargo-lock",
"getrandom 0.2.17",
"interprocess",
"libloading",
"memmap2",
"rkyv",
"serde",
"serde-reflection",
"serde_json",
"thiserror 1.0.69",
]
@ -4086,6 +4228,15 @@ dependencies = [
"portable-atomic",
]
[[package]]
name = "potential_utf"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
dependencies = [
"zerovec",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
@ -4938,6 +5089,10 @@ name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
dependencies = [
"serde",
"serde_core",
]
[[package]]
name = "serde"
@ -4949,6 +5104,20 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde-reflection"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af0fdf9c543af37502581e3d6cf4e6cd1628d6616d6a1f8f4ca88aae3a405f5c"
dependencies = [
"erased-discriminant",
"once_cell",
"serde",
"serde_json",
"thiserror 1.0.69",
"typeid",
]
[[package]]
name = "serde_core"
version = "1.0.228"
@ -5359,6 +5528,17 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "synstructure"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "sys-locale"
version = "0.3.2"
@ -5665,6 +5845,12 @@ dependencies = [
"rustc-hash 2.1.3",
]
[[package]]
name = "typeid"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
[[package]]
name = "typenum"
version = "1.20.1"
@ -5818,6 +6004,18 @@ dependencies = [
"log",
]
[[package]]
name = "url"
version = "2.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
"serde",
]
[[package]]
name = "usvg"
version = "0.45.1"
@ -5846,6 +6044,12 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
@ -6759,6 +6963,12 @@ dependencies = [
"wayland-protocols-wlr",
]
[[package]]
name = "writeable"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
[[package]]
name = "wyz"
version = "0.5.1"
@ -6849,6 +7059,29 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5"
[[package]]
name = "yoke"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
dependencies = [
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
"synstructure",
]
[[package]]
name = "zbus"
version = "5.17.0"
@ -6941,6 +7174,21 @@ name = "zerofrom"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
"synstructure",
]
[[package]]
name = "zeroize"
@ -6948,6 +7196,17 @@ version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
[[package]]
name = "zerotrie"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
]
[[package]]
name = "zerovec"
version = "0.11.6"
@ -6955,7 +7214,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
dependencies = [
"serde",
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47402523226a02bfe5230160dc3ccc089aa6f6f19e7fcbb4e6f824bbb1b4aa62"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.3",
]
[[package]]

View file

@ -31,8 +31,8 @@ glam = { version = "0.33", features = ["bytemuck"] }
rfd = "0.17"
clap = { version = "4", features = ["derive"] }
env_logger = "0.11"
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "4736846", features = ["serde"] }
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "1b84d6c", features = ["acis", "offset"] }
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "c28b2cd", features = ["serde"] }
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "617bdec", features = ["acis", "offset"] }
acadifc = { git = "https://github.com/OpenAEC-Foundation/acadifc.git", rev = "47b5603", optional = true }
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
flate2 = "1"
@ -64,7 +64,6 @@ ashpd = { version = "0.13.13", default-features = false, features = ["async-io",
[patch.crates-io]
iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" }
iced_widget = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" }
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "4736846" }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
ocs_plugin_api = { path = "crates/ocs_plugin_api", features = ["host"] }

195
README.ar.md Normal file
View file

@ -0,0 +1,195 @@
<p align="center">
<a href="README.md">English</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
</p>
<p align="center"><img src="assets/logo.svg" width="112" alt="شعار Open CAD Studio"></p>
<h1 align="center">Open CAD Studio</h1>
<p align="center" dir="rtl">تطبيق مفتوح المصدر للرسم ثنائي الأبعاد والنمذجة ثلاثية الأبعاد على سطح المكتب والويب، مبني بلغة Rust.</p>
<p align="center">
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><img alt="أحدث إصدار" src="https://img.shields.io/github/v/release/HakanSeven12/OpenCADStudio"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases"><img alt="تنزيلات الإصدارات" src="https://img.shields.io/github/downloads/HakanSeven12/OpenCADStudio/total"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers"><img alt="نجوم GitHub" src="https://img.shields.io/github/stars/HakanSeven12/OpenCADStudio"></a>
<a href="LICENSE"><img alt="ترخيص GPL-3.0" src="https://img.shields.io/github/license/HakanSeven12/OpenCADStudio"></a>
</p>
<p align="center" dir="rtl">
<a href="https://www.opencadstudio.com"><strong>تشغيل تطبيق الويب</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><strong>تنزيل تطبيق سطح المكتب</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/discussions"><strong>الانضمام إلى النقاش</strong></a>
</p>
<p align="center"><img src="https://github.com/user-attachments/assets/10635ad0-454b-4c87-935f-1a3a46f24ccb" alt="مساحة عمل Open CAD Studio" width="100%"></p>
## نظرة عامة
Open CAD Studio تطبيق متعدد المنصات للرسم التقني والعمل على التخطيطات ونمذجة المجسمات. يقرأ رسومات DWG وDXF ويكتبها مباشرة، ويشترك إصدار سطح المكتب وإصدار المتصفح في نواة التحرير نفسها.
المشروع قيد التطوير النشط. احتفظ بنسخ احتياطية من رسومات الإنتاج المهمة، وأبلغ عن المشكلات القابلة لإعادة الإنتاج عبر [GitHub Issues](https://github.com/HakanSeven12/OpenCADStudio/issues).
## أبرز الميزات
- **سير عمل أصلي للرسومات** — فتح ملفات DWG وDXF وتحريرها واستعادتها وحفظها من دون خدمة تحويل.
- **رسم ثنائي الأبعاد دقيق** — خطوط وخطوط متعددة ومنحنيات ومنحنيات spline وتهشير والتقاط الكائنات والتتبع والطبقات والكتل والمراجع الخارجية.
- **أدوات التوثيق** — نصوص وأبعاد وخطوط إشارة وتفاوتات وجداول ومساحة النموذج ومساحة الورق وإطارات العرض وأنماط الطباعة.
- **نمذجة ثلاثية الأبعاد مدعومة بنواة هندسية** — مجسمات أولية وبثق ودوران ومسح وloft وعمليات منطقية وتجزئة كيانات ACIS.
- **تصيير عبر GPU** — إطارات عرض ثنائية وثلاثية الأبعاد مسرّعة بواسطة `wgpu` مع كاميرات متعامدة ومنظورية.
- **سير عمل قابل للتوسعة** — إضافات أصلية ونصوص أوامر وتحويل من دون واجهة وواجهة JSON للأتمتة تعتمد على الأسطر.
<p align="center"><img src="https://github.com/user-attachments/assets/2a037a09-e8e8-498c-8ed3-58ecb8ae958d" alt="نموذج ثلاثي الأبعاد في Open CAD Studio" width="100%"></p>
## سير عمل الملفات
| التنسيق أو سير العمل | الدعم |
| --- | --- |
| DWG | قراءة وكتابة؛ اختيار إصدارات الحفظ من R14 إلى 2018 |
| DXF | قراءة وكتابة؛ اختيار إصدارات الحفظ من R14 إلى 2018 |
| BAK / SV$ | فتح النسخ الاحتياطية وملفات الحفظ التلقائي للرسومات |
| OBJ | استيراد الشبكات متعددة الأضلاع |
| LandXML | استيراد نقاط المسح `CgPoint` |
| STL | تصدير بيانات الشبكات ثلاثية الأبعاد |
| STEP AP203 | تصدير بيانات الشبكات ثلاثية الأبعاد |
| PDF | طباعة التخطيطات والعناصر الهندسية المحددة في إصدار سطح المكتب |
| CSV | استخراج بيانات خصائص الكيانات |
| CTB / STB | تحميل جداول أنماط الطباعة وتحريرها |
## سطح المكتب أم الويب
استخدم [تطبيق الويب](https://www.opencadstudio.com) للوصول الفوري من دون تثبيت. تُختار الرسومات من خلال المتصفح وتُحفظ كتنزيلات محلية.
استخدم تطبيق سطح المكتب لارتباطات الملفات الأصلية والصور المصغرة في مدير الملفات وطباعة النظام وإخراج PDF والإضافات الخارجية ونصوص الأوامر والأتمتة من دون واجهة. تتوفر إصدارات لنظام Windows وLinux وmacOS بمعالجات Apple Silicon.
## التثبيت
نزّل جميع الحزم الحالية من [أحدث إصدار](https://github.com/HakanSeven12/OpenCADStudio/releases/latest).
### Windows
اختر إحدى حزم x86-64 الموقّعة:
- `OpenCADStudio-*-windows-x86_64-installer.msi` — برنامج التثبيت الموصى به، ويتضمن اختصارات قائمة ابدأ وارتباطات ملفات DWG/DXF وصوراً مصغرة للرسومات.
- `OpenCADStudio-*-windows-x86_64-portable.exe` — تطبيق مستقل لا يحتاج إلى تثبيت.
### Linux
نزّل AppImage لمعمارية x86-64 واجعله قابلاً للتنفيذ ثم شغّله:
```bash
chmod +x OpenCADStudio-*-linux-x86_64.AppImage
./OpenCADStudio-*-linux-x86_64.AppImage
```
### macOS
تدعم حزمة macOS المنشورة أجهزة Apple Silicon:
1. نزّل `OpenCADStudio-*-macos-arm64.dmg`.
2. افتح الصورة واسحب `OpenCADStudio.app` إلى **Applications**.
3. إذا منع Gatekeeper التشغيل الأول، فاسمح بالتطبيق من **System Settings → Privacy & Security**.
التطبيق موقّع بتوقيع ad hoc، لكنه غير موثّق حالياً لدى Apple.
## اللغات
يمكن لـ Open CAD Studio اتباع لغة النظام أو استخدام إحدى لغات الواجهة التسع عشرة التالية:
> العربية · البرتغالية البرازيلية · التشيكية · الهولندية · الإنجليزية · الفنلندية · الفرنسية · الألمانية · الهندية · المجرية · الإيطالية · اليابانية · الكورية · البولندية · الروسية · الصينية المبسطة · الإسبانية · الصينية التقليدية · التركية
غيّر اللغة من إعدادات التطبيق. عند اختيار **النظام**، يستخدم إصدار المتصفح أيضاً الإعدادات المحلية المفضلة للمتصفح.
## البناء من المصدر
### سطح المكتب
المتطلبات:
- Git
- سلسلة أدوات Rust المستقرة الحالية
- مكتبات تطوير الرسوم والخطوط الخاصة بالمنصة
ثبّت الاعتماديات الأصلية على Ubuntu أو Debian:
```bash
sudo apt update
sudo apt install libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev \
libxrandr-dev libxkbcommon-dev libwayland-dev libfontconfig1-dev \
libfreetype6-dev
```
ثم ابنِ التطبيق:
```bash
git clone https://github.com/HakanSeven12/OpenCADStudio.git
cd OpenCADStudio
cargo build --release --bin OpenCADStudio
```
يُكتب الملف التنفيذي الناتج في `target/release/OpenCADStudio` (`OpenCADStudio.exe` على Windows).
### الويب
ثبّت هدف WebAssembly وأدوات البناء مرة واحدة:
```bash
rustup target add wasm32-unknown-unknown
cargo install trunk wasm-bindgen-cli
```
شغّل خادم التطوير:
```bash
trunk serve
```
## الأتمتة
يدعم الملف التنفيذي لسطح المكتب التحويل لمرة واحدة وخادماً دائماً من دون واجهة:
```bash
OpenCADStudio --export input.dwg output.dxf
OpenCADStudio --serve
OpenCADStudio --serve --port 4242
```
يتبادل الخادم كائن JSON واحداً في كل سطر عبر الإدخال/الإخراج القياسي أو مقبس TCP محلي. راجع [دليل الأتمتة](docs/automation/README.md) و[عميل Python](docs/automation/ocs.py) المرفق.
## الإضافات
تعمل إضافات سطح المكتب في عمليات منفصلة وتتواصل مع المضيف عبر واجهة إضافات ذات إصدارات. لا يحمّل إصدار المتصفح الإضافات الأصلية.
- [بنية الإضافات](docs/plugin-architecture.md)
- [قالب الإضافة](docs/plugin-template/README.md)
- [سجل الإضافات](plugins/README.md)
## وثائق المشروع
- [واجهة الأتمتة](docs/automation/README.md)
- [بنية الإضافات](docs/plugin-architecture.md)
- [مسار التجزئة](docs/tessellation.md)
- [سياسة الأمان](SECURITY.md)
## المساهمة
نرحب بتقارير الأخطاء وطلبات السحب المركزة والترجمات وتحسينات الوثائق والمساهمات في الإضافات.
- ابحث في [المشكلات](https://github.com/HakanSeven12/OpenCADStudio/issues) الحالية قبل فتح تقرير جديد.
- استخدم [Discussions](https://github.com/HakanSeven12/OpenCADStudio/discussions) للأسئلة والأفكار.
- أبلغ عن الثغرات بشكل خاص وفق [سياسة الأمان](SECURITY.md).
## نمو المشروع
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://www.opencadstudio.com/star-history-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://www.opencadstudio.com/star-history-light.svg">
<img alt="نجوم Open CAD Studio وتنزيلات الإصدارات" src="https://www.opencadstudio.com/star-history-light.svg">
</picture>
</a>
## دعم المشروع
إذا ساعدك Open CAD Studio في عملك، فادعم استمرار التطوير عبر [GitHub Sponsors](https://github.com/sponsors/HakanSeven12) أو [Patreon](https://www.patreon.com/HakanSeven12).
## الترخيص
يُوزّع Open CAD Studio بموجب [رخصة جنو العمومية الإصدار 3.0](LICENSE).

195
README.cs.md Normal file
View file

@ -0,0 +1,195 @@
<p align="center">
<a href="README.md">English</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
</p>
<p align="center"><img src="assets/logo.svg" width="112" alt="Logo Open CAD Studio"></p>
<h1 align="center">Open CAD Studio</h1>
<p align="center">Open-source 2D kreslení a 3D modelování pro počítač i web, vytvořené v Rustu.</p>
<p align="center">
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><img alt="Nejnovější verze" src="https://img.shields.io/github/v/release/HakanSeven12/OpenCADStudio"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases"><img alt="Stažení" src="https://img.shields.io/github/downloads/HakanSeven12/OpenCADStudio/total"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers"><img alt="Hvězdy GitHub" src="https://img.shields.io/github/stars/HakanSeven12/OpenCADStudio"></a>
<a href="LICENSE"><img alt="Licence GPL-3.0" src="https://img.shields.io/github/license/HakanSeven12/OpenCADStudio"></a>
</p>
<p align="center">
<a href="https://www.opencadstudio.com"><strong>Spustit webovou aplikaci</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><strong>Stáhnout desktopovou aplikaci</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/discussions"><strong>Zapojit se do diskuse</strong></a>
</p>
<p align="center"><img src="https://github.com/user-attachments/assets/10635ad0-454b-4c87-935f-1a3a46f24ccb" alt="Pracovní plocha Open CAD Studio" width="100%"></p>
## Přehled
Open CAD Studio je multiplatformní aplikace pro technické kreslení, práci s rozvržením a modelování těles. Nativně čte a zapisuje výkresy DWG a DXF; desktopová a prohlížečová verze používají společné editační jádro.
Projekt je aktivně vyvíjen. Důležité produkční výkresy zálohujte a reprodukovatelné problémy hlaste přes [GitHub Issues](https://github.com/HakanSeven12/OpenCADStudio/issues).
## Hlavní funkce
- **Nativní práce s výkresy** — otevírání, úpravy, obnova a ukládání DWG a DXF bez konverzní služby.
- **Přesné 2D kreslení** — úsečky, křivky, spline, šrafy, uchopení objektů, trasování, hladiny, bloky a externí reference.
- **Dokumentační nástroje** — text, kóty, odkazové čáry, tolerance, tabulky, modelový prostor, výkresový prostor, výřezy a styly vykreslování.
- **3D modelování s geometrickým jádrem** — základní tělesa, vysunutí, rotace, tažení, loft, booleovské operace a teselace entit ACIS.
- **Vykreslování přes GPU** — akcelerované 2D a 3D pohledy pomocí `wgpu`, s ortografickou a perspektivní kamerou.
- **Rozšiřitelné postupy** — nativní pluginy, příkazové skripty, bezobslužná konverze a řádkové JSON automatizační API.
<p align="center"><img src="https://github.com/user-attachments/assets/2a037a09-e8e8-498c-8ed3-58ecb8ae958d" alt="3D model v Open CAD Studio" width="100%"></p>
## Práce se soubory
| Formát nebo postup | Podpora |
| --- | --- |
| DWG | Čtení a zápis; cílové verze ukládání R14 až 2018 |
| DXF | Čtení a zápis; cílové verze ukládání R14 až 2018 |
| BAK / SV$ | Otevírání záloh a automaticky uložených výkresů |
| OBJ | Import polygonových sítí |
| LandXML | Import zaměřených bodů `CgPoint` |
| STL | Export dat 3D sítí |
| STEP AP203 | Export dat 3D sítí |
| PDF | Vykreslení rozvržení a vybrané geometrie na počítači |
| CSV | Extrakce vlastností entit |
| CTB / STB | Načtení a úpravy tabulek stylů vykreslování |
## Počítač nebo web
Pro okamžitý přístup bez instalace použijte [webovou aplikaci](https://www.opencadstudio.com). Výkresy se vybírají v prohlížeči a ukládají jako místní soubory ke stažení.
Desktopovou aplikaci použijte pro nativní asociace souborů, náhledy ve správci souborů, systémový tisk, výstup PDF, externí pluginy, příkazové skripty a bezobslužnou automatizaci. Vydání jsou dostupná pro Windows, Linux a macOS s Apple Silicon.
## Instalace
Všechny aktuální balíčky stáhnete z [nejnovějšího vydání](https://github.com/HakanSeven12/OpenCADStudio/releases/latest).
### Windows
Vyberte jeden z podepsaných balíčků x86-64:
- `OpenCADStudio-*-windows-x86_64-installer.msi` — doporučený instalátor se zástupci v nabídce Start, asociacemi DWG/DXF a náhledy výkresů.
- `OpenCADStudio-*-windows-x86_64-portable.exe` — samostatná aplikace bez instalace.
### Linux
Stáhněte x86-64 AppImage, nastavte jej jako spustitelný a spusťte:
```bash
chmod +x OpenCADStudio-*-linux-x86_64.AppImage
./OpenCADStudio-*-linux-x86_64.AppImage
```
### macOS
Publikovaný balíček pro macOS podporuje Apple Silicon:
1. Stáhněte `OpenCADStudio-*-macos-arm64.dmg`.
2. Otevřete obraz a přetáhněte `OpenCADStudio.app` do **Applications**.
3. Pokud Gatekeeper první spuštění zablokuje, povolte aplikaci v **System Settings → Privacy & Security**.
Aplikace je podepsána ad hoc, ale v současnosti není notářsky ověřena společností Apple.
## Jazyky
Open CAD Studio může používat jazyk systému nebo jeden z těchto 19 jazyků rozhraní:
> Arabština · Brazilská portugalština · Čeština · Nizozemština · Angličtina · Finština · Francouzština · Němčina · Hindština · Maďarština · Italština · Japonština · Korejština · Polština · Ruština · Zjednodušená čínština · Španělština · Tradiční čínština · Turečtina
Jazyk změníte v nastavení aplikace. Pokud je vybrána možnost **Systém**, webová verze používá také preferované národní prostředí prohlížeče.
## Sestavení ze zdrojového kódu
### Desktop
Požadavky:
- Git
- Aktuální stabilní nástroje Rust
- Vývojové knihovny grafiky a písem dané platformy
Na Ubuntu nebo Debianu nainstalujte nativní závislosti:
```bash
sudo apt update
sudo apt install libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev \
libxrandr-dev libxkbcommon-dev libwayland-dev libfontconfig1-dev \
libfreetype6-dev
```
Poté sestavte:
```bash
git clone https://github.com/HakanSeven12/OpenCADStudio.git
cd OpenCADStudio
cargo build --release --bin OpenCADStudio
```
Výsledný program bude v `target/release/OpenCADStudio` (ve Windows `OpenCADStudio.exe`).
### Web
Jednorázově nainstalujte cíl WebAssembly a nástroje pro sestavení:
```bash
rustup target add wasm32-unknown-unknown
cargo install trunk wasm-bindgen-cli
```
Spusťte vývojový server:
```bash
trunk serve
```
## Automatizace
Desktopový program podporuje jednorázovou konverzi a trvalý bezobslužný server:
```bash
OpenCADStudio --export input.dwg output.dxf
OpenCADStudio --serve
OpenCADStudio --serve --port 4242
```
Server předává jeden objekt JSON na řádek přes standardní vstup/výstup nebo místní TCP socket. Viz [průvodce automatizací](docs/automation/README.md) a přiložený [klient Python](docs/automation/ocs.py).
## Pluginy
Desktopové pluginy běží v oddělených procesech a komunikují s hostitelem přes verzované API pluginů. Prohlížečová verze nativní pluginy nenačítá.
- [Architektura pluginů](docs/plugin-architecture.md)
- [Šablona pluginu](docs/plugin-template/README.md)
- [Registr pluginů](plugins/README.md)
## Dokumentace projektu
- [Automatizační API](docs/automation/README.md)
- [Architektura pluginů](docs/plugin-architecture.md)
- [Proces teselace](docs/tessellation.md)
- [Bezpečnostní zásady](SECURITY.md)
## Přispívání
Vítáme hlášení chyb, cílené pull requesty, překlady, vylepšení dokumentace i příspěvky k pluginům.
- Před otevřením nového hlášení prohledejte existující [issues](https://github.com/HakanSeven12/OpenCADStudio/issues).
- Pro otázky a nápady použijte [Discussions](https://github.com/HakanSeven12/OpenCADStudio/discussions).
- Zranitelnosti hlaste soukromě podle [bezpečnostních zásad](SECURITY.md).
## Růst projektu
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://www.opencadstudio.com/star-history-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://www.opencadstudio.com/star-history-light.svg">
<img alt="Hvězdy a stažení Open CAD Studio" src="https://www.opencadstudio.com/star-history-light.svg">
</picture>
</a>
## Podpořte projekt
Pokud vám Open CAD Studio pomáhá, podpořte další vývoj přes [GitHub Sponsors](https://github.com/sponsors/HakanSeven12) nebo [Patreon](https://www.patreon.com/HakanSeven12).
## Licence
Open CAD Studio je šířeno pod [GNU General Public License v3.0](LICENSE).

195
README.de.md Normal file
View file

@ -0,0 +1,195 @@
<p align="center">
<a href="README.md">English</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
</p>
<p align="center"><img src="assets/logo.svg" width="112" alt="Open-CAD-Studio-Logo"></p>
<h1 align="center">Open CAD Studio</h1>
<p align="center">Quelloffenes 2D-Zeichnen und 3D-Modellieren für Desktop und Web, entwickelt mit Rust.</p>
<p align="center">
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><img alt="Neueste Version" src="https://img.shields.io/github/v/release/HakanSeven12/OpenCADStudio"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases"><img alt="Downloads" src="https://img.shields.io/github/downloads/HakanSeven12/OpenCADStudio/total"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers"><img alt="GitHub-Sterne" src="https://img.shields.io/github/stars/HakanSeven12/OpenCADStudio"></a>
<a href="LICENSE"><img alt="GPL-3.0-Lizenz" src="https://img.shields.io/github/license/HakanSeven12/OpenCADStudio"></a>
</p>
<p align="center">
<a href="https://www.opencadstudio.com"><strong>Web-App starten</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><strong>Desktop-App herunterladen</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/discussions"><strong>An Diskussionen teilnehmen</strong></a>
</p>
<p align="center"><img src="https://github.com/user-attachments/assets/10635ad0-454b-4c87-935f-1a3a46f24ccb" alt="Arbeitsbereich von Open CAD Studio" width="100%"></p>
## Überblick
Open CAD Studio ist eine plattformübergreifende Anwendung für technische Zeichnungen, Layoutarbeit und Volumenmodellierung. DWG- und DXF-Zeichnungen werden nativ gelesen und geschrieben; Desktop- und Browserversion nutzen denselben Bearbeitungskern.
Das Projekt wird aktiv entwickelt. Bewahre Sicherungskopien wichtiger Produktionszeichnungen auf und melde reproduzierbare Probleme über [GitHub Issues](https://github.com/HakanSeven12/OpenCADStudio/issues).
## Höhepunkte
- **Nativer Zeichnungsablauf** — DWG- und DXF-Dateien ohne Konvertierungsdienst öffnen, bearbeiten, wiederherstellen und speichern.
- **Präzises 2D-Zeichnen** — Linien, Polylinien, Kurven, Splines, Schraffuren, Objektfang, Spurverfolgung, Layer, Blöcke und externe Referenzen.
- **Dokumentationswerkzeuge** — Text, Bemaßungen, Führungslinien, Toleranzen, Tabellen, Modellbereich, Papierbereich, Ansichtsfenster und Plotstile.
- **Kernel-gestützte 3D-Modellierung** — Volumenprimitive, Extrusion, Rotation, Sweep, Loft, boolesche Operationen und Tessellierung von ACIS-Elementen.
- **GPU-Darstellung** — beschleunigte 2D- und 3D-Ansichten über `wgpu` mit orthografischen und perspektivischen Kameras.
- **Erweiterbare Abläufe** — native Plugins, Befehlsskripte, Konvertierung ohne Oberfläche und zeilenbasierte JSON-Automatisierungs-API.
<p align="center"><img src="https://github.com/user-attachments/assets/2a037a09-e8e8-498c-8ed3-58ecb8ae958d" alt="3D-Modell in Open CAD Studio" width="100%"></p>
## Dateiabläufe
| Format oder Ablauf | Unterstützung |
| --- | --- |
| DWG | Lesen und Schreiben; versionierte Speicherziele von R14 bis 2018 |
| DXF | Lesen und Schreiben; versionierte Speicherziele von R14 bis 2018 |
| BAK / SV$ | Zeichnungssicherungen und automatisch gespeicherte Dateien öffnen |
| OBJ | Polygonnetze importieren |
| LandXML | `CgPoint`-Vermessungspunkte importieren |
| STL | 3D-Netzdaten exportieren |
| STEP AP203 | 3D-Netzdaten exportieren |
| PDF | Layouts und ausgewählte Geometrie auf dem Desktop plotten |
| CSV | Eigenschaftsdaten von Elementen extrahieren |
| CTB / STB | Plotstiltabellen laden und bearbeiten |
## Desktop oder Web
Nutze die [Web-App](https://www.opencadstudio.com) für sofortigen Zugriff ohne Installation. Zeichnungen werden im Browser ausgewählt und als lokale Downloads gespeichert.
Nutze die Desktop-Anwendung für native Dateizuordnungen, Vorschaubilder im Dateimanager, Systemdruck, PDF-Ausgabe, externe Plugins, Befehlsskripte und Automatisierung ohne Oberfläche. Veröffentlichungen sind für Windows, Linux und Apple-Silicon-macOS verfügbar.
## Installation
Lade alle aktuellen Pakete von der [neuesten Version](https://github.com/HakanSeven12/OpenCADStudio/releases/latest) herunter.
### Windows
Wähle eines dieser signierten x86-64-Pakete:
- `OpenCADStudio-*-windows-x86_64-installer.msi` — empfohlener Installer mit Startmenü-Verknüpfungen, DWG/DXF-Dateizuordnungen und Zeichnungsvorschauen.
- `OpenCADStudio-*-windows-x86_64-portable.exe` — eigenständige Anwendung ohne Installation.
### Linux
Lade das x86-64-AppImage herunter, mache es ausführbar und starte es:
```bash
chmod +x OpenCADStudio-*-linux-x86_64.AppImage
./OpenCADStudio-*-linux-x86_64.AppImage
```
### macOS
Das veröffentlichte macOS-Paket unterstützt Apple Silicon:
1. `OpenCADStudio-*-macos-arm64.dmg` herunterladen.
2. Image öffnen und `OpenCADStudio.app` nach **Applications** ziehen.
3. Falls Gatekeeper den ersten Start blockiert, die App unter **System Settings → Privacy & Security** freigeben.
Die Anwendung ist ad hoc signiert, wird derzeit aber nicht von Apple notarisiert.
## Sprachen
Open CAD Studio kann der Systemsprache folgen oder eine dieser 19 Oberflächensprachen verwenden:
> Arabisch · Brasilianisches Portugiesisch · Tschechisch · Niederländisch · Englisch · Finnisch · Französisch · Deutsch · Hindi · Ungarisch · Italienisch · Japanisch · Koreanisch · Polnisch · Russisch · Vereinfachtes Chinesisch · Spanisch · Traditionelles Chinesisch · Türkisch
Die Sprache lässt sich in den Anwendungseinstellungen ändern. Die Browserversion verwendet bei Auswahl von **System** ebenfalls das bevorzugte Gebietsschema des Browsers.
## Aus dem Quellcode erstellen
### Desktop
Voraussetzungen:
- Git
- Aktuelle stabile Rust-Toolchain
- Plattformspezifische Entwicklungsbibliotheken für Grafik und Schriften
Unter Ubuntu oder Debian die nativen Abhängigkeiten installieren:
```bash
sudo apt update
sudo apt install libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev \
libxrandr-dev libxkbcommon-dev libwayland-dev libfontconfig1-dev \
libfreetype6-dev
```
Danach erstellen:
```bash
git clone https://github.com/HakanSeven12/OpenCADStudio.git
cd OpenCADStudio
cargo build --release --bin OpenCADStudio
```
Die erzeugte Binärdatei liegt unter `target/release/OpenCADStudio` (unter Windows `OpenCADStudio.exe`).
### Web
WebAssembly-Ziel und Build-Werkzeuge einmalig installieren:
```bash
rustup target add wasm32-unknown-unknown
cargo install trunk wasm-bindgen-cli
```
Entwicklungsserver starten:
```bash
trunk serve
```
## Automatisierung
Die Desktop-Binärdatei unterstützt einmalige Konvertierung und einen dauerhaften Server ohne Oberfläche:
```bash
OpenCADStudio --export input.dwg output.dxf
OpenCADStudio --serve
OpenCADStudio --serve --port 4242
```
Der Server tauscht über Standardein-/ausgabe oder einen lokalen TCP-Socket je Zeile ein JSON-Objekt aus. Siehe [Automatisierungsanleitung](docs/automation/README.md) und den enthaltenen [Python-Client](docs/automation/ocs.py).
## Plugins
Desktop-Plugins laufen in getrennten Prozessen und kommunizieren über die versionierte Plugin-API mit dem Host. Die Browserversion lädt keine nativen Plugins.
- [Plugin-Architektur](docs/plugin-architecture.md)
- [Plugin-Vorlage](docs/plugin-template/README.md)
- [Plugin-Verzeichnis](plugins/README.md)
## Projektdokumentation
- [Automatisierungs-API](docs/automation/README.md)
- [Plugin-Architektur](docs/plugin-architecture.md)
- [Tessellierungs-Pipeline](docs/tessellation.md)
- [Sicherheitsrichtlinie](SECURITY.md)
## Mitwirken
Fehlerberichte, fokussierte Pull Requests, Übersetzungen, Dokumentationsverbesserungen und Plugin-Beiträge sind willkommen.
- Vor einem neuen Bericht bestehende [Issues](https://github.com/HakanSeven12/OpenCADStudio/issues) durchsuchen.
- [Discussions](https://github.com/HakanSeven12/OpenCADStudio/discussions) für Fragen und Ideen verwenden.
- Schwachstellen gemäß der [Sicherheitsrichtlinie](SECURITY.md) vertraulich melden.
## Projektwachstum
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://www.opencadstudio.com/star-history-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://www.opencadstudio.com/star-history-light.svg">
<img alt="Sterne und Versionsdownloads von Open CAD Studio" src="https://www.opencadstudio.com/star-history-light.svg">
</picture>
</a>
## Projekt unterstützen
Wenn Open CAD Studio bei der Arbeit hilft, unterstütze die weitere Entwicklung über [GitHub Sponsors](https://github.com/sponsors/HakanSeven12) oder [Patreon](https://www.patreon.com/HakanSeven12).
## Lizenz
Open CAD Studio wird unter der [GNU General Public License v3.0](LICENSE) bereitgestellt.

195
README.es.md Normal file
View file

@ -0,0 +1,195 @@
<p align="center">
<a href="README.md">English</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
</p>
<p align="center"><img src="assets/logo.svg" width="112" alt="Logotipo de Open CAD Studio"></p>
<h1 align="center">Open CAD Studio</h1>
<p align="center">Dibujo 2D y modelado 3D de código abierto para escritorio y web, desarrollado con Rust.</p>
<p align="center">
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><img alt="Última versión" src="https://img.shields.io/github/v/release/HakanSeven12/OpenCADStudio"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases"><img alt="Descargas" src="https://img.shields.io/github/downloads/HakanSeven12/OpenCADStudio/total"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers"><img alt="Estrellas en GitHub" src="https://img.shields.io/github/stars/HakanSeven12/OpenCADStudio"></a>
<a href="LICENSE"><img alt="Licencia GPL-3.0" src="https://img.shields.io/github/license/HakanSeven12/OpenCADStudio"></a>
</p>
<p align="center">
<a href="https://www.opencadstudio.com"><strong>Abrir la aplicación web</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><strong>Descargar la aplicación de escritorio</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/discussions"><strong>Participar en la conversación</strong></a>
</p>
<p align="center"><img src="https://github.com/user-attachments/assets/10635ad0-454b-4c87-935f-1a3a46f24ccb" alt="Espacio de trabajo de Open CAD Studio" width="100%"></p>
## Descripción general
Open CAD Studio es una aplicación multiplataforma para dibujo técnico, trabajo con presentaciones y modelado de sólidos. Lee y escribe dibujos DWG y DXF de forma nativa, con un núcleo de edición compartido entre las versiones de escritorio y navegador.
El proyecto está en desarrollo activo. Conserva copias de seguridad de los dibujos de producción importantes e informa de problemas reproducibles mediante [GitHub Issues](https://github.com/HakanSeven12/OpenCADStudio/issues).
## Características destacadas
- **Flujo de dibujo nativo** — abre, edita, recupera y guarda archivos DWG y DXF sin un servicio de conversión.
- **Dibujo 2D preciso** — líneas, polilíneas, curvas, splines, sombreados, referencias a objetos, rastreo, capas, bloques y referencias externas.
- **Herramientas de documentación** — texto, cotas, directrices, tolerancias, tablas, espacio modelo, espacio papel, ventanas gráficas y estilos de trazado.
- **Modelado 3D respaldado por kernel** — primitivas sólidas, extrusión, revolución, barrido, loft, operaciones booleanas y teselación de entidades ACIS.
- **Renderizado por GPU** — vistas 2D y 3D aceleradas mediante `wgpu`, con cámaras ortográfica y en perspectiva.
- **Flujos ampliables** — complementos nativos, scripts de comandos, conversión sin interfaz y una API de automatización JSON basada en líneas.
<p align="center"><img src="https://github.com/user-attachments/assets/2a037a09-e8e8-498c-8ed3-58ecb8ae958d" alt="Modelo 3D en Open CAD Studio" width="100%"></p>
## Flujos de archivos
| Formato o flujo | Compatibilidad |
| --- | --- |
| DWG | Lectura y escritura; destinos de guardado versionados de R14 a 2018 |
| DXF | Lectura y escritura; destinos de guardado versionados de R14 a 2018 |
| BAK / SV$ | Apertura de copias de seguridad y archivos de guardado automático |
| OBJ | Importación de mallas poligonales |
| LandXML | Importación de puntos topográficos `CgPoint` |
| STL | Exportación de datos de malla 3D |
| STEP AP203 | Exportación de datos de malla 3D |
| PDF | Trazado de presentaciones y geometría seleccionada en escritorio |
| CSV | Extracción de datos de propiedades de entidades |
| CTB / STB | Carga y edición de tablas de estilos de trazado |
## Escritorio o web
Usa la [aplicación web](https://www.opencadstudio.com) para acceder de inmediato sin instalar nada. Los dibujos se seleccionan mediante el navegador y se guardan como descargas locales.
Usa la aplicación de escritorio para asociaciones de archivos nativas, miniaturas del gestor de archivos, impresión del sistema, salida PDF, complementos externos, scripts de comandos y automatización sin interfaz. Hay versiones para Windows, Linux y macOS con Apple Silicon.
## Instalación
Descarga todos los paquetes actuales desde la [última versión](https://github.com/HakanSeven12/OpenCADStudio/releases/latest).
### Windows
Elige uno de estos paquetes x86-64 firmados:
- `OpenCADStudio-*-windows-x86_64-installer.msi` — instalador recomendado con accesos directos del menú Inicio, asociaciones DWG/DXF y miniaturas de dibujos.
- `OpenCADStudio-*-windows-x86_64-portable.exe` — aplicación independiente; no requiere instalación.
### Linux
Descarga la AppImage x86-64, hazla ejecutable e iníciala:
```bash
chmod +x OpenCADStudio-*-linux-x86_64.AppImage
./OpenCADStudio-*-linux-x86_64.AppImage
```
### macOS
El paquete publicado para macOS es compatible con Apple Silicon:
1. Descarga `OpenCADStudio-*-macos-arm64.dmg`.
2. Abre la imagen y arrastra `OpenCADStudio.app` a **Applications**.
3. Si Gatekeeper bloquea el primer inicio, autoriza la aplicación en **System Settings → Privacy & Security**.
La aplicación tiene firma ad hoc, pero actualmente no está notarizada por Apple.
## Idiomas
Open CAD Studio puede seguir el idioma del sistema o usar cualquiera de estos 19 idiomas de interfaz:
> Árabe · Portugués de Brasil · Checo · Neerlandés · Inglés · Finés · Francés · Alemán · Hindi · Húngaro · Italiano · Japonés · Coreano · Polaco · Ruso · Chino simplificado · Español · Chino tradicional · Turco
Cambia el idioma en los ajustes de la aplicación. La versión web también usa la configuración regional preferida del navegador cuando se selecciona **Sistema**.
## Compilar desde el código fuente
### Escritorio
Requisitos:
- Git
- Cadena de herramientas estable actual de Rust
- Bibliotecas de desarrollo de gráficos y fuentes de la plataforma
En Ubuntu o Debian, instala las dependencias nativas:
```bash
sudo apt update
sudo apt install libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev \
libxrandr-dev libxkbcommon-dev libwayland-dev libfontconfig1-dev \
libfreetype6-dev
```
Después compila:
```bash
git clone https://github.com/HakanSeven12/OpenCADStudio.git
cd OpenCADStudio
cargo build --release --bin OpenCADStudio
```
El binario resultante se escribe en `target/release/OpenCADStudio` (`OpenCADStudio.exe` en Windows).
### Web
Instala una vez el destino WebAssembly y las herramientas de compilación:
```bash
rustup target add wasm32-unknown-unknown
cargo install trunk wasm-bindgen-cli
```
Inicia el servidor de desarrollo:
```bash
trunk serve
```
## Automatización
El binario de escritorio admite conversión puntual y un servidor persistente sin interfaz:
```bash
OpenCADStudio --export input.dwg output.dxf
OpenCADStudio --serve
OpenCADStudio --serve --port 4242
```
El servidor intercambia un objeto JSON por línea mediante entrada/salida estándar o un socket TCP local. Consulta la [guía de automatización](docs/automation/README.md) y el [cliente Python](docs/automation/ocs.py) incluido.
## Complementos
Los complementos de escritorio se ejecutan en procesos separados y se comunican con el anfitrión mediante la API de complementos versionada. La versión del navegador no carga complementos nativos.
- [Arquitectura de complementos](docs/plugin-architecture.md)
- [Plantilla de complemento](docs/plugin-template/README.md)
- [Registro de complementos](plugins/README.md)
## Documentación del proyecto
- [API de automatización](docs/automation/README.md)
- [Arquitectura de complementos](docs/plugin-architecture.md)
- [Canal de teselación](docs/tessellation.md)
- [Política de seguridad](SECURITY.md)
## Contribuir
Son bienvenidos los informes de errores, pull requests específicos, traducciones, mejoras de documentación y contribuciones de complementos.
- Busca en los [issues](https://github.com/HakanSeven12/OpenCADStudio/issues) existentes antes de abrir un informe nuevo.
- Usa [Discussions](https://github.com/HakanSeven12/OpenCADStudio/discussions) para preguntas e ideas.
- Informa de vulnerabilidades de forma privada siguiendo la [política de seguridad](SECURITY.md).
## Crecimiento del proyecto
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://www.opencadstudio.com/star-history-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://www.opencadstudio.com/star-history-light.svg">
<img alt="Estrellas y descargas de versiones de Open CAD Studio" src="https://www.opencadstudio.com/star-history-light.svg">
</picture>
</a>
## Apoya el proyecto
Si Open CAD Studio te ayuda en tu trabajo, apoya su desarrollo mediante [GitHub Sponsors](https://github.com/sponsors/HakanSeven12) o [Patreon](https://www.patreon.com/HakanSeven12).
## Licencia
Open CAD Studio se distribuye bajo la [Licencia Pública General de GNU v3.0](LICENSE).

195
README.fi.md Normal file
View file

@ -0,0 +1,195 @@
<p align="center">
<a href="README.md">English</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
</p>
<p align="center"><img src="assets/logo.svg" width="112" alt="Open CAD Studion logo"></p>
<h1 align="center">Open CAD Studio</h1>
<p align="center">Avoimen lähdekoodin 2D-piirtäminen ja 3D-mallinnus työpöydälle ja verkkoon, toteutettu Rustilla.</p>
<p align="center">
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><img alt="Uusin julkaisu" src="https://img.shields.io/github/v/release/HakanSeven12/OpenCADStudio"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases"><img alt="Lataukset" src="https://img.shields.io/github/downloads/HakanSeven12/OpenCADStudio/total"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers"><img alt="GitHub-tähdet" src="https://img.shields.io/github/stars/HakanSeven12/OpenCADStudio"></a>
<a href="LICENSE"><img alt="GPL-3.0-lisenssi" src="https://img.shields.io/github/license/HakanSeven12/OpenCADStudio"></a>
</p>
<p align="center">
<a href="https://www.opencadstudio.com"><strong>Käynnistä verkkosovellus</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><strong>Lataa työpöytäsovellus</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/discussions"><strong>Osallistu keskusteluun</strong></a>
</p>
<p align="center"><img src="https://github.com/user-attachments/assets/10635ad0-454b-4c87-935f-1a3a46f24ccb" alt="Open CAD Studion työtila" width="100%"></p>
## Yleiskatsaus
Open CAD Studio on monialustainen sovellus tekniseen piirtämiseen, asettelutyöhön ja solidimallinnukseen. Se lukee ja kirjoittaa DWG- ja DXF-piirustuksia suoraan, ja työpöytä- sekä selainversiot käyttävät samaa muokkausydintä.
Projektia kehitetään aktiivisesti. Säilytä tärkeistä tuotantopiirustuksista varmuuskopiot ja ilmoita toistettavista ongelmista [GitHub Issuesissa](https://github.com/HakanSeven12/OpenCADStudio/issues).
## Kohokohdat
- **Suora piirustusprosessi** — avaa, muokkaa, palauta ja tallenna DWG- ja DXF-tiedostoja ilman muunnospalvelua.
- **Tarkka 2D-piirtäminen** — viivat, murtoviivat, käyrät, splinit, viivoitukset, objektikohdistukset, seuranta, tasot, lohkot ja ulkoiset viitteet.
- **Dokumentointityökalut** — teksti, mitoitukset, johtoviivat, toleranssit, taulukot, mallitila, paperitila, näkymät ja tulostustyylit.
- **Geometriaytimeen perustuva 3D-mallinnus** — solidiprimitiivit, pursotus, pyöräytys, pyyhkäisy, loft, Boolen operaatiot ja ACIS-kohteiden tessellointi.
- **GPU-renderöinti**`wgpu`:n kiihdyttämät 2D- ja 3D-näkymät sekä ortografiset ja perspektiivikamerat.
- **Laajennettavat työnkulut** — natiiviliitännäiset, komentoskriptit, käyttöliittymätön muunnos ja rivipohjainen JSON-automaatiorajapinta.
<p align="center"><img src="https://github.com/user-attachments/assets/2a037a09-e8e8-498c-8ed3-58ecb8ae958d" alt="3D-malli Open CAD Studiossa" width="100%"></p>
## Tiedostotyönkulut
| Muoto tai työnkulku | Tuki |
| --- | --- |
| DWG | Luku ja kirjoitus; versioidut tallennuskohteet R142018 |
| DXF | Luku ja kirjoitus; versioidut tallennuskohteet R142018 |
| BAK / SV$ | Piirustusvarmuuskopioiden ja automaattitallennusten avaaminen |
| OBJ | Monikulmioverkkojen tuonti |
| LandXML | `CgPoint`-mittauspisteiden tuonti |
| STL | 3D-verkkotietojen vienti |
| STEP AP203 | 3D-verkkotietojen vienti |
| PDF | Asettelujen ja valitun geometrian tulostus työpöytäversiossa |
| CSV | Kohteiden ominaisuustietojen poiminta |
| CTB / STB | Tulostustyylitaulukoiden lataus ja muokkaus |
## Työpöytä vai verkko
Käytä [verkkosovellusta](https://www.opencadstudio.com), kun haluat aloittaa heti ilman asennusta. Piirustukset valitaan selaimessa ja tallennetaan paikallisina latauksina.
Käytä työpöytäsovellusta natiiveihin tiedostokytkentöihin, tiedostonhallinnan esikatseluihin, järjestelmätulostukseen, PDF-tulosteisiin, ulkoisiin liitännäisiin, komentoskripteihin ja käyttöliittymättömään automaatioon. Julkaisut ovat saatavilla Windowsille, Linuxille ja Apple Silicon macOS:lle.
## Asennus
Lataa kaikki nykyiset paketit [uusimmasta julkaisusta](https://github.com/HakanSeven12/OpenCADStudio/releases/latest).
### Windows
Valitse allekirjoitettu x86-64-paketti:
- `OpenCADStudio-*-windows-x86_64-installer.msi` — suositeltu asennusohjelma, joka sisältää Käynnistä-valikon pikakuvakkeet, DWG/DXF-tiedostokytkennät ja piirustusten esikatselut.
- `OpenCADStudio-*-windows-x86_64-portable.exe` — itsenäinen sovellus, joka ei vaadi asennusta.
### Linux
Lataa x86-64 AppImage, tee siitä suoritettava ja käynnistä se:
```bash
chmod +x OpenCADStudio-*-linux-x86_64.AppImage
./OpenCADStudio-*-linux-x86_64.AppImage
```
### macOS
Julkaistu macOS-paketti tukee Apple Siliconia:
1. Lataa `OpenCADStudio-*-macos-arm64.dmg`.
2. Avaa levykuva ja vedä `OpenCADStudio.app` kansioon **Applications**.
3. Jos Gatekeeper estää ensimmäisen käynnistyksen, hyväksy sovellus kohdassa **System Settings → Privacy & Security**.
Sovellus on allekirjoitettu ad hoc -allekirjoituksella, mutta Apple ei ole tällä hetkellä notaroinut sitä.
## Kielet
Open CAD Studio voi seurata järjestelmän kieltä tai käyttää jotakin näistä 19 käyttöliittymäkielestä:
> Arabia · Brasilianportugali · Tšekki · Hollanti · Englanti · Suomi · Ranska · Saksa · Hindi · Unkari · Italia · Japani · Korea · Puola · Venäjä · Yksinkertaistettu kiina · Espanja · Perinteinen kiina · Turkki
Vaihda kieli sovelluksen asetuksissa. Selainversio käyttää myös selaimen ensisijaista kielialuetta, kun **Järjestelmä** on valittu.
## Kääntäminen lähdekoodista
### Työpöytä
Vaatimukset:
- Git
- Nykyinen vakaa Rust-työkaluketju
- Alustan grafiikka- ja fonttikehityskirjastot
Asenna natiiviriippuvuudet Ubuntussa tai Debianissa:
```bash
sudo apt update
sudo apt install libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev \
libxrandr-dev libxkbcommon-dev libwayland-dev libfontconfig1-dev \
libfreetype6-dev
```
Käännä sitten:
```bash
git clone https://github.com/HakanSeven12/OpenCADStudio.git
cd OpenCADStudio
cargo build --release --bin OpenCADStudio
```
Tuloksena syntyvä ohjelmatiedosto kirjoitetaan polkuun `target/release/OpenCADStudio` (Windowsissa `OpenCADStudio.exe`).
### Web
Asenna WebAssembly-kohde ja rakennustyökalut kerran:
```bash
rustup target add wasm32-unknown-unknown
cargo install trunk wasm-bindgen-cli
```
Käynnistä kehityspalvelin:
```bash
trunk serve
```
## Automaatio
Työpöytäohjelma tukee kertaluonteista muunnosta ja pysyvää käyttöliittymätöntä palvelinta:
```bash
OpenCADStudio --export input.dwg output.dxf
OpenCADStudio --serve
OpenCADStudio --serve --port 4242
```
Palvelin vaihtaa yhden JSON-objektin riviä kohti vakiotulon/-lähdön tai paikallisen TCP-pistokkeen kautta. Katso [automaatio-opas](docs/automation/README.md) ja mukana toimitettu [Python-asiakas](docs/automation/ocs.py).
## Liitännäiset
Työpöytäliitännäiset suoritetaan erillisissä prosesseissa ja ne viestivät isännän kanssa versioidun liitännäisrajapinnan kautta. Selainversio ei lataa natiiviliitännäisiä.
- [Liitännäisarkkitehtuuri](docs/plugin-architecture.md)
- [Liitännäismalli](docs/plugin-template/README.md)
- [Liitännäisrekisteri](plugins/README.md)
## Projektin dokumentaatio
- [Automaatiorajapinta](docs/automation/README.md)
- [Liitännäisarkkitehtuuri](docs/plugin-architecture.md)
- [Tessellointiputki](docs/tessellation.md)
- [Tietoturvakäytäntö](SECURITY.md)
## Osallistuminen
Virheraportit, rajatut pull requestit, käännökset, dokumentaatioparannukset ja liitännäisosallistuminen ovat tervetulleita.
- Etsi olemassa olevista [issueista](https://github.com/HakanSeven12/OpenCADStudio/issues) ennen uuden raportin avaamista.
- Käytä [Discussions](https://github.com/HakanSeven12/OpenCADStudio/discussions)-osiota kysymyksiin ja ideoihin.
- Ilmoita haavoittuvuuksista yksityisesti [tietoturvakäytännön](SECURITY.md) mukaisesti.
## Projektin kasvu
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://www.opencadstudio.com/star-history-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://www.opencadstudio.com/star-history-light.svg">
<img alt="Open CAD Studion tähdet ja julkaisulataukset" src="https://www.opencadstudio.com/star-history-light.svg">
</picture>
</a>
## Tue projektia
Jos Open CAD Studio auttaa työssäsi, tue jatkokehitystä [GitHub Sponsorsin](https://github.com/sponsors/HakanSeven12) tai [Patreonin](https://www.patreon.com/HakanSeven12) kautta.
## Lisenssi
Open CAD Studioa jaetaan [GNU General Public License v3.0](LICENSE) -lisenssillä.

195
README.fr.md Normal file
View file

@ -0,0 +1,195 @@
<p align="center">
<a href="README.md">English</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
</p>
<p align="center"><img src="assets/logo.svg" width="112" alt="Logo Open CAD Studio"></p>
<h1 align="center">Open CAD Studio</h1>
<p align="center">Dessin 2D et modélisation 3D open source pour ordinateur et web, développés en Rust.</p>
<p align="center">
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><img alt="Dernière version" src="https://img.shields.io/github/v/release/HakanSeven12/OpenCADStudio"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases"><img alt="Téléchargements" src="https://img.shields.io/github/downloads/HakanSeven12/OpenCADStudio/total"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers"><img alt="Étoiles GitHub" src="https://img.shields.io/github/stars/HakanSeven12/OpenCADStudio"></a>
<a href="LICENSE"><img alt="Licence GPL-3.0" src="https://img.shields.io/github/license/HakanSeven12/OpenCADStudio"></a>
</p>
<p align="center">
<a href="https://www.opencadstudio.com"><strong>Lancer lapplication web</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><strong>Télécharger lapplication de bureau</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/discussions"><strong>Participer aux discussions</strong></a>
</p>
<p align="center"><img src="https://github.com/user-attachments/assets/10635ad0-454b-4c87-935f-1a3a46f24ccb" alt="Espace de travail Open CAD Studio" width="100%"></p>
## Présentation
Open CAD Studio est une application multiplateforme destinée au dessin technique, à la mise en page et à la modélisation de solides. Elle lit et écrit nativement les dessins DWG et DXF, avec un même cœur dédition pour les versions bureau et navigateur.
Le projet est en développement actif. Conservez des sauvegardes des dessins de production importants et signalez les problèmes reproductibles dans les [issues GitHub](https://github.com/HakanSeven12/OpenCADStudio/issues).
## Points forts
- **Flux de dessin natif** — ouvrez, modifiez, récupérez et enregistrez les fichiers DWG et DXF sans service de conversion.
- **Dessin 2D précis** — lignes, polylignes, courbes, splines, hachures, accrochages aux objets, repérage, calques, blocs et références externes.
- **Outils de documentation** — texte, cotations, repères, tolérances, tableaux, espace objet, espace papier, fenêtres et styles de tracé.
- **Modélisation 3D avec noyau géométrique** — primitives solides, extrusion, révolution, balayage, lissage, opérations booléennes et tessellation dentités ACIS.
- **Rendu GPU** — vues 2D et 3D accélérées par `wgpu`, avec caméras orthographique et perspective.
- **Flux extensibles** — plugins natifs, scripts de commandes, conversion sans interface et API dautomatisation JSON ligne par ligne.
<p align="center"><img src="https://github.com/user-attachments/assets/2a037a09-e8e8-498c-8ed3-58ecb8ae958d" alt="Modèle 3D dans Open CAD Studio" width="100%"></p>
## Flux de fichiers
| Format ou flux | Prise en charge |
| --- | --- |
| DWG | Lecture et écriture ; cibles denregistrement versionnées de R14 à 2018 |
| DXF | Lecture et écriture ; cibles denregistrement versionnées de R14 à 2018 |
| BAK / SV$ | Ouverture des sauvegardes et fichiers denregistrement automatique |
| OBJ | Importation de maillages polygonaux |
| LandXML | Importation de points topographiques `CgPoint` |
| STL | Exportation de données de maillage 3D |
| STEP AP203 | Exportation de données de maillage 3D |
| PDF | Tracé des présentations et de la géométrie sélectionnée sur ordinateur |
| CSV | Extraction des propriétés des entités |
| CTB / STB | Chargement et modification des tables de styles de tracé |
## Bureau ou web
Utilisez l[application web](https://www.opencadstudio.com) pour un accès immédiat sans installation. Les dessins sont sélectionnés dans le navigateur et enregistrés sous forme de téléchargements locaux.
Utilisez lapplication de bureau pour les associations de fichiers natives, les miniatures du gestionnaire de fichiers, limpression système, la sortie PDF, les plugins externes, les scripts de commandes et lautomatisation sans interface. Des versions sont proposées pour Windows, Linux et macOS Apple Silicon.
## Installation
Téléchargez tous les paquets actuels depuis la [dernière version](https://github.com/HakanSeven12/OpenCADStudio/releases/latest).
### Windows
Choisissez lun des paquets x86-64 signés :
- `OpenCADStudio-*-windows-x86_64-installer.msi` — programme dinstallation recommandé avec raccourcis du menu Démarrer, associations DWG/DXF et miniatures des dessins.
- `OpenCADStudio-*-windows-x86_64-portable.exe` — application autonome, sans installation.
### Linux
Téléchargez lAppImage x86-64, rendez-la exécutable et lancez-la :
```bash
chmod +x OpenCADStudio-*-linux-x86_64.AppImage
./OpenCADStudio-*-linux-x86_64.AppImage
```
### macOS
Le paquet macOS publié prend en charge Apple Silicon :
1. Téléchargez `OpenCADStudio-*-macos-arm64.dmg`.
2. Ouvrez limage et faites glisser `OpenCADStudio.app` vers **Applications**.
3. Si Gatekeeper bloque le premier lancement, autorisez lapplication dans **System Settings → Privacy & Security**.
Lapplication est signée de façon ad hoc, mais nest actuellement pas notariée par Apple.
## Langues
Open CAD Studio peut suivre la langue du système ou utiliser lune de ces 19 langues dinterface :
> Arabe · Portugais du Brésil · Tchèque · Néerlandais · Anglais · Finnois · Français · Allemand · Hindi · Hongrois · Italien · Japonais · Coréen · Polonais · Russe · Chinois simplifié · Espagnol · Chinois traditionnel · Turc
Changez la langue dans les paramètres de lapplication. La version web utilise aussi la langue préférée du navigateur lorsque **Système** est sélectionné.
## Compilation depuis les sources
### Bureau
Prérequis :
- Git
- Chaîne doutils Rust stable actuelle
- Bibliothèques de développement graphiques et de polices de la plateforme
Sous Ubuntu ou Debian, installez les dépendances natives :
```bash
sudo apt update
sudo apt install libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev \
libxrandr-dev libxkbcommon-dev libwayland-dev libfontconfig1-dev \
libfreetype6-dev
```
Compilez ensuite :
```bash
git clone https://github.com/HakanSeven12/OpenCADStudio.git
cd OpenCADStudio
cargo build --release --bin OpenCADStudio
```
Le binaire obtenu est écrit dans `target/release/OpenCADStudio` (`OpenCADStudio.exe` sous Windows).
### Web
Installez une fois la cible WebAssembly et les outils de compilation :
```bash
rustup target add wasm32-unknown-unknown
cargo install trunk wasm-bindgen-cli
```
Lancez le serveur de développement :
```bash
trunk serve
```
## Automatisation
Le binaire de bureau prend en charge la conversion ponctuelle et un serveur persistant sans interface :
```bash
OpenCADStudio --export input.dwg output.dxf
OpenCADStudio --serve
OpenCADStudio --serve --port 4242
```
Le serveur échange un objet JSON par ligne via lentrée/sortie standard ou un socket TCP local. Consultez le [guide dautomatisation](docs/automation/README.md) et le [client Python](docs/automation/ocs.py) fourni.
## Plugins
Les plugins de bureau sexécutent dans des processus séparés et communiquent avec lhôte via lAPI de plugins versionnée. La version navigateur ne charge pas de plugins natifs.
- [Architecture des plugins](docs/plugin-architecture.md)
- [Modèle de plugin](docs/plugin-template/README.md)
- [Registre des plugins](plugins/README.md)
## Documentation du projet
- [API dautomatisation](docs/automation/README.md)
- [Architecture des plugins](docs/plugin-architecture.md)
- [Pipeline de tessellation](docs/tessellation.md)
- [Politique de sécurité](SECURITY.md)
## Contribuer
Les rapports de bogues, pull requests ciblées, traductions, améliorations de documentation et contributions de plugins sont les bienvenus.
- Recherchez dans les [issues](https://github.com/HakanSeven12/OpenCADStudio/issues) existantes avant douvrir un nouveau rapport.
- Utilisez les [Discussions](https://github.com/HakanSeven12/OpenCADStudio/discussions) pour les questions et les idées.
- Signalez les vulnérabilités en privé en suivant la [politique de sécurité](SECURITY.md).
## Croissance du projet
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://www.opencadstudio.com/star-history-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://www.opencadstudio.com/star-history-light.svg">
<img alt="Étoiles et téléchargements dOpen CAD Studio" src="https://www.opencadstudio.com/star-history-light.svg">
</picture>
</a>
## Soutenir le projet
Si Open CAD Studio vous aide dans votre travail, soutenez son développement via [GitHub Sponsors](https://github.com/sponsors/HakanSeven12) ou [Patreon](https://www.patreon.com/HakanSeven12).
## Licence
Open CAD Studio est distribué sous la [Licence publique générale GNU v3.0](LICENSE).

195
README.hi.md Normal file
View file

@ -0,0 +1,195 @@
<p align="center">
<a href="README.md">English</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
</p>
<p align="center"><img src="assets/logo.svg" width="112" alt="Open CAD Studio लोगो"></p>
<h1 align="center">Open CAD Studio</h1>
<p align="center">Rust से निर्मित, डेस्कटॉप और वेब के लिए मुक्त-स्रोत 2D ड्राफ्टिंग और 3D मॉडलिंग अनुप्रयोग।</p>
<p align="center">
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><img alt="नवीनतम रिलीज़" src="https://img.shields.io/github/v/release/HakanSeven12/OpenCADStudio"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases"><img alt="रिलीज़ डाउनलोड" src="https://img.shields.io/github/downloads/HakanSeven12/OpenCADStudio/total"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers"><img alt="GitHub स्टार" src="https://img.shields.io/github/stars/HakanSeven12/OpenCADStudio"></a>
<a href="LICENSE"><img alt="GPL-3.0 लाइसेंस" src="https://img.shields.io/github/license/HakanSeven12/OpenCADStudio"></a>
</p>
<p align="center">
<a href="https://www.opencadstudio.com"><strong>वेब ऐप खोलें</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><strong>डेस्कटॉप ऐप डाउनलोड करें</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/discussions"><strong>चर्चा में शामिल हों</strong></a>
</p>
<p align="center"><img src="https://github.com/user-attachments/assets/10635ad0-454b-4c87-935f-1a3a46f24ccb" alt="Open CAD Studio कार्यक्षेत्र" width="100%"></p>
## परिचय
Open CAD Studio तकनीकी ड्राइंग, लेआउट कार्य और सॉलिड मॉडलिंग के लिए एक क्रॉस-प्लेटफ़ॉर्म अनुप्रयोग है। यह DWG और DXF ड्राइंग को मूल रूप से पढ़ता और लिखता है, तथा डेस्कटॉप और ब्राउज़र संस्करण समान संपादन कोर साझा करते हैं।
परियोजना सक्रिय विकास में है। महत्वपूर्ण उत्पादन ड्राइंग की बैकअप प्रतियाँ रखें और दोहराई जा सकने वाली समस्याएँ [GitHub Issues](https://github.com/HakanSeven12/OpenCADStudio/issues) पर रिपोर्ट करें।
## प्रमुख विशेषताएँ
- **मूल ड्राइंग कार्यप्रवाह** — किसी रूपांतरण सेवा के बिना DWG और DXF फ़ाइलें खोलें, संपादित करें, पुनर्प्राप्त करें और सहेजें।
- **सटीक 2D ड्राफ्टिंग** — रेखाएँ, पॉलीलाइन, वक्र, स्प्लाइन, हैच, ऑब्जेक्ट स्नैप, ट्रैकिंग, लेयर, ब्लॉक और बाहरी संदर्भ।
- **दस्तावेज़ीकरण उपकरण** — टेक्स्ट, आयाम, लीडर, टॉलरेंस, टेबल, मॉडल स्पेस, पेपर स्पेस, व्यूपोर्ट और प्लॉट शैली।
- **ज्यामिति कर्नेल आधारित 3D मॉडलिंग** — सॉलिड प्रिमिटिव, एक्सट्रूज़न, रिवॉल्व, स्वीप, लॉफ्ट, बूलियन ऑपरेशन और ACIS एंटिटी टेसेलेशन।
- **GPU रेंडरिंग**`wgpu` से तेज़ किए गए 2D और 3D व्यूपोर्ट, ऑर्थोग्राफ़िक और पर्सपेक्टिव कैमरा सहित।
- **विस्तार योग्य कार्यप्रवाह** — मूल प्लगइन, कमांड स्क्रिप्ट, हेडलेस रूपांतरण और पंक्ति-आधारित JSON स्वचालन API।
<p align="center"><img src="https://github.com/user-attachments/assets/2a037a09-e8e8-498c-8ed3-58ecb8ae958d" alt="Open CAD Studio में 3D मॉडल" width="100%"></p>
## फ़ाइल कार्यप्रवाह
| प्रारूप या कार्यप्रवाह | समर्थन |
| --- | --- |
| DWG | पढ़ना और लिखना; R14 से 2018 तक संस्करण चुनकर सहेजना |
| DXF | पढ़ना और लिखना; R14 से 2018 तक संस्करण चुनकर सहेजना |
| BAK / SV$ | ड्राइंग बैकअप और स्वतः-सहेजी गई फ़ाइलें खोलना |
| OBJ | पॉलीगॉन मेश आयात करना |
| LandXML | `CgPoint` सर्वेक्षण बिंदु आयात करना |
| STL | 3D मेश डेटा निर्यात करना |
| STEP AP203 | 3D मेश डेटा निर्यात करना |
| PDF | डेस्कटॉप पर लेआउट और चुनी हुई ज्यामिति प्लॉट करना |
| CSV | एंटिटी गुणों का डेटा निकालना |
| CTB / STB | प्लॉट शैली टेबल लोड और संपादित करना |
## डेस्कटॉप या वेब
बिना स्थापना तुरंत शुरू करने के लिए [वेब ऐप](https://www.opencadstudio.com) का उपयोग करें। ड्राइंग ब्राउज़र से चुनी जाती हैं और स्थानीय डाउनलोड के रूप में सहेजी जाती हैं।
मूल फ़ाइल संबद्धता, फ़ाइल मैनेजर थंबनेल, सिस्टम प्रिंटिंग, PDF आउटपुट, बाहरी प्लगइन, कमांड स्क्रिप्ट और हेडलेस स्वचालन के लिए डेस्कटॉप अनुप्रयोग उपयोग करें। Windows, Linux और Apple Silicon macOS के लिए रिलीज़ उपलब्ध हैं।
## स्थापना
सभी मौजूदा पैकेज [नवीनतम रिलीज़](https://github.com/HakanSeven12/OpenCADStudio/releases/latest) से डाउनलोड करें।
### Windows
इन हस्ताक्षरित x86-64 पैकेजों में से एक चुनें:
- `OpenCADStudio-*-windows-x86_64-installer.msi` — Start Menu शॉर्टकट, DWG/DXF फ़ाइल संबद्धता और ड्राइंग थंबनेल वाला अनुशंसित इंस्टॉलर।
- `OpenCADStudio-*-windows-x86_64-portable.exe` — स्वतंत्र अनुप्रयोग; स्थापना आवश्यक नहीं।
### Linux
x86-64 AppImage डाउनलोड करें, उसे निष्पादन योग्य बनाएँ और चलाएँ:
```bash
chmod +x OpenCADStudio-*-linux-x86_64.AppImage
./OpenCADStudio-*-linux-x86_64.AppImage
```
### macOS
प्रकाशित macOS पैकेज Apple Silicon का समर्थन करता है:
1. `OpenCADStudio-*-macos-arm64.dmg` डाउनलोड करें।
2. इमेज खोलें और `OpenCADStudio.app` को **Applications** में खींचें।
3. यदि Gatekeeper पहली बार चलाने से रोके, तो **System Settings → Privacy & Security** में ऐप को अनुमति दें।
अनुप्रयोग पर ad hoc हस्ताक्षर हैं, लेकिन वर्तमान में Apple ने इसे नोटराइज़ नहीं किया है।
## भाषाएँ
Open CAD Studio सिस्टम भाषा का अनुसरण कर सकता है या इन 19 इंटरफ़ेस भाषाओं में से किसी एक का उपयोग कर सकता है:
> अरबी · ब्राज़ीलियाई पुर्तगाली · चेक · डच · अंग्रेज़ी · फ़िनिश · फ़्रेंच · जर्मन · हिन्दी · हंगेरियन · इतालवी · जापानी · कोरियाई · पोलिश · रूसी · सरलीकृत चीनी · स्पैनिश · पारंपरिक चीनी · तुर्की
अनुप्रयोग सेटिंग में भाषा बदलें। **सिस्टम** चुने जाने पर ब्राउज़र संस्करण भी ब्राउज़र की पसंदीदा लोकेल का उपयोग करता है।
## स्रोत से निर्माण
### डेस्कटॉप
आवश्यकताएँ:
- Git
- वर्तमान स्थिर Rust टूलचेन
- प्लेटफ़ॉर्म की ग्राफ़िक्स और फ़ॉन्ट विकास लाइब्रेरी
Ubuntu या Debian पर मूल निर्भरताएँ स्थापित करें:
```bash
sudo apt update
sudo apt install libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev \
libxrandr-dev libxkbcommon-dev libwayland-dev libfontconfig1-dev \
libfreetype6-dev
```
फिर निर्माण करें:
```bash
git clone https://github.com/HakanSeven12/OpenCADStudio.git
cd OpenCADStudio
cargo build --release --bin OpenCADStudio
```
तैयार बाइनरी `target/release/OpenCADStudio` में लिखी जाती है (Windows पर `OpenCADStudio.exe`)।
### वेब
WebAssembly लक्ष्य और निर्माण उपकरण एक बार स्थापित करें:
```bash
rustup target add wasm32-unknown-unknown
cargo install trunk wasm-bindgen-cli
```
डेवलपमेंट सर्वर शुरू करें:
```bash
trunk serve
```
## स्वचालन
डेस्कटॉप बाइनरी एक बार के रूपांतरण और लगातार चलने वाले हेडलेस सर्वर का समर्थन करती है:
```bash
OpenCADStudio --export input.dwg output.dxf
OpenCADStudio --serve
OpenCADStudio --serve --port 4242
```
सर्वर मानक इनपुट/आउटपुट या स्थानीय TCP सॉकेट के माध्यम से प्रति पंक्ति एक JSON ऑब्जेक्ट का आदान-प्रदान करता है। [स्वचालन मार्गदर्शिका](docs/automation/README.md) और शामिल [Python क्लाइंट](docs/automation/ocs.py) देखें।
## प्लगइन
डेस्कटॉप प्लगइन अलग प्रक्रियाओं में चलते हैं और संस्करणित प्लगइन API के माध्यम से होस्ट से संवाद करते हैं। ब्राउज़र बिल्ड मूल प्लगइन लोड नहीं करता।
- [प्लगइन संरचना](docs/plugin-architecture.md)
- [प्लगइन टेम्पलेट](docs/plugin-template/README.md)
- [प्लगइन रजिस्ट्री](plugins/README.md)
## परियोजना दस्तावेज़
- [स्वचालन API](docs/automation/README.md)
- [प्लगइन संरचना](docs/plugin-architecture.md)
- [टेसेलेशन पाइपलाइन](docs/tessellation.md)
- [सुरक्षा नीति](SECURITY.md)
## योगदान
बग रिपोर्ट, केंद्रित pull request, अनुवाद, दस्तावेज़ सुधार और प्लगइन योगदान का स्वागत है।
- नई रिपोर्ट खोलने से पहले मौजूदा [issues](https://github.com/HakanSeven12/OpenCADStudio/issues) खोजें।
- प्रश्नों और विचारों के लिए [Discussions](https://github.com/HakanSeven12/OpenCADStudio/discussions) उपयोग करें।
- [सुरक्षा नीति](SECURITY.md) के अनुसार कमजोरियों की निजी रूप से रिपोर्ट करें।
## परियोजना की वृद्धि
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://www.opencadstudio.com/star-history-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://www.opencadstudio.com/star-history-light.svg">
<img alt="Open CAD Studio स्टार और रिलीज़ डाउनलोड" src="https://www.opencadstudio.com/star-history-light.svg">
</picture>
</a>
## परियोजना का समर्थन करें
यदि Open CAD Studio आपके काम में सहायक है, तो [GitHub Sponsors](https://github.com/sponsors/HakanSeven12) या [Patreon](https://www.patreon.com/HakanSeven12) के माध्यम से इसके निरंतर विकास का समर्थन करें।
## लाइसेंस
Open CAD Studio को [GNU General Public License v3.0](LICENSE) के अंतर्गत वितरित किया जाता है।

195
README.hu.md Normal file
View file

@ -0,0 +1,195 @@
<p align="center">
<a href="README.md">English</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
</p>
<p align="center"><img src="assets/logo.svg" width="112" alt="Open CAD Studio logó"></p>
<h1 align="center">Open CAD Studio</h1>
<p align="center">Nyílt forráskódú 2D rajzolás és 3D modellezés asztali gépre és webre, Rust nyelven.</p>
<p align="center">
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><img alt="Legújabb kiadás" src="https://img.shields.io/github/v/release/HakanSeven12/OpenCADStudio"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases"><img alt="Letöltések" src="https://img.shields.io/github/downloads/HakanSeven12/OpenCADStudio/total"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers"><img alt="GitHub-csillagok" src="https://img.shields.io/github/stars/HakanSeven12/OpenCADStudio"></a>
<a href="LICENSE"><img alt="GPL-3.0 licenc" src="https://img.shields.io/github/license/HakanSeven12/OpenCADStudio"></a>
</p>
<p align="center">
<a href="https://www.opencadstudio.com"><strong>Webalkalmazás indítása</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><strong>Asztali alkalmazás letöltése</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/discussions"><strong>Csatlakozás a beszélgetéshez</strong></a>
</p>
<p align="center"><img src="https://github.com/user-attachments/assets/10635ad0-454b-4c87-935f-1a3a46f24ccb" alt="Open CAD Studio munkaterület" width="100%"></p>
## Áttekintés
Az Open CAD Studio többplatformos alkalmazás műszaki rajzoláshoz, elrendezések készítéséhez és testmodellezéshez. Natívan olvas és ír DWG- és DXF-rajzokat; az asztali és böngészős változat közös szerkesztőmagot használ.
A projekt aktív fejlesztés alatt áll. A fontos gyártási rajzokról tarts biztonsági másolatot, a reprodukálható hibákat pedig jelentsd a [GitHub Issues](https://github.com/HakanSeven12/OpenCADStudio/issues) oldalon.
## Főbb jellemzők
- **Natív rajzi munkafolyamat** — DWG- és DXF-fájlok megnyitása, szerkesztése, helyreállítása és mentése átalakító szolgáltatás nélkül.
- **Pontos 2D rajzolás** — vonalak, vonalláncok, görbék, spline-ok, sraffozások, tárgyraszterek, követés, rétegek, blokkok és külső referenciák.
- **Dokumentációs eszközök** — szöveg, méretezés, mutatóvonalak, tűrések, táblázatok, modelltér, papírtér, nézetablakok és nyomtatási stílusok.
- **Kernelalapú 3D modellezés** — testprimitívek, kihúzás, forgatás, söprés, loft, logikai műveletek és ACIS-entitások tesszellációja.
- **GPU-megjelenítés**`wgpu` által gyorsított 2D és 3D nézetek, ortografikus és perspektivikus kamerákkal.
- **Bővíthető munkafolyamatok** — natív bővítmények, parancsfájlok, felület nélküli konverzió és soralapú JSON automatizálási API.
<p align="center"><img src="https://github.com/user-attachments/assets/2a037a09-e8e8-498c-8ed3-58ecb8ae958d" alt="3D modell az Open CAD Studióban" width="100%"></p>
## Fájlmunkafolyamatok
| Formátum vagy munkafolyamat | Támogatás |
| --- | --- |
| DWG | Olvasás és írás; verziózott mentési célok R14-től 2018-ig |
| DXF | Olvasás és írás; verziózott mentési célok R14-től 2018-ig |
| BAK / SV$ | Rajzi biztonsági másolatok és automatikus mentések megnyitása |
| OBJ | Poligonhálók importálása |
| LandXML | `CgPoint` felmérési pontok importálása |
| STL | 3D hálóadatok exportálása |
| STEP AP203 | 3D hálóadatok exportálása |
| PDF | Elrendezések és kijelölt geometria nyomtatása asztali gépen |
| CSV | Entitástulajdonságok adatainak kinyerése |
| CTB / STB | Nyomtatásistílus-táblák betöltése és szerkesztése |
## Asztali vagy webes változat
Használd a [webalkalmazást](https://www.opencadstudio.com) azonnali, telepítés nélküli hozzáféréshez. A rajzok a böngészőben választhatók ki és helyi letöltésként menthetők.
Az asztali alkalmazást válaszd natív fájltársításokhoz, fájlkezelői bélyegképekhez, rendszernyomtatáshoz, PDF-kimenethez, külső bővítményekhez, parancsfájlokhoz és felület nélküli automatizáláshoz. Windows, Linux és Apple Silicon macOS rendszerhez érhetők el kiadások.
## Telepítés
Minden aktuális csomag a [legújabb kiadásból](https://github.com/HakanSeven12/OpenCADStudio/releases/latest) tölthető le.
### Windows
Válassz az aláírt x86-64 csomagok közül:
- `OpenCADStudio-*-windows-x86_64-installer.msi` — ajánlott telepítő Start menü-parancsikonokkal, DWG/DXF-fájltársításokkal és rajzi bélyegképekkel.
- `OpenCADStudio-*-windows-x86_64-portable.exe` — önálló alkalmazás, telepítés nélkül.
### Linux
Töltsd le az x86-64 AppImage fájlt, tedd futtathatóvá, majd indítsd el:
```bash
chmod +x OpenCADStudio-*-linux-x86_64.AppImage
./OpenCADStudio-*-linux-x86_64.AppImage
```
### macOS
A kiadott macOS-csomag az Apple Silicon rendszereket támogatja:
1. Töltsd le az `OpenCADStudio-*-macos-arm64.dmg` fájlt.
2. Nyisd meg a lemezképet, és húzd az `OpenCADStudio.app` alkalmazást az **Applications** mappába.
3. Ha a Gatekeeper blokkolja az első indítást, engedélyezd az alkalmazást a **System Settings → Privacy & Security** alatt.
Az alkalmazás ad hoc aláírással rendelkezik, de az Apple jelenleg nem hitelesítette közjegyzői eljárással.
## Nyelvek
Az Open CAD Studio követheti a rendszer nyelvét, vagy használhatja az alábbi 19 felületi nyelv egyikét:
> Arab · Brazil portugál · Cseh · Holland · Angol · Finn · Francia · Német · Hindi · Magyar · Olasz · Japán · Koreai · Lengyel · Orosz · Egyszerűsített kínai · Spanyol · Hagyományos kínai · Török
A nyelv az alkalmazás beállításaiban módosítható. **Rendszer** választásakor a böngészős változat is a böngésző előnyben részesített területi beállítását használja.
## Fordítás forráskódból
### Asztali alkalmazás
Követelmények:
- Git
- Aktuális stabil Rust-eszközlánc
- A platform grafikai és betűkészlet-fejlesztő könyvtárai
Ubuntu vagy Debian alatt telepítsd a natív függőségeket:
```bash
sudo apt update
sudo apt install libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev \
libxrandr-dev libxkbcommon-dev libwayland-dev libfontconfig1-dev \
libfreetype6-dev
```
Ezután fordítsd le:
```bash
git clone https://github.com/HakanSeven12/OpenCADStudio.git
cd OpenCADStudio
cargo build --release --bin OpenCADStudio
```
A létrejövő bináris a `target/release/OpenCADStudio` helyre kerül (Windows alatt `OpenCADStudio.exe`).
### Web
Telepítsd egyszer a WebAssembly célt és a fordítóeszközöket:
```bash
rustup target add wasm32-unknown-unknown
cargo install trunk wasm-bindgen-cli
```
Indítsd el a fejlesztői kiszolgálót:
```bash
trunk serve
```
## Automatizálás
Az asztali bináris egyszeri konverziót és tartós, felület nélküli kiszolgálót támogat:
```bash
OpenCADStudio --export input.dwg output.dxf
OpenCADStudio --serve
OpenCADStudio --serve --port 4242
```
A kiszolgáló soronként egy JSON-objektumot cserél a szabványos bemeneten/kimeneten vagy helyi TCP-foglalaton. Lásd az [automatizálási útmutatót](docs/automation/README.md) és a mellékelt [Python-klienst](docs/automation/ocs.py).
## Bővítmények
Az asztali bővítmények külön folyamatokban futnak, és a verziózott bővítmény-API-n keresztül kommunikálnak a gazdával. A böngészős változat nem tölt be natív bővítményeket.
- [Bővítményarchitektúra](docs/plugin-architecture.md)
- [Bővítménysablon](docs/plugin-template/README.md)
- [Bővítményjegyzék](plugins/README.md)
## Projektdokumentáció
- [Automatizálási API](docs/automation/README.md)
- [Bővítményarchitektúra](docs/plugin-architecture.md)
- [Tesszellációs folyamat](docs/tessellation.md)
- [Biztonsági szabályzat](SECURITY.md)
## Közreműködés
Hibajelentéseket, célzott pull requesteket, fordításokat, dokumentációfejlesztést és bővítmény-hozzájárulásokat egyaránt várunk.
- Új jelentés előtt keress a meglévő [issue-k](https://github.com/HakanSeven12/OpenCADStudio/issues) között.
- Kérdésekhez és ötletekhez használd a [Discussions](https://github.com/HakanSeven12/OpenCADStudio/discussions) oldalt.
- A sérülékenységeket a [biztonsági szabályzat](SECURITY.md) szerint, bizalmasan jelentsd.
## A projekt növekedése
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://www.opencadstudio.com/star-history-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://www.opencadstudio.com/star-history-light.svg">
<img alt="Open CAD Studio csillagok és kiadásletöltések" src="https://www.opencadstudio.com/star-history-light.svg">
</picture>
</a>
## A projekt támogatása
Ha az Open CAD Studio segíti a munkádat, támogasd a további fejlesztést a [GitHub Sponsors](https://github.com/sponsors/HakanSeven12) vagy a [Patreon](https://www.patreon.com/HakanSeven12) oldalán.
## Licenc
Az Open CAD Studio a [GNU General Public License v3.0](LICENSE) alatt kerül terjesztésre.

195
README.it.md Normal file
View file

@ -0,0 +1,195 @@
<p align="center">
<a href="README.md">English</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
</p>
<p align="center"><img src="assets/logo.svg" width="112" alt="Logo di Open CAD Studio"></p>
<h1 align="center">Open CAD Studio</h1>
<p align="center">Disegno 2D e modellazione 3D open source per desktop e web, sviluppati in Rust.</p>
<p align="center">
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><img alt="Ultima versione" src="https://img.shields.io/github/v/release/HakanSeven12/OpenCADStudio"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases"><img alt="Download" src="https://img.shields.io/github/downloads/HakanSeven12/OpenCADStudio/total"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers"><img alt="Stelle GitHub" src="https://img.shields.io/github/stars/HakanSeven12/OpenCADStudio"></a>
<a href="LICENSE"><img alt="Licenza GPL-3.0" src="https://img.shields.io/github/license/HakanSeven12/OpenCADStudio"></a>
</p>
<p align="center">
<a href="https://www.opencadstudio.com"><strong>Avvia lapp web</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><strong>Scarica lapp desktop</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/discussions"><strong>Partecipa alla discussione</strong></a>
</p>
<p align="center"><img src="https://github.com/user-attachments/assets/10635ad0-454b-4c87-935f-1a3a46f24ccb" alt="Area di lavoro di Open CAD Studio" width="100%"></p>
## Panoramica
Open CAD Studio è unapplicazione multipiattaforma per disegno tecnico, impaginazione e modellazione solida. Legge e scrive nativamente disegni DWG e DXF, usando lo stesso nucleo di modifica nelle versioni desktop e browser.
Il progetto è in sviluppo attivo. Conserva copie di sicurezza dei disegni di produzione importanti e segnala i problemi riproducibili tramite [GitHub Issues](https://github.com/HakanSeven12/OpenCADStudio/issues).
## Funzionalità principali
- **Flusso di disegno nativo** — apri, modifica, recupera e salva file DWG e DXF senza servizi di conversione.
- **Disegno 2D preciso** — linee, polilinee, curve, spline, tratteggi, snap a oggetto, tracciamento, layer, blocchi e riferimenti esterni.
- **Strumenti di documentazione** — testo, quote, direttrici, tolleranze, tabelle, spazio modello, spazio carta, finestre e stili di stampa.
- **Modellazione 3D basata su kernel** — primitive solide, estrusione, rivoluzione, sweep, loft, operazioni booleane e tassellazione di entità ACIS.
- **Rendering GPU** — viste 2D e 3D accelerate tramite `wgpu`, con telecamere ortografiche e prospettiche.
- **Flussi estensibili** — plugin nativi, script di comandi, conversione headless e API di automazione JSON basata su righe.
<p align="center"><img src="https://github.com/user-attachments/assets/2a037a09-e8e8-498c-8ed3-58ecb8ae958d" alt="Modello 3D in Open CAD Studio" width="100%"></p>
## Flussi dei file
| Formato o flusso | Supporto |
| --- | --- |
| DWG | Lettura e scrittura; destinazioni di salvataggio versionate da R14 a 2018 |
| DXF | Lettura e scrittura; destinazioni di salvataggio versionate da R14 a 2018 |
| BAK / SV$ | Apertura di backup e file di salvataggio automatico |
| OBJ | Importazione di mesh poligonali |
| LandXML | Importazione di punti di rilievo `CgPoint` |
| STL | Esportazione di dati mesh 3D |
| STEP AP203 | Esportazione di dati mesh 3D |
| PDF | Stampa di layout e geometria selezionata sul desktop |
| CSV | Estrazione dei dati delle proprietà delle entità |
| CTB / STB | Caricamento e modifica delle tabelle degli stili di stampa |
## Desktop o web
Usa l[app web](https://www.opencadstudio.com) per laccesso immediato senza installazione. I disegni vengono selezionati nel browser e salvati come download locali.
Usa lapplicazione desktop per associazioni file native, anteprime nel gestore file, stampa di sistema, output PDF, plugin esterni, script di comandi e automazione headless. Sono disponibili build per Windows, Linux e macOS Apple Silicon.
## Installazione
Scarica tutti i pacchetti aggiornati dall[ultima versione](https://github.com/HakanSeven12/OpenCADStudio/releases/latest).
### Windows
Scegli uno di questi pacchetti x86-64 firmati:
- `OpenCADStudio-*-windows-x86_64-installer.msi` — programma di installazione consigliato con collegamenti nel menu Start, associazioni DWG/DXF e anteprime dei disegni.
- `OpenCADStudio-*-windows-x86_64-portable.exe` — applicazione autonoma, senza installazione.
### Linux
Scarica lAppImage x86-64, rendila eseguibile e avviala:
```bash
chmod +x OpenCADStudio-*-linux-x86_64.AppImage
./OpenCADStudio-*-linux-x86_64.AppImage
```
### macOS
Il pacchetto macOS pubblicato supporta Apple Silicon:
1. Scarica `OpenCADStudio-*-macos-arm64.dmg`.
2. Apri limmagine e trascina `OpenCADStudio.app` in **Applications**.
3. Se Gatekeeper blocca il primo avvio, autorizza lapp da **System Settings → Privacy & Security**.
Lapplicazione è firmata ad hoc, ma attualmente non è autenticata da Apple.
## Lingue
Open CAD Studio può seguire la lingua di sistema o usare una di queste 19 lingue dellinterfaccia:
> Arabo · Portoghese brasiliano · Ceco · Olandese · Inglese · Finlandese · Francese · Tedesco · Hindi · Ungherese · Italiano · Giapponese · Coreano · Polacco · Russo · Cinese semplificato · Spagnolo · Cinese tradizionale · Turco
Cambia la lingua nelle impostazioni dellapplicazione. La versione browser usa anche la lingua preferita del browser quando è selezionato **Sistema**.
## Compilazione dal sorgente
### Desktop
Requisiti:
- Git
- Toolchain Rust stabile corrente
- Librerie di sviluppo grafiche e dei font della piattaforma
Su Ubuntu o Debian installa le dipendenze native:
```bash
sudo apt update
sudo apt install libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev \
libxrandr-dev libxkbcommon-dev libwayland-dev libfontconfig1-dev \
libfreetype6-dev
```
Poi compila:
```bash
git clone https://github.com/HakanSeven12/OpenCADStudio.git
cd OpenCADStudio
cargo build --release --bin OpenCADStudio
```
Il binario viene scritto in `target/release/OpenCADStudio` (`OpenCADStudio.exe` su Windows).
### Web
Installa una volta il target WebAssembly e gli strumenti di compilazione:
```bash
rustup target add wasm32-unknown-unknown
cargo install trunk wasm-bindgen-cli
```
Avvia il server di sviluppo:
```bash
trunk serve
```
## Automazione
Il binario desktop supporta conversioni singole e un server headless persistente:
```bash
OpenCADStudio --export input.dwg output.dxf
OpenCADStudio --serve
OpenCADStudio --serve --port 4242
```
Il server scambia un oggetto JSON per riga tramite ingresso/uscita standard o un socket TCP locale. Consulta la [guida allautomazione](docs/automation/README.md) e il [client Python](docs/automation/ocs.py) incluso.
## Plugin
I plugin desktop vengono eseguiti in processi separati e comunicano con lhost tramite lAPI dei plugin versionata. La build per browser non carica plugin nativi.
- [Architettura dei plugin](docs/plugin-architecture.md)
- [Modello di plugin](docs/plugin-template/README.md)
- [Registro dei plugin](plugins/README.md)
## Documentazione del progetto
- [API di automazione](docs/automation/README.md)
- [Architettura dei plugin](docs/plugin-architecture.md)
- [Pipeline di tassellazione](docs/tessellation.md)
- [Politica di sicurezza](SECURITY.md)
## Contribuire
Segnalazioni di bug, pull request mirate, traduzioni, miglioramenti alla documentazione e contributi ai plugin sono benvenuti.
- Cerca nelle [issue](https://github.com/HakanSeven12/OpenCADStudio/issues) esistenti prima di aprire una nuova segnalazione.
- Usa [Discussions](https://github.com/HakanSeven12/OpenCADStudio/discussions) per domande e idee.
- Segnala le vulnerabilità privatamente seguendo la [politica di sicurezza](SECURITY.md).
## Crescita del progetto
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://www.opencadstudio.com/star-history-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://www.opencadstudio.com/star-history-light.svg">
<img alt="Stelle e download delle versioni di Open CAD Studio" src="https://www.opencadstudio.com/star-history-light.svg">
</picture>
</a>
## Sostieni il progetto
Se Open CAD Studio ti aiuta nel lavoro, sostieni lo sviluppo tramite [GitHub Sponsors](https://github.com/sponsors/HakanSeven12) o [Patreon](https://www.patreon.com/HakanSeven12).
## Licenza
Open CAD Studio è distribuito con la [GNU General Public License v3.0](LICENSE).

195
README.ja.md Normal file
View file

@ -0,0 +1,195 @@
<p align="center">
<a href="README.md">English</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
</p>
<p align="center"><img src="assets/logo.svg" width="112" alt="Open CAD Studio ロゴ"></p>
<h1 align="center">Open CAD Studio</h1>
<p align="center">Rust で開発された、デスクトップと Web 向けのオープンソース 2D 製図・3D モデリングアプリケーション。</p>
<p align="center">
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><img alt="最新リリース" src="https://img.shields.io/github/v/release/HakanSeven12/OpenCADStudio"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases"><img alt="ダウンロード数" src="https://img.shields.io/github/downloads/HakanSeven12/OpenCADStudio/total"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers"><img alt="GitHub スター" src="https://img.shields.io/github/stars/HakanSeven12/OpenCADStudio"></a>
<a href="LICENSE"><img alt="GPL-3.0 ライセンス" src="https://img.shields.io/github/license/HakanSeven12/OpenCADStudio"></a>
</p>
<p align="center">
<a href="https://www.opencadstudio.com"><strong>Web アプリを起動</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><strong>デスクトップアプリをダウンロード</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/discussions"><strong>ディスカッションに参加</strong></a>
</p>
<p align="center"><img src="https://github.com/user-attachments/assets/10635ad0-454b-4c87-935f-1a3a46f24ccb" alt="Open CAD Studio ワークスペース" width="100%"></p>
## 概要
Open CAD Studio は、技術製図、レイアウト作業、ソリッドモデリングのためのクロスプラットフォームアプリケーションです。DWG および DXF 図面をネイティブに読み書きし、デスクトップ版とブラウザ版で共通の編集コアを使用します。
本プロジェクトは活発に開発されています。重要な実務図面はバックアップを保存し、再現可能な問題は [GitHub Issues](https://github.com/HakanSeven12/OpenCADStudio/issues) で報告してください。
## 主な特長
- **ネイティブ図面ワークフロー** — 変換サービスを使わず、DWG/DXF ファイルを開く、編集する、復旧する、保存することができます。
- **高精度な 2D 製図** — 線分、ポリライン、曲線、スプライン、ハッチング、オブジェクトスナップ、トラッキング、レイヤー、ブロック、外部参照。
- **ドキュメント作成ツール** — テキスト、寸法、引出線、公差、表、モデル空間、ペーパー空間、ビューポート、印刷スタイル。
- **カーネルベースの 3D モデリング** — ソリッドプリミティブ、押し出し、回転、スイープ、ロフト、ブーリアン演算、ACIS エンティティのテッセレーション。
- **GPU レンダリング**`wgpu` による高速な 2D/3D ビューポートと、正投影・透視投影カメラ。
- **拡張可能なワークフロー** — ネイティブプラグイン、コマンドスクリプト、ヘッドレス変換、行単位の JSON 自動化 API。
<p align="center"><img src="https://github.com/user-attachments/assets/2a037a09-e8e8-498c-8ed3-58ecb8ae958d" alt="Open CAD Studio の 3D モデル" width="100%"></p>
## ファイルワークフロー
| 形式またはワークフロー | 対応内容 |
| --- | --- |
| DWG | 読み書き。R14 から 2018 までのバージョンを指定して保存 |
| DXF | 読み書き。R14 から 2018 までのバージョンを指定して保存 |
| BAK / SV$ | 図面バックアップと自動保存ファイルを開く |
| OBJ | ポリゴンメッシュをインポート |
| LandXML | `CgPoint` 測量点をインポート |
| STL | 3D メッシュデータをエクスポート |
| STEP AP203 | 3D メッシュデータをエクスポート |
| PDF | デスクトップ版でレイアウトと選択ジオメトリを出力 |
| CSV | エンティティのプロパティデータを抽出 |
| CTB / STB | 印刷スタイルテーブルを読み込み、編集 |
## デスクトップ版と Web 版
インストールせずにすぐ使うには [Web アプリ](https://www.opencadstudio.com) を利用してください。図面はブラウザで選択し、ローカルダウンロードとして保存されます。
ネイティブなファイル関連付け、ファイルマネージャーのサムネイル、システム印刷、PDF 出力、外部プラグイン、コマンドスクリプト、ヘッドレス自動化にはデスクトップ版を使用してください。Windows、Linux、Apple Silicon macOS 向けのリリースがあります。
## インストール
現在のすべてのパッケージは [最新リリース](https://github.com/HakanSeven12/OpenCADStudio/releases/latest) からダウンロードできます。
### Windows
署名済み x86-64 パッケージのいずれかを選択します。
- `OpenCADStudio-*-windows-x86_64-installer.msi` — スタートメニューのショートカット、DWG/DXF 関連付け、図面サムネイルを含む推奨インストーラー。
- `OpenCADStudio-*-windows-x86_64-portable.exe` — インストール不要のスタンドアロンアプリケーション。
### Linux
x86-64 AppImage をダウンロードし、実行可能にして起動します。
```bash
chmod +x OpenCADStudio-*-linux-x86_64.AppImage
./OpenCADStudio-*-linux-x86_64.AppImage
```
### macOS
公開されている macOS パッケージは Apple Silicon に対応しています。
1. `OpenCADStudio-*-macos-arm64.dmg` をダウンロードします。
2. イメージを開き、`OpenCADStudio.app`**Applications** にドラッグします。
3. Gatekeeper が初回起動をブロックした場合は、**System Settings → Privacy & Security** でアプリを許可します。
アプリケーションは ad hoc 署名されていますが、現在 Apple の公証は受けていません。
## 言語
Open CAD Studio はシステム言語に従うか、次の 19 のインターフェース言語を使用できます。
> アラビア語 · ブラジルポルトガル語 · チェコ語 · オランダ語 · 英語 · フィンランド語 · フランス語 · ドイツ語 · ヒンディー語 · ハンガリー語 · イタリア語 · 日本語 · 韓国語 · ポーランド語 · ロシア語 · 簡体字中国語 · スペイン語 · 繁体字中国語 · トルコ語
言語はアプリケーション設定から変更できます。**システム** を選ぶと、ブラウザ版もブラウザの優先ロケールを使用します。
## ソースからビルド
### デスクトップ
必要なもの:
- Git
- 現在の安定版 Rust ツールチェーン
- 各プラットフォームのグラフィックスおよびフォント開発ライブラリ
Ubuntu または Debian では、ネイティブ依存関係をインストールします。
```bash
sudo apt update
sudo apt install libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev \
libxrandr-dev libxkbcommon-dev libwayland-dev libfontconfig1-dev \
libfreetype6-dev
```
続いてビルドします。
```bash
git clone https://github.com/HakanSeven12/OpenCADStudio.git
cd OpenCADStudio
cargo build --release --bin OpenCADStudio
```
生成されたバイナリは `target/release/OpenCADStudio` に保存されますWindows では `OpenCADStudio.exe`)。
### Web
WebAssembly ターゲットとビルドツールを一度インストールします。
```bash
rustup target add wasm32-unknown-unknown
cargo install trunk wasm-bindgen-cli
```
開発サーバーを起動します。
```bash
trunk serve
```
## 自動化
デスクトップバイナリは、1 回限りの変換と常駐ヘッドレスサーバーに対応しています。
```bash
OpenCADStudio --export input.dwg output.dxf
OpenCADStudio --serve
OpenCADStudio --serve --port 4242
```
サーバーは標準入出力またはローカル TCP ソケットを通じ、1 行につき 1 個の JSON オブジェクトを交換します。[自動化ガイド](docs/automation/README.md) と付属の [Python クライアント](docs/automation/ocs.py) を参照してください。
## プラグイン
デスクトッププラグインは別プロセスで動作し、バージョン管理されたプラグイン API を介してホストと通信します。ブラウザ版はネイティブプラグインを読み込みません。
- [プラグインアーキテクチャ](docs/plugin-architecture.md)
- [プラグインテンプレート](docs/plugin-template/README.md)
- [プラグインレジストリ](plugins/README.md)
## プロジェクト文書
- [自動化 API](docs/automation/README.md)
- [プラグインアーキテクチャ](docs/plugin-architecture.md)
- [テッセレーションパイプライン](docs/tessellation.md)
- [セキュリティポリシー](SECURITY.md)
## コントリビューション
バグ報告、目的を絞った pull request、翻訳、文書の改善、プラグインへの貢献を歓迎します。
- 新しい報告を作成する前に既存の [issues](https://github.com/HakanSeven12/OpenCADStudio/issues) を検索してください。
- 質問やアイデアには [Discussions](https://github.com/HakanSeven12/OpenCADStudio/discussions) を利用してください。
- 脆弱性は [セキュリティポリシー](SECURITY.md) に従って非公開で報告してください。
## プロジェクトの成長
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://www.opencadstudio.com/star-history-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://www.opencadstudio.com/star-history-light.svg">
<img alt="Open CAD Studio のスター数とリリースダウンロード数" src="https://www.opencadstudio.com/star-history-light.svg">
</picture>
</a>
## プロジェクトを支援
Open CAD Studio が作業に役立つ場合は、[GitHub Sponsors](https://github.com/sponsors/HakanSeven12) または [Patreon](https://www.patreon.com/HakanSeven12) で継続的な開発をご支援ください。
## ライセンス
Open CAD Studio は [GNU General Public License v3.0](LICENSE) の下で配布されています。

195
README.ko.md Normal file
View file

@ -0,0 +1,195 @@
<p align="center">
<a href="README.md">English</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
</p>
<p align="center"><img src="assets/logo.svg" width="112" alt="Open CAD Studio 로고"></p>
<h1 align="center">Open CAD Studio</h1>
<p align="center">Rust로 개발한 데스크톱 및 웹용 오픈 소스 2D 제도·3D 모델링 애플리케이션입니다.</p>
<p align="center">
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><img alt="최신 릴리스" src="https://img.shields.io/github/v/release/HakanSeven12/OpenCADStudio"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases"><img alt="릴리스 다운로드" src="https://img.shields.io/github/downloads/HakanSeven12/OpenCADStudio/total"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers"><img alt="GitHub 스타" src="https://img.shields.io/github/stars/HakanSeven12/OpenCADStudio"></a>
<a href="LICENSE"><img alt="GPL-3.0 라이선스" src="https://img.shields.io/github/license/HakanSeven12/OpenCADStudio"></a>
</p>
<p align="center">
<a href="https://www.opencadstudio.com"><strong>웹 앱 실행</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><strong>데스크톱 앱 다운로드</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/discussions"><strong>토론 참여</strong></a>
</p>
<p align="center"><img src="https://github.com/user-attachments/assets/10635ad0-454b-4c87-935f-1a3a46f24ccb" alt="Open CAD Studio 작업 공간" width="100%"></p>
## 개요
Open CAD Studio는 기술 도면 작성, 배치 작업, 솔리드 모델링을 위한 크로스 플랫폼 애플리케이션입니다. DWG 및 DXF 도면을 기본 형식으로 읽고 쓰며 데스크톱 버전과 브라우저 버전이 같은 편집 코어를 공유합니다.
이 프로젝트는 활발히 개발되고 있습니다. 중요한 실무 도면은 백업을 보관하고 재현 가능한 문제는 [GitHub Issues](https://github.com/HakanSeven12/OpenCADStudio/issues)에 보고해 주세요.
## 주요 기능
- **네이티브 도면 작업 흐름** — 변환 서비스 없이 DWG 및 DXF 파일을 열고, 편집하고, 복구하고, 저장합니다.
- **정밀한 2D 제도** — 선, 폴리라인, 곡선, 스플라인, 해치, 객체 스냅, 추적, 레이어, 블록, 외부 참조를 지원합니다.
- **문서화 도구** — 문자, 치수, 지시선, 공차, 표, 모델 공간, 도면 공간, 뷰포트, 플롯 스타일을 제공합니다.
- **커널 기반 3D 모델링** — 솔리드 기본 형상, 돌출, 회전, 스윕, 로프트, 불리언 연산, ACIS 엔티티 테셀레이션을 지원합니다.
- **GPU 렌더링**`wgpu`로 가속된 2D 및 3D 뷰포트와 직교·원근 카메라를 제공합니다.
- **확장 가능한 작업 흐름** — 네이티브 플러그인, 명령 스크립트, 헤드리스 변환, 줄 단위 JSON 자동화 API를 지원합니다.
<p align="center"><img src="https://github.com/user-attachments/assets/2a037a09-e8e8-498c-8ed3-58ecb8ae958d" alt="Open CAD Studio의 3D 모델" width="100%"></p>
## 파일 작업 흐름
| 형식 또는 작업 흐름 | 지원 내용 |
| --- | --- |
| DWG | 읽기 및 쓰기, R14부터 2018까지 버전을 지정해 저장 |
| DXF | 읽기 및 쓰기, R14부터 2018까지 버전을 지정해 저장 |
| BAK / SV$ | 도면 백업 및 자동 저장 파일 열기 |
| OBJ | 폴리곤 메시 가져오기 |
| LandXML | `CgPoint` 측량점 가져오기 |
| STL | 3D 메시 데이터 내보내기 |
| STEP AP203 | 3D 메시 데이터 내보내기 |
| PDF | 데스크톱에서 배치와 선택한 형상 플롯 |
| CSV | 엔티티 속성 데이터 추출 |
| CTB / STB | 플롯 스타일 테이블 불러오기 및 편집 |
## 데스크톱 또는 웹
설치 없이 바로 사용하려면 [웹 앱](https://www.opencadstudio.com)을 이용하세요. 도면은 브라우저에서 선택하고 로컬 다운로드로 저장합니다.
네이티브 파일 연결, 파일 관리자 미리보기, 시스템 인쇄, PDF 출력, 외부 플러그인, 명령 스크립트, 헤드리스 자동화에는 데스크톱 애플리케이션을 사용하세요. Windows, Linux, Apple Silicon macOS용 릴리스가 제공됩니다.
## 설치
현재 제공되는 모든 패키지는 [최신 릴리스](https://github.com/HakanSeven12/OpenCADStudio/releases/latest)에서 다운로드할 수 있습니다.
### Windows
서명된 x86-64 패키지 중 하나를 선택하세요.
- `OpenCADStudio-*-windows-x86_64-installer.msi` — 시작 메뉴 바로 가기, DWG/DXF 파일 연결, 도면 미리보기가 포함된 권장 설치 프로그램입니다.
- `OpenCADStudio-*-windows-x86_64-portable.exe` — 설치가 필요 없는 독립 실행형 애플리케이션입니다.
### Linux
x86-64 AppImage를 다운로드하고 실행 권한을 부여한 뒤 실행하세요.
```bash
chmod +x OpenCADStudio-*-linux-x86_64.AppImage
./OpenCADStudio-*-linux-x86_64.AppImage
```
### macOS
배포되는 macOS 패키지는 Apple Silicon을 지원합니다.
1. `OpenCADStudio-*-macos-arm64.dmg`를 다운로드합니다.
2. 이미지를 열고 `OpenCADStudio.app`을 **Applications**로 드래그합니다.
3. Gatekeeper가 첫 실행을 차단하면 **System Settings → Privacy & Security**에서 앱을 허용합니다.
애플리케이션은 임시 서명되어 있지만 현재 Apple 공증을 받지 않았습니다.
## 언어
Open CAD Studio는 시스템 언어를 따르거나 다음 19개 인터페이스 언어 중 하나를 사용할 수 있습니다.
> 아랍어 · 브라질 포르투갈어 · 체코어 · 네덜란드어 · 영어 · 핀란드어 · 프랑스어 · 독일어 · 힌디어 · 헝가리어 · 이탈리아어 · 일본어 · 한국어 · 폴란드어 · 러시아어 · 중국어 간체 · 스페인어 · 중국어 번체 · 터키어
애플리케이션 설정에서 언어를 변경할 수 있습니다. **시스템**을 선택하면 브라우저 버전도 브라우저의 기본 로캘을 사용합니다.
## 소스에서 빌드
### 데스크톱
요구 사항:
- Git
- 최신 안정 Rust 툴체인
- 플랫폼별 그래픽 및 글꼴 개발 라이브러리
Ubuntu 또는 Debian에서는 네이티브 종속성을 설치하세요.
```bash
sudo apt update
sudo apt install libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev \
libxrandr-dev libxkbcommon-dev libwayland-dev libfontconfig1-dev \
libfreetype6-dev
```
그런 다음 빌드합니다.
```bash
git clone https://github.com/HakanSeven12/OpenCADStudio.git
cd OpenCADStudio
cargo build --release --bin OpenCADStudio
```
생성된 바이너리는 `target/release/OpenCADStudio`에 저장됩니다(Windows에서는 `OpenCADStudio.exe`).
### 웹
WebAssembly 대상과 빌드 도구를 한 번 설치하세요.
```bash
rustup target add wasm32-unknown-unknown
cargo install trunk wasm-bindgen-cli
```
개발 서버를 시작합니다.
```bash
trunk serve
```
## 자동화
데스크톱 바이너리는 일회성 변환과 지속형 헤드리스 서버를 지원합니다.
```bash
OpenCADStudio --export input.dwg output.dxf
OpenCADStudio --serve
OpenCADStudio --serve --port 4242
```
서버는 표준 입출력 또는 로컬 TCP 소켓을 통해 한 줄에 하나의 JSON 객체를 교환합니다. [자동화 안내서](docs/automation/README.md)와 포함된 [Python 클라이언트](docs/automation/ocs.py)를 참고하세요.
## 플러그인
데스크톱 플러그인은 별도 프로세스에서 실행되며 버전이 지정된 플러그인 API를 통해 호스트와 통신합니다. 브라우저 빌드는 네이티브 플러그인을 불러오지 않습니다.
- [플러그인 아키텍처](docs/plugin-architecture.md)
- [플러그인 템플릿](docs/plugin-template/README.md)
- [플러그인 레지스트리](plugins/README.md)
## 프로젝트 문서
- [자동화 API](docs/automation/README.md)
- [플러그인 아키텍처](docs/plugin-architecture.md)
- [테셀레이션 파이프라인](docs/tessellation.md)
- [보안 정책](SECURITY.md)
## 기여하기
버그 보고, 목적이 분명한 pull request, 번역, 문서 개선, 플러그인 기여를 환영합니다.
- 새 보고서를 열기 전에 기존 [issues](https://github.com/HakanSeven12/OpenCADStudio/issues)를 검색하세요.
- 질문과 아이디어는 [Discussions](https://github.com/HakanSeven12/OpenCADStudio/discussions)를 이용하세요.
- 취약점은 [보안 정책](SECURITY.md)에 따라 비공개로 보고하세요.
## 프로젝트 성장
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://www.opencadstudio.com/star-history-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://www.opencadstudio.com/star-history-light.svg">
<img alt="Open CAD Studio 스타 및 릴리스 다운로드" src="https://www.opencadstudio.com/star-history-light.svg">
</picture>
</a>
## 프로젝트 후원
Open CAD Studio가 업무에 도움이 된다면 [GitHub Sponsors](https://github.com/sponsors/HakanSeven12) 또는 [Patreon](https://www.patreon.com/HakanSeven12)을 통해 지속적인 개발을 후원해 주세요.
## 라이선스
Open CAD Studio는 [GNU General Public License v3.0](LICENSE)에 따라 배포됩니다.

308
README.md
View file

@ -1,171 +1,225 @@
# Open CAD Studio
<p align="center">
<a href="README.md">English</a> ·
<a href="README.pt-BR.md">Português (Brasil)</a> ·
<a href="README.cs.md">Čeština</a> ·
<a href="README.nl.md">Nederlands</a> ·
<a href="README.fr.md">Français</a> ·
<a href="README.fi.md">Suomi</a> ·
<a href="README.de.md">Deutsch</a> ·
<a href="README.hu.md">Magyar</a> ·
<a href="README.it.md">Italiano</a> ·
<a href="README.ja.md">日本語</a> ·
<a href="README.ko.md">한국어</a> ·
<a href="README.pl.md">Polski</a> ·
<a href="README.ru.md">Русский</a> ·
<a href="README.zh-CN.md">简体中文</a> ·
<a href="README.es.md">Español</a> ·
<a href="README.zh-TW.md">繁體中文</a> ·
<a href="README.tr.md">Türkçe</a> ·
<a href="README.hi.md">हिन्दी</a> ·
<a href="README.ar.md">العربية</a>
</p>
[![Release](https://img.shields.io/github/v/release/HakanSeven12/OpenCADStudio)](https://github.com/HakanSeven12/OpenCADStudio/releases/latest)
[![Downloads](https://img.shields.io/github/downloads/HakanSeven12/OpenCADStudio/total)](https://github.com/HakanSeven12/OpenCADStudio/releases)
[![Stars](https://img.shields.io/github/stars/HakanSeven12/OpenCADStudio)](https://github.com/HakanSeven12/OpenCADStudio/stargazers)
[![License](https://img.shields.io/github/license/HakanSeven12/OpenCADStudio)](LICENSE)
<p align="center">
<img src="assets/logo.svg" width="112" alt="Open CAD Studio logo">
</p>
OCS is a CAD application for 2D drafting and 3D modeling, built with Rust. Reads and writes DWG and DXF files natively. Also has a web version now!
<h1 align="center">Open CAD Studio</h1>
## OCS Web try it in the browser: https://hakanseven12.github.io/OpenCADStudio/
<p align="center">
Open-source 2D drafting and 3D modeling for desktop and web, built with Rust.
</p>
<img width="1920" height="940" alt="resim" src="https://github.com/user-attachments/assets/10635ad0-454b-4c87-935f-1a3a46f24ccb" />
<img width="1920" height="940" alt="resim2" src="https://github.com/user-attachments/assets/2a037a09-e8e8-498c-8ed3-58ecb8ae958d" />
<p align="center">
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><img alt="Latest release" src="https://img.shields.io/github/v/release/HakanSeven12/OpenCADStudio"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases"><img alt="Release downloads" src="https://img.shields.io/github/downloads/HakanSeven12/OpenCADStudio/total"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/HakanSeven12/OpenCADStudio"></a>
<a href="LICENSE"><img alt="GPL-3.0 license" src="https://img.shields.io/github/license/HakanSeven12/OpenCADStudio"></a>
</p>
## Features
<p align="center">
<a href="https://www.opencadstudio.com"><strong>Launch the web app</strong></a>
·
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><strong>Download the desktop app</strong></a>
·
<a href="https://github.com/HakanSeven12/OpenCADStudio/discussions"><strong>Join the discussion</strong></a>
</p>
### File Formats
- **DWG** read/write (R13 through R2018)
- **DXF** read/write (R13 through R2018)
- **STL** export (`STLOUT` / `EXPORTSTL`)
- **STEP AP203** export (`STEPOUT`)
- **OBJ** import (`IMPORTOBJ`)
- **PDF** export (plot layouts to PDF)
- **WBLOCK** — write selected entities or a block to an external file
- **XREF** — attach, reload, and auto-resolve external references
<p align="center">
<img src="https://github.com/user-attachments/assets/10635ad0-454b-4c87-935f-1a3a46f24ccb" alt="Open CAD Studio workspace" width="100%">
</p>
### 2D Drafting
| Command | Description |
|---------|-------------|
| `LINE`, `PLINE`, `RECTANG`, `POLYGON` | Basic geometry |
| `CIRCLE`, `ARC`, `ELLIPSE`, `SPLINE` | Curves |
| `HATCH`, `HATCHEDIT` | Hatch fills with pattern, scale, angle editing |
| `OFFSET`, `TRIM`, `EXTEND`, `FILLET` | Modify geometry (supports lines, arcs, ellipses, polylines, splines) |
| `BREAK`, `STRETCH`, `LENGTHEN` | Shape editing |
| `ARRAY`, `MIRROR`, `MOVE`, `COPY`, `ROTATE`, `SCALE` | Transformations |
| `EXPLODE` | Explode blocks, dimensions, polylines, mlines |
| `DDEDIT` | Double-click text editing |
| `MASSPROP` | Area, perimeter, centroid of selected entities |
## Overview
### 3D Modeling
| Command | Description |
|---------|-------------|
| `BOX`, `SPHERE`, `CYLINDER` | Solid primitives |
| `EXTRUDE`, `REVOLVE` | Profile-based solids |
| `LOFT` | Ruled-surface loft through cross-sections |
| `SWEEP` | Sweep a profile along a path |
| `ARRAY3D` | 3D array |
| ACIS tessellation | Renders `3DSOLID`, `REGION`, and `BODY` entities |
Open CAD Studio is a cross-platform application for technical drawing, layout work, and solid modeling. It reads and writes DWG and DXF drawings natively, with a shared editing core across the desktop and browser versions.
### Annotations & Dimensions
- **Dimensions**: Linear, Aligned, Angular, Radial, Diameter, Ordinate — with full `DIMSTYLE` support (`DIMASZ`, `DIMSCALE`, `DIMEXO`, `DIMEXE`, and more)
- **Text**: `MTEXT`, `TEXT`, `DTEXT` with font browser (`STYLE DIALOG`)
- **Leaders**: `MLEADER` with straight and spline path types; `MLEADERSTYLE` manager
- **Tolerances**: GD&T feature control frames
- **Tables**: `TABLE` entity render; `TABLESTYLE` manager
- **MLine**: `MLINE` entity with `MLSTYLE` manager and `EXPLODE` support
The project is under active development. Keep backups of important production drawings and report reproducible problems through [GitHub Issues](https://github.com/HakanSeven12/OpenCADStudio/issues).
### Paper Space & Layouts
- Multi-tab layout system with model space and unlimited paper space tabs
- **Viewport projection**: Model content correctly projected into paper-space viewport rectangles
- **Camera persistence**: View position and zoom saved per layout; restored on file open and tab switch
- **Correct paper size**: Physical paper dimensions read from embedded PlotSettings (not drawing limits)
- Inline MSPACE overlay — enter a viewport with double-click; edit model entities in place
- `VPORTS` — preset viewport configurations (single, 2H, 2V, 4-way)
- `LAYOUTMANAGER` / `LAYOUTPANEL` — GUI layout manager
- `PLOTSTYLEPANEL` / `STYLESMANAGER` — plot style table editor (CTB/STB)
- `PRINT` — send layout to system printer
## Highlights
### Blocks & References
- `INSERT` with attribute prompting (`ATTREQ`)
- `ATTEDIT` — edit block attribute values interactively
- `REFEDIT` / `REFCLOSE` — in-place block reference editing
- `XREF` — attach, reload, and resolve external DWG/DXF references
- `DATAEXTRACTION` — export entity property data to CSV
- **Native drawing workflow** — open, edit, recover, and save DWG and DXF files without a conversion service.
- **Precise 2D drafting** — lines, polylines, curves, splines, hatches, object snaps, tracking, layers, blocks, and external references.
- **Documentation tools** — text, dimensions, leaders, tolerances, tables, model space, paper space, viewports, and plot styles.
- **Kernel-backed 3D modeling** — solid primitives, extrusion, revolution, sweep, loft, Boolean operations, and ACIS entity tessellation.
- **GPU rendering** — accelerated 2D and 3D viewports through `wgpu`, with orthographic and perspective cameras.
- **Extensible workflows** — native plugins, command scripts, headless conversion, and a line-based JSON automation API.
### Snapping & Precision
- Object snaps: Endpoint, Midpoint, Center, Node, Quadrant, Intersection, Perpendicular, Tangent, Nearest, Insertion, and more
- Ellipse arc endpoints, LWPolyline arc midpoints, Hatch boundary points
- **Object Snap Tracking** (`OTRACK` / `F11`)
- **Polar Tracking** with configurable angle increment
- **Dynamic Input** overlay (`DYNMODE` / `F12`)
- Grid snap with adaptive spacing
- Command history navigation (↑ / ↓)
<p align="center">
<img src="https://github.com/user-attachments/assets/2a037a09-e8e8-498c-8ed3-58ecb8ae958d" alt="3D model in Open CAD Studio" width="100%">
</p>
### Rendering
- GPU-accelerated via WebGPU (wgpu)
- 4× MSAA anti-aliasing
- Orthographic and perspective camera
- ViewCube with face/edge/corner snapping
- **Wide polylines**: LWPolyline and Polyline2D filled strokes
- **Raster images**: GPU-textured quad pipeline (`IMAGE` command)
- **Wipeout**: Solid fill masking
- **Complex linetypes**: Text and shape elements rendered in linetype patterns
- White/black entity colors adapt to background luminance
- Per-viewport background color (`BACKGROUND`)
- Visual style selector (Wireframe, Shaded, etc.)
- X-ray ghost pass for selected wires occluded by geometry
## File workflows
### UI
- Modular ribbon interface — Home, Insert, Annotate, View, Manage, Layout
- Command line with autocomplete and history
- Layer Manager with per-viewport freeze columns
- Properties panel
- `COLORSCHEME` — runtime theme switching
- `SHORTCUTS` — keyboard shortcuts panel
- `SPLINEDIT` — close, open, reverse spline control points
- UCS icon with 3D foreshortening and axis labels
| Format or workflow | Support |
| --- | --- |
| DWG | Read and write; versioned save targets from R14 through 2018 |
| DXF | Read and write; versioned save targets from R14 through 2018 |
| BAK / SV$ | Open drawing backups and autosave files |
| OBJ | Import polygon meshes |
| LandXML | Import `CgPoint` survey points |
| STL | Export 3D mesh data |
| STEP AP203 | Export 3D mesh data |
| PDF | Plot layouts and selected geometry on desktop |
| CSV | Extract entity property data |
| CTB / STB | Load and edit plot style tables |
## Installation
## Desktop or web
### Linux (AppImage)
Use the [web app](https://www.opencadstudio.com) for immediate access with no installation. Drawings are selected through the browser and saved as local downloads.
Download `OpenCADStudio-*-linux-x86_64.AppImage` from the [latest release](https://github.com/HakanSeven12/OpenCADStudio/releases/latest), then:
Use the desktop application for native file associations, file-manager thumbnails, system printing, PDF output, external plugins, command scripts, and headless automation. Release builds are available for Windows, Linux, and Apple Silicon macOS.
## Install
Download all current packages from the [latest release](https://github.com/HakanSeven12/OpenCADStudio/releases/latest).
### Windows
Choose one of these signed x86-64 packages:
- `OpenCADStudio-*-windows-x86_64-installer.msi` — recommended installer with Start Menu shortcuts, DWG/DXF file associations, and drawing thumbnails.
- `OpenCADStudio-*-windows-x86_64-portable.exe` — standalone application; no installation required.
### Linux
Download the x86-64 AppImage, make it executable, and run it:
```bash
chmod +x OpenCADStudio-*-linux-x86_64.AppImage
./OpenCADStudio-*-linux-x86_64.AppImage
```
No installation required — runs directly on any modern Linux distribution.
### macOS
### Windows
The published macOS package supports Apple Silicon:
Download `OpenCADStudio-*-windows-x86_64.exe` from the [latest release](https://github.com/HakanSeven12/OpenCADStudio/releases/latest) and run it directly. Windows SmartScreen may show "Windows protected your PC" because the binary is not yet code-signed — click **More info → Run anyway**.
1. Download `OpenCADStudio-*-macos-arm64.dmg`.
2. Open the image and drag `OpenCADStudio.app` into **Applications**.
3. If Gatekeeper blocks the first launch, approve the app from **System Settings → Privacy & Security**.
### macOS (Apple Silicon)
The application is ad-hoc signed but is not currently notarized by Apple.
Apple Silicon (M-series) only; Intel macOS isn't built. The app is ad-hoc signed but **not Apple-notarised** (notarisation requires a paid Apple Developer ID), so macOS Gatekeeper guards the first launch. Pick whichever path is easiest:
## Languages
**Option A — Homebrew (recommended):**
Open CAD Studio can follow the system language or use any of these 19 interface languages:
> Arabic · Brazilian Portuguese · Czech · Dutch · English · Finnish · French · German · Hindi · Hungarian · Italian · Japanese · Korean · Polish · Russian · Simplified Chinese · Spanish · Traditional Chinese · Turkish
Change the language from the application settings. The browser version also uses the browser's preferred locale when **System** is selected.
## Build from source
### Desktop
Requirements:
- Git
- Current stable Rust toolchain
- Platform graphics and font development libraries
On Ubuntu or Debian, install the native dependencies with:
```bash
brew install --cask --no-quarantine \
https://raw.githubusercontent.com/HakanSeven12/OpenCADStudio/main/packaging/homebrew/open-cad-studio.rb
sudo apt update
sudo apt install libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev \
libxrandr-dev libxkbcommon-dev libwayland-dev libfontconfig1-dev \
libfreetype6-dev
```
`--no-quarantine` lets Gatekeeper skip the unsigned-app prompt. See [`packaging/homebrew/`](packaging/homebrew/) for publishing this as a `brew tap`.
**Option B — manual .dmg:**
Download `OpenCADStudio-*-macos-arm64.dmg` from the [latest release](https://github.com/HakanSeven12/OpenCADStudio/releases/latest), open it, and drag `OpenCADStudio.app` to `/Applications`. If the first launch is blocked, clear the quarantine flag once:
```bash
xattr -dr com.apple.quarantine /Applications/OpenCADStudio.app
```
On older macOS you can instead right-click `OpenCADStudio.app → Open` and confirm; on macOS Ventura and later, approve it via **System Settings → Privacy & Security → Open Anyway**.
### Build from Source
Requirements: Rust 1.75+
Then build and run:
```bash
git clone https://github.com/HakanSeven12/OpenCADStudio.git
cd OpenCADStudio
cargo build --release --bin OpenCADStudio
./target/release/OpenCADStudio
```
## Star History
The resulting binary is written to `target/release/OpenCADStudio` (`OpenCADStudio.exe` on Windows).
### Web
Install the WebAssembly target and build tools once:
```bash
rustup target add wasm32-unknown-unknown
cargo install trunk wasm-bindgen-cli
```
Start the development server:
```bash
trunk serve
```
## Automation
The desktop binary supports one-shot conversion and a persistent headless server:
```bash
OpenCADStudio --export input.dwg output.dxf
OpenCADStudio --serve
OpenCADStudio --serve --port 4242
```
The server exchanges one JSON object per line over standard input/output or a local TCP socket. See the [automation guide](docs/automation/README.md) and the included [Python client](docs/automation/ocs.py).
## Plugins
Desktop plugins run in separate processes and communicate with the host through the versioned plugin API. The browser build does not load native plugins.
- [Plugin architecture](docs/plugin-architecture.md)
- [Plugin template](docs/plugin-template/README.md)
- [Plugin registry](plugins/README.md)
## Project documentation
- [Automation API](docs/automation/README.md)
- [Plugin architecture](docs/plugin-architecture.md)
- [Tessellation pipeline](docs/tessellation.md)
- [Security policy](SECURITY.md)
## Contributing
Bug reports, focused pull requests, translations, documentation improvements, and plugin contributions are welcome.
- Search existing [issues](https://github.com/HakanSeven12/OpenCADStudio/issues) before opening a new report.
- Use [Discussions](https://github.com/HakanSeven12/OpenCADStudio/discussions) for questions and ideas.
- Report vulnerabilities privately by following the [security policy](SECURITY.md).
## Project growth
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://hakanseven12.github.io/OpenCADStudio/star-history-dark.svg" />
<source media="(prefers-color-scheme: light)" srcset="https://hakanseven12.github.io/OpenCADStudio/star-history-light.svg" />
<img alt="OpenCADStudio GitHub star history" src="https://hakanseven12.github.io/OpenCADStudio/star-history-light.svg" />
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://www.opencadstudio.com/star-history-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://www.opencadstudio.com/star-history-light.svg">
<img alt="Open CAD Studio stars and release downloads" src="https://www.opencadstudio.com/star-history-light.svg">
</picture>
</a>
## Support the project
If Open CAD Studio helps your work, support continued development through [GitHub Sponsors](https://github.com/sponsors/HakanSeven12) or [Patreon](https://www.patreon.com/HakanSeven12).
## License
GPL-3.0-only — see [LICENSE](LICENSE)
Open CAD Studio is distributed under the [GNU General Public License v3.0](LICENSE).

195
README.nl.md Normal file
View file

@ -0,0 +1,195 @@
<p align="center">
<a href="README.md">English</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
</p>
<p align="center"><img src="assets/logo.svg" width="112" alt="Logo van Open CAD Studio"></p>
<h1 align="center">Open CAD Studio</h1>
<p align="center">Open-source 2D-tekenen en 3D-modelleren voor desktop en web, gebouwd met Rust.</p>
<p align="center">
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><img alt="Nieuwste versie" src="https://img.shields.io/github/v/release/HakanSeven12/OpenCADStudio"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases"><img alt="Downloads" src="https://img.shields.io/github/downloads/HakanSeven12/OpenCADStudio/total"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers"><img alt="GitHub-sterren" src="https://img.shields.io/github/stars/HakanSeven12/OpenCADStudio"></a>
<a href="LICENSE"><img alt="GPL-3.0-licentie" src="https://img.shields.io/github/license/HakanSeven12/OpenCADStudio"></a>
</p>
<p align="center">
<a href="https://www.opencadstudio.com"><strong>Webapp starten</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><strong>Desktopapp downloaden</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/discussions"><strong>Deelnemen aan discussies</strong></a>
</p>
<p align="center"><img src="https://github.com/user-attachments/assets/10635ad0-454b-4c87-935f-1a3a46f24ccb" alt="Werkruimte van Open CAD Studio" width="100%"></p>
## Overzicht
Open CAD Studio is een platformonafhankelijke toepassing voor technisch tekenen, lay-outwerk en solid modeling. DWG- en DXF-tekeningen worden rechtstreeks gelezen en geschreven; de desktop- en browserversie delen dezelfde bewerkingskern.
Het project wordt actief ontwikkeld. Bewaar reservekopieën van belangrijke productietekeningen en meld reproduceerbare problemen via [GitHub Issues](https://github.com/HakanSeven12/OpenCADStudio/issues).
## Hoogtepunten
- **Native tekenworkflow** — DWG- en DXF-bestanden openen, bewerken, herstellen en opslaan zonder conversiedienst.
- **Nauwkeurig 2D-tekenen** — lijnen, polylijnen, krommen, splines, arceringen, object-snaps, tracking, lagen, blokken en externe referenties.
- **Documentatiegereedschap** — tekst, maatvoering, verwijslijnen, toleranties, tabellen, modelruimte, papierruimte, viewports en plotstijlen.
- **Kernelgestuurd 3D-modelleren** — solid-primitieven, extrusie, omwenteling, sweep, loft, Booleaanse bewerkingen en tessellatie van ACIS-entiteiten.
- **GPU-rendering** — versnelde 2D- en 3D-viewports via `wgpu`, met orthografische en perspectiefcamera's.
- **Uitbreidbare workflows** — native plug-ins, opdrachtscripts, headless conversie en een regelgebaseerde JSON-automatiserings-API.
<p align="center"><img src="https://github.com/user-attachments/assets/2a037a09-e8e8-498c-8ed3-58ecb8ae958d" alt="3D-model in Open CAD Studio" width="100%"></p>
## Bestandsworkflows
| Formaat of workflow | Ondersteuning |
| --- | --- |
| DWG | Lezen en schrijven; opslagdoelen van R14 tot en met 2018 |
| DXF | Lezen en schrijven; opslagdoelen van R14 tot en met 2018 |
| BAK / SV$ | Back-ups en automatisch opgeslagen tekeningen openen |
| OBJ | Polygonale meshes importeren |
| LandXML | `CgPoint`-meetpunten importeren |
| STL | 3D-meshgegevens exporteren |
| STEP AP203 | 3D-meshgegevens exporteren |
| PDF | Lay-outs en geselecteerde geometrie plotten op desktop |
| CSV | Eigenschapsgegevens van entiteiten extraheren |
| CTB / STB | Plotstijltabellen laden en bewerken |
## Desktop of web
Gebruik de [webapp](https://www.opencadstudio.com) voor directe toegang zonder installatie. Tekeningen worden via de browser geselecteerd en als lokale downloads opgeslagen.
Gebruik de desktoptoepassing voor native bestandskoppelingen, miniaturen in bestandsbeheer, systeemafdrukken, PDF-uitvoer, externe plug-ins, opdrachtscripts en headless automatisering. Uitgaven zijn beschikbaar voor Windows, Linux en Apple Silicon macOS.
## Installatie
Download alle actuele pakketten van de [nieuwste uitgave](https://github.com/HakanSeven12/OpenCADStudio/releases/latest).
### Windows
Kies een van deze ondertekende x86-64-pakketten:
- `OpenCADStudio-*-windows-x86_64-installer.msi` — aanbevolen installatieprogramma met snelkoppelingen in het menu Start, DWG/DXF-bestandskoppelingen en tekenminiaturen.
- `OpenCADStudio-*-windows-x86_64-portable.exe` — zelfstandige toepassing; geen installatie nodig.
### Linux
Download de x86-64 AppImage, maak deze uitvoerbaar en start hem:
```bash
chmod +x OpenCADStudio-*-linux-x86_64.AppImage
./OpenCADStudio-*-linux-x86_64.AppImage
```
### macOS
Het gepubliceerde macOS-pakket ondersteunt Apple Silicon:
1. Download `OpenCADStudio-*-macos-arm64.dmg`.
2. Open de schijfkopie en sleep `OpenCADStudio.app` naar **Applications**.
3. Als Gatekeeper de eerste start blokkeert, keur de app dan goed via **System Settings → Privacy & Security**.
De toepassing is ad hoc ondertekend, maar momenteel niet door Apple genotariseerd.
## Talen
Open CAD Studio kan de systeemtaal volgen of een van deze 19 interfacetalen gebruiken:
> Arabisch · Braziliaans-Portugees · Tsjechisch · Nederlands · Engels · Fins · Frans · Duits · Hindi · Hongaars · Italiaans · Japans · Koreaans · Pools · Russisch · Vereenvoudigd Chinees · Spaans · Traditioneel Chinees · Turks
Wijzig de taal in de toepassingsinstellingen. De browserversie gebruikt ook de voorkeurstaal van de browser wanneer **Systeem** is geselecteerd.
## Bouwen vanuit de broncode
### Desktop
Vereisten:
- Git
- Huidige stabiele Rust-toolchain
- Platformbibliotheken voor grafische en lettertypeontwikkeling
Installeer op Ubuntu of Debian de native afhankelijkheden:
```bash
sudo apt update
sudo apt install libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev \
libxrandr-dev libxkbcommon-dev libwayland-dev libfontconfig1-dev \
libfreetype6-dev
```
Bouw daarna:
```bash
git clone https://github.com/HakanSeven12/OpenCADStudio.git
cd OpenCADStudio
cargo build --release --bin OpenCADStudio
```
Het binaire bestand wordt geschreven naar `target/release/OpenCADStudio` (`OpenCADStudio.exe` op Windows).
### Web
Installeer eenmalig het WebAssembly-doel en de buildgereedschappen:
```bash
rustup target add wasm32-unknown-unknown
cargo install trunk wasm-bindgen-cli
```
Start de ontwikkelserver:
```bash
trunk serve
```
## Automatisering
Het desktopprogramma ondersteunt eenmalige conversie en een permanente headless server:
```bash
OpenCADStudio --export input.dwg output.dxf
OpenCADStudio --serve
OpenCADStudio --serve --port 4242
```
De server wisselt per regel één JSON-object uit via standaardinvoer/-uitvoer of een lokale TCP-socket. Bekijk de [automatiseringshandleiding](docs/automation/README.md) en de meegeleverde [Python-client](docs/automation/ocs.py).
## Plug-ins
Desktopplug-ins draaien in afzonderlijke processen en communiceren via de geversioneerde plug-in-API met de host. De browserbuild laadt geen native plug-ins.
- [Plug-inarchitectuur](docs/plugin-architecture.md)
- [Plug-insjabloon](docs/plugin-template/README.md)
- [Plug-inregister](plugins/README.md)
## Projectdocumentatie
- [Automatiserings-API](docs/automation/README.md)
- [Plug-inarchitectuur](docs/plugin-architecture.md)
- [Tessellatiepijplijn](docs/tessellation.md)
- [Beveiligingsbeleid](SECURITY.md)
## Bijdragen
Bugmeldingen, gerichte pull requests, vertalingen, documentatieverbeteringen en plug-inbijdragen zijn welkom.
- Doorzoek bestaande [issues](https://github.com/HakanSeven12/OpenCADStudio/issues) voordat je een nieuwe melding opent.
- Gebruik [Discussions](https://github.com/HakanSeven12/OpenCADStudio/discussions) voor vragen en ideeën.
- Meld kwetsbaarheden privé volgens het [beveiligingsbeleid](SECURITY.md).
## Projectgroei
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://www.opencadstudio.com/star-history-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://www.opencadstudio.com/star-history-light.svg">
<img alt="Sterren en uitgavedownloads van Open CAD Studio" src="https://www.opencadstudio.com/star-history-light.svg">
</picture>
</a>
## Steun het project
Als Open CAD Studio je werk helpt, steun dan de verdere ontwikkeling via [GitHub Sponsors](https://github.com/sponsors/HakanSeven12) of [Patreon](https://www.patreon.com/HakanSeven12).
## Licentie
Open CAD Studio wordt verspreid onder de [GNU General Public License v3.0](LICENSE).

195
README.pl.md Normal file
View file

@ -0,0 +1,195 @@
<p align="center">
<a href="README.md">English</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
</p>
<p align="center"><img src="assets/logo.svg" width="112" alt="Logo Open CAD Studio"></p>
<h1 align="center">Open CAD Studio</h1>
<p align="center">Otwarte narzędzie do rysowania 2D i modelowania 3D na komputerze i w przeglądarce, napisane w Rust.</p>
<p align="center">
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><img alt="Najnowsze wydanie" src="https://img.shields.io/github/v/release/HakanSeven12/OpenCADStudio"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases"><img alt="Pobrania" src="https://img.shields.io/github/downloads/HakanSeven12/OpenCADStudio/total"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers"><img alt="Gwiazdki GitHub" src="https://img.shields.io/github/stars/HakanSeven12/OpenCADStudio"></a>
<a href="LICENSE"><img alt="Licencja GPL-3.0" src="https://img.shields.io/github/license/HakanSeven12/OpenCADStudio"></a>
</p>
<p align="center">
<a href="https://www.opencadstudio.com"><strong>Uruchom aplikację webową</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><strong>Pobierz aplikację komputerową</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/discussions"><strong>Dołącz do dyskusji</strong></a>
</p>
<p align="center"><img src="https://github.com/user-attachments/assets/10635ad0-454b-4c87-935f-1a3a46f24ccb" alt="Obszar roboczy Open CAD Studio" width="100%"></p>
## Przegląd
Open CAD Studio to wieloplatformowa aplikacja do rysunku technicznego, pracy z układami i modelowania brył. Natywnie odczytuje i zapisuje rysunki DWG i DXF, a wersje komputerowa i przeglądarkowa korzystają ze wspólnego rdzenia edycji.
Projekt jest aktywnie rozwijany. Zachowuj kopie zapasowe ważnych rysunków produkcyjnych, a powtarzalne problemy zgłaszaj przez [GitHub Issues](https://github.com/HakanSeven12/OpenCADStudio/issues).
## Najważniejsze funkcje
- **Natywna praca z rysunkiem** — otwieranie, edycja, odzyskiwanie i zapis plików DWG oraz DXF bez usługi konwersji.
- **Precyzyjne rysowanie 2D** — linie, polilinie, krzywe, splajny, kreskowania, przyciąganie do obiektów, śledzenie, warstwy, bloki i odnośniki zewnętrzne.
- **Narzędzia dokumentacji** — tekst, wymiary, odnośniki, tolerancje, tabele, obszar modelu, obszar papieru, rzutnie i style wydruku.
- **Modelowanie 3D oparte na jądrze** — prymitywy bryłowe, wyciąganie, obrót, przeciągnięcie, loft, operacje logiczne i teselacja elementów ACIS.
- **Renderowanie GPU** — przyspieszone widoki 2D i 3D przez `wgpu`, z kamerą ortograficzną i perspektywiczną.
- **Rozszerzalne przepływy pracy** — natywne wtyczki, skrypty poleceń, konwersja bez interfejsu i wierszowe API automatyzacji JSON.
<p align="center"><img src="https://github.com/user-attachments/assets/2a037a09-e8e8-498c-8ed3-58ecb8ae958d" alt="Model 3D w Open CAD Studio" width="100%"></p>
## Przepływy plików
| Format lub przepływ | Obsługa |
| --- | --- |
| DWG | Odczyt i zapis; wersjonowane cele zapisu od R14 do 2018 |
| DXF | Odczyt i zapis; wersjonowane cele zapisu od R14 do 2018 |
| BAK / SV$ | Otwieranie kopii zapasowych i plików autozapisu |
| OBJ | Import siatek wielokątów |
| LandXML | Import punktów pomiarowych `CgPoint` |
| STL | Eksport danych siatki 3D |
| STEP AP203 | Eksport danych siatki 3D |
| PDF | Drukowanie układów i wybranej geometrii w wersji komputerowej |
| CSV | Wyodrębnianie danych właściwości elementów |
| CTB / STB | Wczytywanie i edycja tabel stylów wydruku |
## Komputer czy przeglądarka
Użyj [aplikacji webowej](https://www.opencadstudio.com), aby rozpocząć bez instalacji. Rysunki wybiera się w przeglądarce i zapisuje jako lokalne pliki do pobrania.
Użyj aplikacji komputerowej do natywnych skojarzeń plików, miniatur w menedżerze plików, drukowania systemowego, wyjścia PDF, zewnętrznych wtyczek, skryptów poleceń i automatyzacji bez interfejsu. Wydania są dostępne dla Windows, Linux i macOS na Apple Silicon.
## Instalacja
Wszystkie aktualne pakiety pobierzesz z [najnowszego wydania](https://github.com/HakanSeven12/OpenCADStudio/releases/latest).
### Windows
Wybierz jeden z podpisanych pakietów x86-64:
- `OpenCADStudio-*-windows-x86_64-installer.msi` — zalecany instalator ze skrótami menu Start, skojarzeniami DWG/DXF i miniaturami rysunków.
- `OpenCADStudio-*-windows-x86_64-portable.exe` — samodzielna aplikacja niewymagająca instalacji.
### Linux
Pobierz x86-64 AppImage, nadaj mu uprawnienia wykonywania i uruchom:
```bash
chmod +x OpenCADStudio-*-linux-x86_64.AppImage
./OpenCADStudio-*-linux-x86_64.AppImage
```
### macOS
Opublikowany pakiet macOS obsługuje Apple Silicon:
1. Pobierz `OpenCADStudio-*-macos-arm64.dmg`.
2. Otwórz obraz i przeciągnij `OpenCADStudio.app` do **Applications**.
3. Jeśli Gatekeeper zablokuje pierwsze uruchomienie, zatwierdź aplikację w **System Settings → Privacy & Security**.
Aplikacja jest podpisana ad hoc, ale obecnie nie jest notaryzowana przez Apple.
## Języki
Open CAD Studio może korzystać z języka systemu lub jednego z 19 języków interfejsu:
> Arabski · Portugalski brazylijski · Czeski · Niderlandzki · Angielski · Fiński · Francuski · Niemiecki · Hindi · Węgierski · Włoski · Japoński · Koreański · Polski · Rosyjski · Chiński uproszczony · Hiszpański · Chiński tradycyjny · Turecki
Język zmienisz w ustawieniach aplikacji. Gdy wybrano **System**, wersja przeglądarkowa używa także preferowanych ustawień regionalnych przeglądarki.
## Budowanie ze źródeł
### Wersja komputerowa
Wymagania:
- Git
- Aktualny stabilny zestaw narzędzi Rust
- Biblioteki programistyczne grafiki i czcionek dla platformy
W Ubuntu lub Debianie zainstaluj natywne zależności:
```bash
sudo apt update
sudo apt install libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev \
libxrandr-dev libxkbcommon-dev libwayland-dev libfontconfig1-dev \
libfreetype6-dev
```
Następnie zbuduj:
```bash
git clone https://github.com/HakanSeven12/OpenCADStudio.git
cd OpenCADStudio
cargo build --release --bin OpenCADStudio
```
Powstały plik wykonywalny znajduje się w `target/release/OpenCADStudio` (`OpenCADStudio.exe` w Windows).
### Web
Jednorazowo zainstaluj cel WebAssembly i narzędzia budowania:
```bash
rustup target add wasm32-unknown-unknown
cargo install trunk wasm-bindgen-cli
```
Uruchom serwer programistyczny:
```bash
trunk serve
```
## Automatyzacja
Program komputerowy obsługuje jednorazową konwersję i trwały serwer bez interfejsu:
```bash
OpenCADStudio --export input.dwg output.dxf
OpenCADStudio --serve
OpenCADStudio --serve --port 4242
```
Serwer wymienia jeden obiekt JSON na wiersz przez standardowe wejście/wyjście lub lokalne gniazdo TCP. Zobacz [przewodnik automatyzacji](docs/automation/README.md) i dołączonego [klienta Python](docs/automation/ocs.py).
## Wtyczki
Wtyczki komputerowe działają w oddzielnych procesach i komunikują się z hostem przez wersjonowane API wtyczek. Wersja przeglądarkowa nie ładuje natywnych wtyczek.
- [Architektura wtyczek](docs/plugin-architecture.md)
- [Szablon wtyczki](docs/plugin-template/README.md)
- [Rejestr wtyczek](plugins/README.md)
## Dokumentacja projektu
- [API automatyzacji](docs/automation/README.md)
- [Architektura wtyczek](docs/plugin-architecture.md)
- [Potok teselacji](docs/tessellation.md)
- [Zasady bezpieczeństwa](SECURITY.md)
## Współtworzenie
Mile widziane są zgłoszenia błędów, ukierunkowane pull requesty, tłumaczenia, ulepszenia dokumentacji i wkład we wtyczki.
- Przed otwarciem nowego zgłoszenia przeszukaj istniejące [issues](https://github.com/HakanSeven12/OpenCADStudio/issues).
- Do pytań i pomysłów używaj [Discussions](https://github.com/HakanSeven12/OpenCADStudio/discussions).
- Luki zgłaszaj prywatnie zgodnie z [zasadami bezpieczeństwa](SECURITY.md).
## Rozwój projektu
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://www.opencadstudio.com/star-history-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://www.opencadstudio.com/star-history-light.svg">
<img alt="Gwiazdki i pobrania wydań Open CAD Studio" src="https://www.opencadstudio.com/star-history-light.svg">
</picture>
</a>
## Wesprzyj projekt
Jeśli Open CAD Studio pomaga w Twojej pracy, wesprzyj dalszy rozwój przez [GitHub Sponsors](https://github.com/sponsors/HakanSeven12) lub [Patreon](https://www.patreon.com/HakanSeven12).
## Licencja
Open CAD Studio jest rozpowszechniane na warunkach [GNU General Public License v3.0](LICENSE).

195
README.pt-BR.md Normal file
View file

@ -0,0 +1,195 @@
<p align="center">
<a href="README.md">English</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
</p>
<p align="center"><img src="assets/logo.svg" width="112" alt="Logotipo do Open CAD Studio"></p>
<h1 align="center">Open CAD Studio</h1>
<p align="center">Desenho 2D e modelagem 3D de código aberto para desktop e web, desenvolvido em Rust.</p>
<p align="center">
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><img alt="Versão mais recente" src="https://img.shields.io/github/v/release/HakanSeven12/OpenCADStudio"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases"><img alt="Downloads das versões" src="https://img.shields.io/github/downloads/HakanSeven12/OpenCADStudio/total"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers"><img alt="Estrelas no GitHub" src="https://img.shields.io/github/stars/HakanSeven12/OpenCADStudio"></a>
<a href="LICENSE"><img alt="Licença GPL-3.0" src="https://img.shields.io/github/license/HakanSeven12/OpenCADStudio"></a>
</p>
<p align="center">
<a href="https://www.opencadstudio.com"><strong>Abrir o aplicativo web</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><strong>Baixar o aplicativo desktop</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/discussions"><strong>Participar da discussão</strong></a>
</p>
<p align="center"><img src="https://github.com/user-attachments/assets/10635ad0-454b-4c87-935f-1a3a46f24ccb" alt="Área de trabalho do Open CAD Studio" width="100%"></p>
## Visão geral
Open CAD Studio é um aplicativo multiplataforma para desenho técnico, trabalho com layouts e modelagem de sólidos. Ele lê e grava desenhos DWG e DXF nativamente, com um núcleo de edição compartilhado entre as versões desktop e web.
O projeto está em desenvolvimento ativo. Mantenha cópias de segurança de desenhos de produção importantes e relate problemas reproduzíveis pelo [GitHub Issues](https://github.com/HakanSeven12/OpenCADStudio/issues).
## Destaques
- **Fluxo de desenho nativo** — abra, edite, recupere e salve arquivos DWG e DXF sem serviço de conversão.
- **Desenho 2D preciso** — linhas, polilinhas, curvas, splines, hachuras, snaps a objetos, rastreamento, camadas, blocos e referências externas.
- **Ferramentas de documentação** — texto, cotas, chamadas, tolerâncias, tabelas, espaço do modelo, espaço do papel, viewports e estilos de plotagem.
- **Modelagem 3D com kernel** — primitivas sólidas, extrusão, revolução, varredura, loft, operações booleanas e tesselação de entidades ACIS.
- **Renderização por GPU** — viewports 2D e 3D aceleradas por `wgpu`, com câmeras ortográfica e em perspectiva.
- **Fluxos extensíveis** — plugins nativos, scripts de comandos, conversão sem interface e API de automação JSON baseada em linhas.
<p align="center"><img src="https://github.com/user-attachments/assets/2a037a09-e8e8-498c-8ed3-58ecb8ae958d" alt="Modelo 3D no Open CAD Studio" width="100%"></p>
## Fluxos de arquivos
| Formato ou fluxo | Suporte |
| --- | --- |
| DWG | Leitura e gravação; destinos de salvamento versionados de R14 a 2018 |
| DXF | Leitura e gravação; destinos de salvamento versionados de R14 a 2018 |
| BAK / SV$ | Abertura de backups e arquivos de salvamento automático |
| OBJ | Importação de malhas poligonais |
| LandXML | Importação de pontos topográficos `CgPoint` |
| STL | Exportação de dados de malha 3D |
| STEP AP203 | Exportação de dados de malha 3D |
| PDF | Plotagem de layouts e geometria selecionada no desktop |
| CSV | Extração de dados de propriedades das entidades |
| CTB / STB | Carregamento e edição de tabelas de estilos de plotagem |
## Desktop ou web
Use o [aplicativo web](https://www.opencadstudio.com) para acesso imediato, sem instalação. Os desenhos são selecionados pelo navegador e salvos como downloads locais.
Use o aplicativo desktop para associações nativas de arquivos, miniaturas no gerenciador de arquivos, impressão do sistema, saída em PDF, plugins externos, scripts de comandos e automação sem interface. Há versões para Windows, Linux e macOS com Apple Silicon.
## Instalação
Baixe todos os pacotes atuais na [versão mais recente](https://github.com/HakanSeven12/OpenCADStudio/releases/latest).
### Windows
Escolha um destes pacotes x86-64 assinados:
- `OpenCADStudio-*-windows-x86_64-installer.msi` — instalador recomendado com atalhos no menu Iniciar, associações DWG/DXF e miniaturas de desenhos.
- `OpenCADStudio-*-windows-x86_64-portable.exe` — aplicativo independente; não requer instalação.
### Linux
Baixe o AppImage x86-64, torne-o executável e execute:
```bash
chmod +x OpenCADStudio-*-linux-x86_64.AppImage
./OpenCADStudio-*-linux-x86_64.AppImage
```
### macOS
O pacote publicado para macOS é compatível com Apple Silicon:
1. Baixe `OpenCADStudio-*-macos-arm64.dmg`.
2. Abra a imagem e arraste `OpenCADStudio.app` para **Applications**.
3. Se o Gatekeeper bloquear a primeira abertura, autorize o aplicativo em **System Settings → Privacy & Security**.
O aplicativo possui assinatura ad hoc, mas atualmente não é notarizado pela Apple.
## Idiomas
Open CAD Studio pode seguir o idioma do sistema ou usar um destes 19 idiomas de interface:
> Árabe · Português do Brasil · Tcheco · Holandês · Inglês · Finlandês · Francês · Alemão · Hindi · Húngaro · Italiano · Japonês · Coreano · Polonês · Russo · Chinês simplificado · Espanhol · Chinês tradicional · Turco
Altere o idioma nas configurações do aplicativo. A versão web também usa a localidade preferida do navegador quando **Sistema** está selecionado.
## Compilar a partir do código-fonte
### Desktop
Requisitos:
- Git
- Toolchain estável atual do Rust
- Bibliotecas de desenvolvimento gráfico e de fontes da plataforma
No Ubuntu ou Debian, instale as dependências nativas:
```bash
sudo apt update
sudo apt install libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev \
libxrandr-dev libxkbcommon-dev libwayland-dev libfontconfig1-dev \
libfreetype6-dev
```
Depois compile:
```bash
git clone https://github.com/HakanSeven12/OpenCADStudio.git
cd OpenCADStudio
cargo build --release --bin OpenCADStudio
```
O binário resultante é gravado em `target/release/OpenCADStudio` (`OpenCADStudio.exe` no Windows).
### Web
Instale uma vez o alvo WebAssembly e as ferramentas de compilação:
```bash
rustup target add wasm32-unknown-unknown
cargo install trunk wasm-bindgen-cli
```
Inicie o servidor de desenvolvimento:
```bash
trunk serve
```
## Automação
O binário desktop oferece conversão única e um servidor persistente sem interface:
```bash
OpenCADStudio --export input.dwg output.dxf
OpenCADStudio --serve
OpenCADStudio --serve --port 4242
```
O servidor troca um objeto JSON por linha pela entrada/saída padrão ou por um socket TCP local. Consulte o [guia de automação](docs/automation/README.md) e o [cliente Python](docs/automation/ocs.py) incluído.
## Plugins
Plugins desktop são executados em processos separados e se comunicam com o host pela API de plugins versionada. A versão para navegador não carrega plugins nativos.
- [Arquitetura de plugins](docs/plugin-architecture.md)
- [Modelo de plugin](docs/plugin-template/README.md)
- [Registro de plugins](plugins/README.md)
## Documentação do projeto
- [API de automação](docs/automation/README.md)
- [Arquitetura de plugins](docs/plugin-architecture.md)
- [Pipeline de tesselação](docs/tessellation.md)
- [Política de segurança](SECURITY.md)
## Como contribuir
Relatos de erros, pull requests focados, traduções, melhorias na documentação e contribuições de plugins são bem-vindos.
- Pesquise as [issues](https://github.com/HakanSeven12/OpenCADStudio/issues) existentes antes de abrir um novo relato.
- Use [Discussions](https://github.com/HakanSeven12/OpenCADStudio/discussions) para perguntas e ideias.
- Relate vulnerabilidades em particular seguindo a [política de segurança](SECURITY.md).
## Crescimento do projeto
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://www.opencadstudio.com/star-history-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://www.opencadstudio.com/star-history-light.svg">
<img alt="Estrelas e downloads de versões do Open CAD Studio" src="https://www.opencadstudio.com/star-history-light.svg">
</picture>
</a>
## Apoie o projeto
Se o Open CAD Studio ajuda no seu trabalho, apoie o desenvolvimento contínuo pelo [GitHub Sponsors](https://github.com/sponsors/HakanSeven12) ou [Patreon](https://www.patreon.com/HakanSeven12).
## Licença
Open CAD Studio é distribuído sob a [Licença Pública Geral GNU v3.0](LICENSE).

195
README.ru.md Normal file
View file

@ -0,0 +1,195 @@
<p align="center">
<a href="README.md">English</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
</p>
<p align="center"><img src="assets/logo.svg" width="112" alt="Логотип Open CAD Studio"></p>
<h1 align="center">Open CAD Studio</h1>
<p align="center">Открытое приложение для 2D-черчения и 3D-моделирования на компьютере и в браузере, созданное на Rust.</p>
<p align="center">
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><img alt="Последняя версия" src="https://img.shields.io/github/v/release/HakanSeven12/OpenCADStudio"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases"><img alt="Загрузки" src="https://img.shields.io/github/downloads/HakanSeven12/OpenCADStudio/total"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers"><img alt="Звёзды GitHub" src="https://img.shields.io/github/stars/HakanSeven12/OpenCADStudio"></a>
<a href="LICENSE"><img alt="Лицензия GPL-3.0" src="https://img.shields.io/github/license/HakanSeven12/OpenCADStudio"></a>
</p>
<p align="center">
<a href="https://www.opencadstudio.com"><strong>Запустить веб-приложение</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><strong>Скачать приложение для компьютера</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/discussions"><strong>Присоединиться к обсуждению</strong></a>
</p>
<p align="center"><img src="https://github.com/user-attachments/assets/10635ad0-454b-4c87-935f-1a3a46f24ccb" alt="Рабочее пространство Open CAD Studio" width="100%"></p>
## Обзор
Open CAD Studio — кроссплатформенное приложение для технического черчения, работы с листами и моделирования тел. Оно напрямую читает и записывает чертежи DWG и DXF; версии для компьютера и браузера используют общее ядро редактирования.
Проект активно развивается. Храните резервные копии важных рабочих чертежей и сообщайте о воспроизводимых проблемах через [GitHub Issues](https://github.com/HakanSeven12/OpenCADStudio/issues).
## Основные возможности
- **Прямая работа с чертежами** — открытие, редактирование, восстановление и сохранение DWG и DXF без службы преобразования.
- **Точное 2D-черчение** — линии, полилинии, кривые, сплайны, штриховки, объектные привязки, отслеживание, слои, блоки и внешние ссылки.
- **Средства оформления** — текст, размеры, выноски, допуски, таблицы, пространство модели, пространство листа, видовые экраны и стили печати.
- **3D-моделирование на геометрическом ядре** — примитивы тел, выдавливание, вращение, заметание, лофт, булевы операции и тесселяция объектов ACIS.
- **Отрисовка на GPU** — ускоренные 2D- и 3D-виды через `wgpu`, ортографическая и перспективная камеры.
- **Расширяемые процессы** — нативные плагины, сценарии команд, преобразование без интерфейса и построчный JSON API автоматизации.
<p align="center"><img src="https://github.com/user-attachments/assets/2a037a09-e8e8-498c-8ed3-58ecb8ae958d" alt="3D-модель в Open CAD Studio" width="100%"></p>
## Работа с файлами
| Формат или процесс | Поддержка |
| --- | --- |
| DWG | Чтение и запись; сохранение в версиях от R14 до 2018 |
| DXF | Чтение и запись; сохранение в версиях от R14 до 2018 |
| BAK / SV$ | Открытие резервных копий и файлов автосохранения |
| OBJ | Импорт полигональных сеток |
| LandXML | Импорт точек съёмки `CgPoint` |
| STL | Экспорт данных 3D-сетки |
| STEP AP203 | Экспорт данных 3D-сетки |
| PDF | Печать листов и выбранной геометрии в версии для компьютера |
| CSV | Извлечение данных свойств объектов |
| CTB / STB | Загрузка и редактирование таблиц стилей печати |
## Компьютер или браузер
Используйте [веб-приложение](https://www.opencadstudio.com), чтобы начать без установки. Чертежи выбираются в браузере и сохраняются как локальные загрузки.
Используйте приложение для компьютера, если нужны системные ассоциации файлов, миниатюры в файловом менеджере, системная печать, вывод PDF, внешние плагины, сценарии команд и автоматизация без интерфейса. Выпуски доступны для Windows, Linux и macOS на Apple Silicon.
## Установка
Все актуальные пакеты доступны в [последнем выпуске](https://github.com/HakanSeven12/OpenCADStudio/releases/latest).
### Windows
Выберите один из подписанных пакетов x86-64:
- `OpenCADStudio-*-windows-x86_64-installer.msi` — рекомендуемый установщик с ярлыками в меню «Пуск», ассоциациями DWG/DXF и миниатюрами чертежей.
- `OpenCADStudio-*-windows-x86_64-portable.exe` — автономное приложение, не требующее установки.
### Linux
Скачайте x86-64 AppImage, сделайте его исполняемым и запустите:
```bash
chmod +x OpenCADStudio-*-linux-x86_64.AppImage
./OpenCADStudio-*-linux-x86_64.AppImage
```
### macOS
Опубликованный пакет macOS поддерживает Apple Silicon:
1. Скачайте `OpenCADStudio-*-macos-arm64.dmg`.
2. Откройте образ и перетащите `OpenCADStudio.app` в **Applications**.
3. Если Gatekeeper блокирует первый запуск, разрешите приложение в **System Settings → Privacy & Security**.
Приложение имеет специальную подпись, но в настоящее время не нотарифицировано Apple.
## Языки
Open CAD Studio может следовать языку системы или использовать один из 19 языков интерфейса:
> Арабский · Бразильский португальский · Чешский · Нидерландский · Английский · Финский · Французский · Немецкий · Хинди · Венгерский · Итальянский · Японский · Корейский · Польский · Русский · Упрощённый китайский · Испанский · Традиционный китайский · Турецкий
Язык меняется в настройках приложения. При выборе **Система** версия для браузера также использует предпочтительную локаль браузера.
## Сборка из исходного кода
### Приложение для компьютера
Требования:
- Git
- Текущая стабильная цепочка инструментов Rust
- Библиотеки разработки графики и шрифтов для платформы
В Ubuntu или Debian установите нативные зависимости:
```bash
sudo apt update
sudo apt install libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev \
libxrandr-dev libxkbcommon-dev libwayland-dev libfontconfig1-dev \
libfreetype6-dev
```
Затем выполните сборку:
```bash
git clone https://github.com/HakanSeven12/OpenCADStudio.git
cd OpenCADStudio
cargo build --release --bin OpenCADStudio
```
Готовый файл находится в `target/release/OpenCADStudio` (`OpenCADStudio.exe` в Windows).
### Web
Один раз установите цель WebAssembly и инструменты сборки:
```bash
rustup target add wasm32-unknown-unknown
cargo install trunk wasm-bindgen-cli
```
Запустите сервер разработки:
```bash
trunk serve
```
## Автоматизация
Программа для компьютера поддерживает однократное преобразование и постоянный сервер без интерфейса:
```bash
OpenCADStudio --export input.dwg output.dxf
OpenCADStudio --serve
OpenCADStudio --serve --port 4242
```
Сервер обменивается одним объектом JSON в строке через стандартный ввод/вывод или локальный TCP-сокет. См. [руководство по автоматизации](docs/automation/README.md) и включённый [клиент Python](docs/automation/ocs.py).
## Плагины
Плагины для компьютера работают в отдельных процессах и взаимодействуют с основной программой через версионированный API. Браузерная сборка не загружает нативные плагины.
- [Архитектура плагинов](docs/plugin-architecture.md)
- [Шаблон плагина](docs/plugin-template/README.md)
- [Реестр плагинов](plugins/README.md)
## Документация проекта
- [API автоматизации](docs/automation/README.md)
- [Архитектура плагинов](docs/plugin-architecture.md)
- [Конвейер тесселяции](docs/tessellation.md)
- [Политика безопасности](SECURITY.md)
## Участие в проекте
Приветствуются сообщения об ошибках, целевые pull request, переводы, улучшения документации и вклад в плагины.
- Перед созданием нового сообщения выполните поиск по существующим [issues](https://github.com/HakanSeven12/OpenCADStudio/issues).
- Для вопросов и идей используйте [Discussions](https://github.com/HakanSeven12/OpenCADStudio/discussions).
- Сообщайте об уязвимостях конфиденциально в соответствии с [политикой безопасности](SECURITY.md).
## Рост проекта
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://www.opencadstudio.com/star-history-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://www.opencadstudio.com/star-history-light.svg">
<img alt="Звёзды и загрузки выпусков Open CAD Studio" src="https://www.opencadstudio.com/star-history-light.svg">
</picture>
</a>
## Поддержать проект
Если Open CAD Studio помогает в вашей работе, поддержите дальнейшую разработку через [GitHub Sponsors](https://github.com/sponsors/HakanSeven12) или [Patreon](https://www.patreon.com/HakanSeven12).
## Лицензия
Open CAD Studio распространяется по [GNU General Public License v3.0](LICENSE).

197
README.tr.md Normal file
View file

@ -0,0 +1,197 @@
<p align="center">
<a href="README.md">English</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
</p>
<p align="center"><img src="assets/logo.svg" width="112" alt="Open CAD Studio logosu"></p>
<h1 align="center">Open CAD Studio</h1>
<p align="center">Rust ile geliştirilen, masaüstü ve web için açık kaynaklı 2B çizim ve 3B modelleme uygulaması.</p>
<p align="center">
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><img alt="Son sürüm" src="https://img.shields.io/github/v/release/HakanSeven12/OpenCADStudio"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases"><img alt="Sürüm indirmeleri" src="https://img.shields.io/github/downloads/HakanSeven12/OpenCADStudio/total"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers"><img alt="GitHub yıldızları" src="https://img.shields.io/github/stars/HakanSeven12/OpenCADStudio"></a>
<a href="LICENSE"><img alt="GPL-3.0 lisansı" src="https://img.shields.io/github/license/HakanSeven12/OpenCADStudio"></a>
</p>
<p align="center">
<a href="https://www.opencadstudio.com"><strong>Web uygulamasını aç</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><strong>Masaüstü uygulamasını indir</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/discussions"><strong>Tartışmalara katıl</strong></a>
</p>
<p align="center"><img src="https://github.com/user-attachments/assets/10635ad0-454b-4c87-935f-1a3a46f24ccb" alt="Open CAD Studio çalışma alanı" width="100%"></p>
## Genel bakış
Open CAD Studio; teknik çizim, pafta düzenleme ve katı modelleme için platformlar arası bir uygulamadır. DWG ve DXF çizimlerini doğrudan okur ve yazar; masaüstü ile tarayıcı sürümleri aynı düzenleme çekirdeğini paylaşır.
Proje aktif olarak geliştirilmektedir. Önemli üretim çizimlerinin yedeklerini saklayın ve tekrarlanabilir sorunları [GitHub Issues](https://github.com/HakanSeven12/OpenCADStudio/issues) üzerinden bildirin.
## Öne çıkanlar
- **Doğrudan çizim iş akışı** — dönüştürme hizmeti olmadan DWG ve DXF dosyalarınıın, düzenleyin, kurtarın ve kaydedin.
- **Hassas 2B çizim** — çizgiler, çoklu çizgiler, eğriler, spline'lar, taramalar, nesne yakalama, izleme, katmanlar, bloklar ve dış referanslar.
- **Belgelendirme araçları** — metin, ölçülendirme, kılavuzlar, toleranslar, tablolar, model alanı, kağıt alanı, görünüm pencereleri ve çizim stilleri.
- **Çekirdek destekli 3B modelleme** — katı temel şekiller, ekstrüzyon, döndürme, süpürme, loft, Boolean işlemleri ve ACIS varlık mozaiklemesi.
- **GPU ile görüntüleme**`wgpu` üzerinden hızlandırılmış 2B ve 3B görünüm pencereleri; ortografik ve perspektif kameralar.
- **Genişletilebilir iş akışları** — yerel eklentiler, komut betikleri, arayüzsüz dönüştürme ve satır tabanlı JSON otomasyon API'si.
<p align="center"><img src="https://github.com/user-attachments/assets/2a037a09-e8e8-498c-8ed3-58ecb8ae958d" alt="Open CAD Studio içinde 3B model" width="100%"></p>
## Dosya iş akışları
| Biçim veya iş akışı | Destek |
| --- | --- |
| DWG | Okuma ve yazma; R14 ile 2018 arasında sürümlü kaydetme hedefleri |
| DXF | Okuma ve yazma; R14 ile 2018 arasında sürümlü kaydetme hedefleri |
| BAK / SV$ | Çizim yedeklerini ve otomatik kayıt dosyalarını açma |
| OBJ | Çokgen ağlarını içe aktarma |
| LandXML | `CgPoint` ölçüm noktalarını içe aktarma |
| STL | 3B ağ verilerini dışa aktarma |
| STEP AP203 | 3B ağ verilerini dışa aktarma |
| PDF | Masaüstünde paftaları ve seçili geometrileri çizdirme |
| CSV | Varlık özellik verilerini çıkarma |
| CTB / STB | Çizim stili tablolarını yükleme ve düzenleme |
## Masaüstü mü, web mi?
Kurulum yapmadan hemen başlamak için [web uygulamasını](https://www.opencadstudio.com) kullanın. Çizimler tarayıcı üzerinden seçilir ve yerel indirme olarak kaydedilir.
Yerel dosya ilişkilendirmeleri, dosya yöneticisi küçük resimleri, sistem yazdırması, PDF çıktısı, harici eklentiler, komut betikleri ve arayüzsüz otomasyon için masaüstü uygulamasını kullanın. Windows, Linux ve Apple Silicon macOS için sürüm derlemeleri sunulur.
## Kurulum
Güncel paketlerin tamamını [son sürümden](https://github.com/HakanSeven12/OpenCADStudio/releases/latest) indirin.
### Windows
İmzalı x86-64 paketlerinden birini seçin:
- `OpenCADStudio-*-windows-x86_64-installer.msi` — Başlat menüsü kısayolları, DWG/DXF dosya ilişkilendirmeleri ve çizim küçük resimleri içeren önerilen kurucu.
- `OpenCADStudio-*-windows-x86_64-portable.exe` — kurulum gerektirmeyen bağımsız uygulama.
### Linux
x86-64 AppImage dosyasını indirin, çalıştırılabilir yapın ve başlatın:
```bash
chmod +x OpenCADStudio-*-linux-x86_64.AppImage
./OpenCADStudio-*-linux-x86_64.AppImage
```
### macOS
Yayımlanan macOS paketi Apple Silicon'ı destekler:
1. `OpenCADStudio-*-macos-arm64.dmg` dosyasını indirin.
2. Disk imajınıın ve `OpenCADStudio.app` uygulamasını **Applications** klasörüne sürükleyin.
3. Gatekeeper ilk çalıştırmayı engellerse uygulamayı **System Settings → Privacy & Security** bölümünden onaylayın.
Uygulama geçici imzaya sahiptir ancak şu anda Apple tarafından noterlenmemiştir.
## Diller
Open CAD Studio sistem dilini izleyebilir veya şu 19 arayüz dilinden birini kullanabilir:
> Arapça · Brezilya Portekizcesi · Çekçe · Felemenkçe · İngilizce · Fince · Fransızca · Almanca · Hintçe · Macarca · İtalyanca · Japonca · Korece · Lehçe · Rusça · Basitleştirilmiş Çince · İspanyolca · Geleneksel Çince · Türkçe
Dili uygulama ayarlarından değiştirin. **Sistem** seçildiğinde tarayıcı sürümü de tarayıcının tercih ettiği yerel ayarı kullanır.
## Kaynaktan derleme
### Masaüstü
Gereksinimler:
- Git
- Güncel kararlı Rust araç zinciri
- Platforma ait grafik ve yazı tipi geliştirme kitaplıkları
Ubuntu veya Debian'da yerel bağımlılıkları yükleyin:
```bash
sudo apt update
sudo apt install libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev \
libxrandr-dev libxkbcommon-dev libwayland-dev libfontconfig1-dev \
libfreetype6-dev
```
Ardından derleyin:
```bash
git clone https://github.com/HakanSeven12/OpenCADStudio.git
cd OpenCADStudio
cargo build --release --bin OpenCADStudio
```
Oluşturulan ikili dosya `target/release/OpenCADStudio` konumuna yazılır (Windows'ta `OpenCADStudio.exe`).
### Web
WebAssembly hedefini ve derleme araçlarını bir kez yükleyin:
```bash
rustup target add wasm32-unknown-unknown
cargo install trunk wasm-bindgen-cli
```
Geliştirme sunucusunu başlatın:
```bash
trunk serve
```
## Otomasyon
Masaüstü ikili dosyası tek seferlik dönüştürmeyi ve kalıcı arayüzsüz sunucuyu destekler:
```bash
OpenCADStudio --export input.dwg output.dxf
OpenCADStudio --serve
OpenCADStudio --serve --port 4242
```
Sunucu standart giriş/çıkış veya yerel TCP soketi üzerinden satır başına bir JSON nesnesi alışverişi yapar. [Otomasyon kılavuzuna](docs/automation/README.md) ve birlikte verilen [Python istemcisine](docs/automation/ocs.py) bakın.
## Eklentiler
Masaüstü eklentileri ayrı işlemlerde çalışır ve sürümlü eklenti API'si üzerinden ana uygulamayla iletişim kurar. Tarayıcı derlemesi yerel eklentileri yüklemez.
- [Eklenti mimarisi](docs/plugin-architecture.md)
- [Eklenti şablonu](docs/plugin-template/README.md)
- [Eklenti kayıt defteri](plugins/README.md)
## Proje belgeleri
- [Otomasyon API'si](docs/automation/README.md)
- [Eklenti mimarisi](docs/plugin-architecture.md)
- [Mozaikleme işlem hattı](docs/tessellation.md)
- [Güvenlik politikası](SECURITY.md)
## Katkıda bulunma
Hata bildirimleri, odaklı pull request'ler, çeviriler, belge iyileştirmeleri ve eklenti katkıları kabul edilir.
- Yeni bildirim açmadan önce mevcut [issue'larda](https://github.com/HakanSeven12/OpenCADStudio/issues) arama yapın.
- Sorular ve fikirler için [Discussions](https://github.com/HakanSeven12/OpenCADStudio/discussions) bölümünü kullanın.
- Güvenlik açıklarını [güvenlik politikasını](SECURITY.md) izleyerek özel olarak bildirin.
## Proje büyümesi
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://www.opencadstudio.com/star-history-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://www.opencadstudio.com/star-history-light.svg">
<img alt="Open CAD Studio yıldızları ve sürüm indirmeleri" src="https://www.opencadstudio.com/star-history-light.svg">
</picture>
</a>
## Projeyi destekleyin
Open CAD Studio işinize yardımcı oluyorsa geliştirmeyi [GitHub Sponsors](https://github.com/sponsors/HakanSeven12) veya [Patreon](https://www.patreon.com/HakanSeven12) üzerinden destekleyin.
## Lisans
Open CAD Studio, [GNU Genel Kamu Lisansı v3.0](LICENSE) kapsamında dağıtılır.

195
README.zh-CN.md Normal file
View file

@ -0,0 +1,195 @@
<p align="center">
<a href="README.md">English</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
</p>
<p align="center"><img src="assets/logo.svg" width="112" alt="Open CAD Studio 标志"></p>
<h1 align="center">Open CAD Studio</h1>
<p align="center">使用 Rust 构建、面向桌面端和网页端的开源二维绘图与三维建模应用。</p>
<p align="center">
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><img alt="最新版本" src="https://img.shields.io/github/v/release/HakanSeven12/OpenCADStudio"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases"><img alt="版本下载量" src="https://img.shields.io/github/downloads/HakanSeven12/OpenCADStudio/total"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers"><img alt="GitHub 星标" src="https://img.shields.io/github/stars/HakanSeven12/OpenCADStudio"></a>
<a href="LICENSE"><img alt="GPL-3.0 许可证" src="https://img.shields.io/github/license/HakanSeven12/OpenCADStudio"></a>
</p>
<p align="center">
<a href="https://www.opencadstudio.com"><strong>启动网页应用</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><strong>下载桌面应用</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/discussions"><strong>参与讨论</strong></a>
</p>
<p align="center"><img src="https://github.com/user-attachments/assets/10635ad0-454b-4c87-935f-1a3a46f24ccb" alt="Open CAD Studio 工作区" width="100%"></p>
## 概览
Open CAD Studio 是一款用于技术绘图、布局设计和实体建模的跨平台应用。它可原生读取和写入 DWG 与 DXF 图纸,桌面版和浏览器版共享同一套编辑核心。
本项目正在积极开发中。请保留重要生产图纸的备份,并通过 [GitHub Issues](https://github.com/HakanSeven12/OpenCADStudio/issues) 报告可重现的问题。
## 主要功能
- **原生图纸工作流** — 无需转换服务即可打开、编辑、恢复和保存 DWG/DXF 文件。
- **精确二维绘图** — 支持直线、多段线、曲线、样条曲线、填充、对象捕捉、追踪、图层、块和外部参照。
- **文档工具** — 支持文字、标注、引线、公差、表格、模型空间、图纸空间、视口和打印样式。
- **几何内核支持的三维建模** — 支持实体基本体、拉伸、旋转、扫掠、放样、布尔运算以及 ACIS 实体细分。
- **GPU 渲染** — 通过 `wgpu` 加速二维和三维视口,并支持正交与透视相机。
- **可扩展工作流** — 支持原生插件、命令脚本、无界面转换和逐行 JSON 自动化 API。
<p align="center"><img src="https://github.com/user-attachments/assets/2a037a09-e8e8-498c-8ed3-58ecb8ae958d" alt="Open CAD Studio 中的三维模型" width="100%"></p>
## 文件工作流
| 格式或工作流 | 支持情况 |
| --- | --- |
| DWG | 读取和写入;可选择 R14 至 2018 的保存版本 |
| DXF | 读取和写入;可选择 R14 至 2018 的保存版本 |
| BAK / SV$ | 打开图纸备份和自动保存文件 |
| OBJ | 导入多边形网格 |
| LandXML | 导入 `CgPoint` 测量点 |
| STL | 导出三维网格数据 |
| STEP AP203 | 导出三维网格数据 |
| PDF | 在桌面版中输出布局和选定几何图形 |
| CSV | 提取实体属性数据 |
| CTB / STB | 加载和编辑打印样式表 |
## 桌面版或网页版
使用[网页应用](https://www.opencadstudio.com)可无需安装立即开始。图纸通过浏览器选择,并以本地下载文件保存。
需要原生文件关联、文件管理器缩略图、系统打印、PDF 输出、外部插件、命令脚本和无界面自动化时,请使用桌面应用。项目提供 Windows、Linux 和 Apple Silicon macOS 版本。
## 安装
请从[最新版本](https://github.com/HakanSeven12/OpenCADStudio/releases/latest)下载所有当前软件包。
### Windows
请选择一个已签名的 x86-64 软件包:
- `OpenCADStudio-*-windows-x86_64-installer.msi` — 推荐的安装程序包含开始菜单快捷方式、DWG/DXF 文件关联和图纸缩略图。
- `OpenCADStudio-*-windows-x86_64-portable.exe` — 无需安装的独立应用程序。
### Linux
下载 x86-64 AppImage添加执行权限并运行
```bash
chmod +x OpenCADStudio-*-linux-x86_64.AppImage
./OpenCADStudio-*-linux-x86_64.AppImage
```
### macOS
发布的 macOS 软件包支持 Apple Silicon
1. 下载 `OpenCADStudio-*-macos-arm64.dmg`
2. 打开镜像,将 `OpenCADStudio.app` 拖入 **Applications**
3. 如果 Gatekeeper 阻止首次启动,请在 **System Settings → Privacy & Security** 中允许该应用。
应用采用临时签名,但目前尚未通过 Apple 公证。
## 语言
Open CAD Studio 可以跟随系统语言,也可以使用以下 19 种界面语言:
> 阿拉伯语 · 巴西葡萄牙语 · 捷克语 · 荷兰语 · 英语 · 芬兰语 · 法语 · 德语 · 印地语 · 匈牙利语 · 意大利语 · 日语 · 韩语 · 波兰语 · 俄语 · 简体中文 · 西班牙语 · 繁体中文 · 土耳其语
可在应用设置中更改语言。选择**系统**时,浏览器版也会使用浏览器的首选区域设置。
## 从源代码构建
### 桌面版
要求:
- Git
- 当前稳定版 Rust 工具链
- 对应平台的图形和字体开发库
在 Ubuntu 或 Debian 上安装原生依赖:
```bash
sudo apt update
sudo apt install libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev \
libxrandr-dev libxkbcommon-dev libwayland-dev libfontconfig1-dev \
libfreetype6-dev
```
然后构建:
```bash
git clone https://github.com/HakanSeven12/OpenCADStudio.git
cd OpenCADStudio
cargo build --release --bin OpenCADStudio
```
生成的二进制文件位于 `target/release/OpenCADStudio`Windows 上为 `OpenCADStudio.exe`)。
### 网页版
一次性安装 WebAssembly 目标和构建工具:
```bash
rustup target add wasm32-unknown-unknown
cargo install trunk wasm-bindgen-cli
```
启动开发服务器:
```bash
trunk serve
```
## 自动化
桌面二进制程序支持一次性转换和持续运行的无界面服务器:
```bash
OpenCADStudio --export input.dwg output.dxf
OpenCADStudio --serve
OpenCADStudio --serve --port 4242
```
服务器通过标准输入/输出或本地 TCP 套接字逐行交换 JSON 对象。请参阅[自动化指南](docs/automation/README.md)和随附的 [Python 客户端](docs/automation/ocs.py)。
## 插件
桌面插件运行于独立进程中,并通过有版本管理的插件 API 与主程序通信。浏览器版本不会加载原生插件。
- [插件架构](docs/plugin-architecture.md)
- [插件模板](docs/plugin-template/README.md)
- [插件注册表](plugins/README.md)
## 项目文档
- [自动化 API](docs/automation/README.md)
- [插件架构](docs/plugin-architecture.md)
- [细分处理流程](docs/tessellation.md)
- [安全策略](SECURITY.md)
## 参与贡献
欢迎提交错误报告、目标明确的 pull request、翻译、文档改进和插件贡献。
- 提交新报告前请搜索现有 [issues](https://github.com/HakanSeven12/OpenCADStudio/issues)。
- 问题和想法请使用 [Discussions](https://github.com/HakanSeven12/OpenCADStudio/discussions)。
- 请按照[安全策略](SECURITY.md)私下报告漏洞。
## 项目成长
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://www.opencadstudio.com/star-history-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://www.opencadstudio.com/star-history-light.svg">
<img alt="Open CAD Studio 星标和版本下载量" src="https://www.opencadstudio.com/star-history-light.svg">
</picture>
</a>
## 支持项目
如果 Open CAD Studio 对您的工作有所帮助,请通过 [GitHub Sponsors](https://github.com/sponsors/HakanSeven12) 或 [Patreon](https://www.patreon.com/HakanSeven12) 支持持续开发。
## 许可证
Open CAD Studio 根据 [GNU General Public License v3.0](LICENSE) 分发。

195
README.zh-TW.md Normal file
View file

@ -0,0 +1,195 @@
<p align="center">
<a href="README.md">English</a> · <a href="README.pt-BR.md">Português (Brasil)</a> · <a href="README.cs.md">Čeština</a> · <a href="README.nl.md">Nederlands</a> · <a href="README.fr.md">Français</a> · <a href="README.fi.md">Suomi</a> · <a href="README.de.md">Deutsch</a> · <a href="README.hu.md">Magyar</a> · <a href="README.it.md">Italiano</a> · <a href="README.ja.md">日本語</a> · <a href="README.ko.md">한국어</a> · <a href="README.pl.md">Polski</a> · <a href="README.ru.md">Русский</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.es.md">Español</a> · <a href="README.zh-TW.md">繁體中文</a> · <a href="README.tr.md">Türkçe</a> · <a href="README.hi.md">हिन्दी</a> · <a href="README.ar.md">العربية</a>
</p>
<p align="center"><img src="assets/logo.svg" width="112" alt="Open CAD Studio 標誌"></p>
<h1 align="center">Open CAD Studio</h1>
<p align="center">使用 Rust 建構、適用於桌面與網頁的開放原始碼二維繪圖和三維建模應用程式。</p>
<p align="center">
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><img alt="最新版本" src="https://img.shields.io/github/v/release/HakanSeven12/OpenCADStudio"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases"><img alt="版本下載量" src="https://img.shields.io/github/downloads/HakanSeven12/OpenCADStudio/total"></a>
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers"><img alt="GitHub 星號" src="https://img.shields.io/github/stars/HakanSeven12/OpenCADStudio"></a>
<a href="LICENSE"><img alt="GPL-3.0 授權" src="https://img.shields.io/github/license/HakanSeven12/OpenCADStudio"></a>
</p>
<p align="center">
<a href="https://www.opencadstudio.com"><strong>啟動網頁應用程式</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/releases/latest"><strong>下載桌面應用程式</strong></a> ·
<a href="https://github.com/HakanSeven12/OpenCADStudio/discussions"><strong>參與討論</strong></a>
</p>
<p align="center"><img src="https://github.com/user-attachments/assets/10635ad0-454b-4c87-935f-1a3a46f24ccb" alt="Open CAD Studio 工作區" width="100%"></p>
## 概覽
Open CAD Studio 是用於技術繪圖、配置作業和實體建模的跨平台應用程式。它能原生讀取與寫入 DWG 和 DXF 圖面,桌面版與瀏覽器版共用相同的編輯核心。
本專案正積極開發中。請保留重要生產圖面的備份,並透過 [GitHub Issues](https://github.com/HakanSeven12/OpenCADStudio/issues) 回報可重現的問題。
## 主要功能
- **原生圖面工作流程** — 不需轉換服務即可開啟、編輯、復原及儲存 DWG/DXF 檔案。
- **精確二維繪圖** — 支援直線、聚合線、曲線、雲形線、填充線、物件鎖點、追蹤、圖層、圖塊和外部參考。
- **文件工具** — 支援文字、標註、引線、公差、表格、模型空間、圖紙空間、視埠和出圖型式。
- **幾何核心支援的三維建模** — 支援實體基本形、擠出、旋轉、掃掠、疊層、布林運算和 ACIS 圖元細分。
- **GPU 轉譯** — 透過 `wgpu` 加速二維和三維視埠,並支援正投影與透視相機。
- **可擴充工作流程** — 支援原生外掛程式、指令碼、無介面轉換和逐行 JSON 自動化 API。
<p align="center"><img src="https://github.com/user-attachments/assets/2a037a09-e8e8-498c-8ed3-58ecb8ae958d" alt="Open CAD Studio 中的三維模型" width="100%"></p>
## 檔案工作流程
| 格式或工作流程 | 支援內容 |
| --- | --- |
| DWG | 讀取與寫入;可選擇 R14 至 2018 的儲存版本 |
| DXF | 讀取與寫入;可選擇 R14 至 2018 的儲存版本 |
| BAK / SV$ | 開啟圖面備份與自動儲存檔案 |
| OBJ | 匯入多邊形網格 |
| LandXML | 匯入 `CgPoint` 測量點 |
| STL | 匯出三維網格資料 |
| STEP AP203 | 匯出三維網格資料 |
| PDF | 在桌面版中出圖配置和選取的幾何圖形 |
| CSV | 擷取圖元屬性資料 |
| CTB / STB | 載入與編輯出圖型式表 |
## 桌面版或網頁版
使用[網頁應用程式](https://www.opencadstudio.com)即可不需安裝立即開始。圖面透過瀏覽器選取,並儲存為本機下載檔案。
需要原生檔案關聯、檔案管理員縮圖、系統列印、PDF 輸出、外部外掛程式、指令碼和無介面自動化時,請使用桌面應用程式。專案提供 Windows、Linux 和 Apple Silicon macOS 版本。
## 安裝
請從[最新版本](https://github.com/HakanSeven12/OpenCADStudio/releases/latest)下載所有目前的套件。
### Windows
請選擇一個已簽署的 x86-64 套件:
- `OpenCADStudio-*-windows-x86_64-installer.msi` — 建議的安裝程式包含開始功能表捷徑、DWG/DXF 檔案關聯和圖面縮圖。
- `OpenCADStudio-*-windows-x86_64-portable.exe` — 不需安裝的獨立應用程式。
### Linux
下載 x86-64 AppImage加入執行權限並執行
```bash
chmod +x OpenCADStudio-*-linux-x86_64.AppImage
./OpenCADStudio-*-linux-x86_64.AppImage
```
### macOS
發佈的 macOS 套件支援 Apple Silicon
1. 下載 `OpenCADStudio-*-macos-arm64.dmg`
2. 開啟映像檔,將 `OpenCADStudio.app` 拖入 **Applications**
3. 如果 Gatekeeper 阻止首次啟動,請在 **System Settings → Privacy & Security** 中允許此應用程式。
應用程式採用臨時簽章,但目前尚未通過 Apple 公證。
## 語言
Open CAD Studio 可以跟隨系統語言,也可以使用以下 19 種介面語言:
> 阿拉伯文 · 巴西葡萄牙文 · 捷克文 · 荷蘭文 · 英文 · 芬蘭文 · 法文 · 德文 · 北印度文 · 匈牙利文 · 義大利文 · 日文 · 韓文 · 波蘭文 · 俄文 · 簡體中文 · 西班牙文 · 繁體中文 · 土耳其文
可在應用程式設定中變更語言。選擇**系統**時,瀏覽器版也會使用瀏覽器的偏好地區設定。
## 從原始碼建構
### 桌面版
需求:
- Git
- 目前穩定版 Rust 工具鏈
- 對應平台的圖形與字型開發程式庫
在 Ubuntu 或 Debian 上安裝原生相依套件:
```bash
sudo apt update
sudo apt install libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev \
libxrandr-dev libxkbcommon-dev libwayland-dev libfontconfig1-dev \
libfreetype6-dev
```
接著建構:
```bash
git clone https://github.com/HakanSeven12/OpenCADStudio.git
cd OpenCADStudio
cargo build --release --bin OpenCADStudio
```
產生的二進位檔位於 `target/release/OpenCADStudio`Windows 上為 `OpenCADStudio.exe`)。
### 網頁版
一次安裝 WebAssembly 目標和建構工具:
```bash
rustup target add wasm32-unknown-unknown
cargo install trunk wasm-bindgen-cli
```
啟動開發伺服器:
```bash
trunk serve
```
## 自動化
桌面二進位程式支援單次轉換和持續執行的無介面伺服器:
```bash
OpenCADStudio --export input.dwg output.dxf
OpenCADStudio --serve
OpenCADStudio --serve --port 4242
```
伺服器透過標準輸入/輸出或本機 TCP 通訊端逐行交換 JSON 物件。請參閱[自動化指南](docs/automation/README.md)和隨附的 [Python 用戶端](docs/automation/ocs.py)。
## 外掛程式
桌面外掛程式在獨立處理程序中執行,並透過有版本管理的外掛程式 API 與主程式通訊。瀏覽器版本不會載入原生外掛程式。
- [外掛程式架構](docs/plugin-architecture.md)
- [外掛程式範本](docs/plugin-template/README.md)
- [外掛程式登錄檔](plugins/README.md)
## 專案文件
- [自動化 API](docs/automation/README.md)
- [外掛程式架構](docs/plugin-architecture.md)
- [細分處理流程](docs/tessellation.md)
- [安全性政策](SECURITY.md)
## 參與貢獻
歡迎提交錯誤報告、目標明確的 pull request、翻譯、文件改進和外掛程式貢獻。
- 建立新報告前請搜尋現有 [issues](https://github.com/HakanSeven12/OpenCADStudio/issues)。
- 問題和想法請使用 [Discussions](https://github.com/HakanSeven12/OpenCADStudio/discussions)。
- 請依照[安全性政策](SECURITY.md)私下回報弱點。
## 專案成長
<a href="https://github.com/HakanSeven12/OpenCADStudio/stargazers">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://www.opencadstudio.com/star-history-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://www.opencadstudio.com/star-history-light.svg">
<img alt="Open CAD Studio 星號與版本下載量" src="https://www.opencadstudio.com/star-history-light.svg">
</picture>
</a>
## 支持專案
如果 Open CAD Studio 對您的工作有幫助,請透過 [GitHub Sponsors](https://github.com/sponsors/HakanSeven12) 或 [Patreon](https://www.patreon.com/HakanSeven12) 支持持續開發。
## 授權
Open CAD Studio 依 [GNU General Public License v3.0](LICENSE) 散佈。

View file

@ -14,12 +14,7 @@ serde = { version = "1", features = ["derive"] }
# Pulled in only by the `host` feature, which adds the `acadrust`-typed
# `HostApi` runtime surface. The default crate stays dependency-free so engine
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
#
# acadrust types appear in the HostApi trait object, so the plugin and host
# MUST resolve the exact same acadrust source. The root Cargo.toml pins the
# exact revision via [patch.crates-io]; this crate uses the same version
# requirement so the patch applies consistently across the workspace.
acadrust = { version = "0.4", optional = true, features = ["serde"] }
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "c28b2cd", optional = true, features = ["serde"] }
# Runtime IPC and serialization (host feature only).
interprocess = { version = "2", optional = true }
@ -42,9 +37,7 @@ serde_json = "1"
serde = { version = "1", features = ["derive"] }
cargo-lock = "11"
# acadrust is scanned at build time to generate the embedded type registry.
# Use the same version requirement as the root Cargo.toml so the
# [patch.crates-io] override applies here too.
acadrust = { version = "0.4", features = ["serde"] }
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "c28b2cd", features = ["serde"] }
[dev-dependencies]
serde_json = "1"

View file

@ -18,7 +18,10 @@ fn main() {
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
generate_type_registry(&out_dir);
generate_version_info(&out_dir);
println!("cargo:rerun-if-changed=Cargo.lock");
println!(
"cargo:rerun-if-changed={}",
workspace_cargo_lock_path().display()
);
}
// ════════════════════════════════════════════════════════════════════════════

View file

@ -256,10 +256,6 @@ impl<'de> Deserialize<'de> for PluginNotification {
}
}
/// An add-on package's entry point: its manifest, optional ribbon tab, and
/// command dispatch. Built-in (in-tree) and dynamically-loaded (cdylib) plugins
/// implement the same trait from this crate, so an out-of-tree add-on targets
/// the stable contract rather than the host binary.
/// An add-on package's entry point: its manifest, optional ribbon tab, and
/// command dispatch. Built-in (in-tree) and dynamically-loaded (cdylib) plugins
/// implement the same trait from this crate, so an out-of-tree add-on targets

View file

@ -143,6 +143,25 @@ mod tests {
}
}
#[test]
fn v4_additions_preserve_legacy_discriminants() {
fn discriminant<T: serde::Serialize>(value: &T) -> u32 {
let bytes = bincode::serialize(value).unwrap();
u32::from_le_bytes(bytes[..4].try_into().unwrap())
}
assert_eq!(discriminant(&HostRequest::Shutdown), 6);
assert_eq!(discriminant(&HostResponse::Error(String::new())), 5);
assert_eq!(
discriminant(&crate::ipc::protocol::PluginRequest::BumpGeometry),
6
);
assert_eq!(
discriminant(&crate::ipc::protocol::PluginResponse::Record(None)),
3
);
}
#[test]
fn transport_rejects_oversized_message() {
let (mut a, _b) = connect_pair();

View file

@ -59,13 +59,13 @@ pub enum HostRequest {
NeedsEntityPick {
command_id: u64,
},
Shutdown,
ExecuteCode {
command_id: u64,
source: CommandSource,
code: String,
tab_index: usize,
},
Shutdown,
}
/// Responses the plugin runner sends back for `HostRequest`.
@ -76,8 +76,8 @@ pub enum HostResponse {
Text(String),
Ribbon(Vec<OwnedRibbonGroup>),
Manifest(OwnedPluginManifest),
CodeExecutionResult(crate::host::ExecutionResult),
Error(String),
CodeExecutionResult(crate::host::ExecutionResult),
}
/// Requests the plugin runner sends to the host.
@ -87,8 +87,6 @@ pub enum PluginRequest {
PushOutput(String),
PushError(String),
AddEntity(EntityType),
/// Add multiple entities in a single request.
AddEntities(Vec<EntityType>),
/// Replace the existing entity carrying this entity's handle in place.
UpdateEntity(EntityType),
/// Delete the entity with `handle`.
@ -119,6 +117,8 @@ pub enum PluginRequest {
/// Ask the host to create/refresh a shared-memory document view and return
/// the file path + current version.
OpenDocumentView,
/// Add multiple entities in a single request.
AddEntities(Vec<EntityType>),
/// V4: ask the host to create/refresh a tab-keyed shared-memory document
/// view and return the file path + current version.
OpenDocumentViewV4 { tab_id: u64 },
@ -134,7 +134,6 @@ pub enum PluginResponse {
Ok,
Bool(bool),
Handle(Handle),
Handles(Vec<Handle>),
Record(Option<ExtendedDataRecord>),
Document(Box<CadDocument>),
Error(String),
@ -143,6 +142,7 @@ pub enum PluginResponse {
path: String,
version: u64,
},
Handles(Vec<Handle>),
/// V4: path to the tab-keyed memory-mapped file and current version.
DocumentViewV4 {
path: String,

View file

@ -11,7 +11,7 @@
//! formats or enum variants.
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::net::{Shutdown, TcpListener, TcpStream};
use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;
@ -82,7 +82,7 @@ pub fn recv_framed<R: Read, T: serde::de::DeserializeOwned>(reader: &mut R) -> R
/// Thread-safe [`PluginRequestSender`] implementation that forwards requests
/// over a single TCP stream to a request proxy server.
pub struct ProxyPluginRequestSender {
stream: Mutex<TcpStream>,
stream: Arc<Mutex<TcpStream>>,
}
impl ProxyPluginRequestSender {
@ -98,21 +98,14 @@ impl ProxyPluginRequestSender {
stream.flush()?;
stream.set_read_timeout(Some(REQUEST_TIMEOUT))?;
Ok(Self {
stream: Mutex::new(stream),
stream: Arc::new(Mutex::new(stream)),
})
}
/// Create a second handle to the same underlying socket so multiple
/// callers can share the proxy connection (requests are still serialized
/// by the mutex on each handle).
/// Create a second handle that shares the connection and its request lock.
pub fn try_clone(&self) -> Result<Self, ProxyError> {
let stream = self
.stream
.lock()
.map_err(|e| ProxyError::Io(std::io::Error::other(e.to_string())))?
.try_clone()?;
Ok(Self {
stream: Mutex::new(stream),
stream: Arc::clone(&self.stream),
})
}
@ -145,6 +138,7 @@ impl ProxyPluginRequestSender {
|| e.kind() == ErrorKind::TimedOut =>
{
if let Err(e) = poll() {
let _ = stream.shutdown(Shutdown::Both);
break Err(e);
}
}

View file

@ -43,7 +43,7 @@ type NotificationQueue = mpsc::Receiver<(Option<u64>, HostNotification)>;
struct Shared {
writer: Mutex<Stream>,
next_id: AtomicU64,
in_flight: Mutex<Option<mpsc::Sender<(u64, PluginResponse)>>>,
in_flight: Mutex<HashMap<u64, mpsc::Sender<PluginResponse>>>,
}
/// A frame the V4 reader thread delivers to the plugin runner loop.
@ -96,7 +96,7 @@ impl V4Client {
let shared = Arc::new(Shared {
writer: Mutex::new(stream),
next_id: AtomicU64::new(1),
in_flight: Mutex::new(None),
in_flight: Mutex::new(HashMap::new()),
});
let shared_for_reader = Arc::clone(&shared);
@ -122,6 +122,15 @@ impl V4Client {
self.runner_queue.recv()
}
/// Wait up to `timeout` for a host request so the runner can also drain
/// notifications while otherwise idle.
pub fn recv_runner_frame_timeout(
&self,
timeout: Duration,
) -> Result<RunnerFrame, mpsc::RecvTimeoutError> {
self.runner_queue.recv_timeout(timeout)
}
/// Non-blocking poll for host-to-plugin notifications.
pub fn try_recv_notification(&self) -> Option<(Option<u64>, HostNotification)> {
self.notifications.lock().unwrap_or_else(|e| e.into_inner()).try_recv().ok()
@ -214,8 +223,13 @@ fn reader_thread(
loop {
match recv::<HostToPluginV4>(&mut reader) {
Ok(HostToPluginV4::Response { id, payload }) => {
if let Some(tx) = shared.in_flight.lock().unwrap_or_else(|e| e.into_inner()).take() {
let _ = tx.send((id, payload));
let tx = shared
.in_flight
.lock()
.unwrap_or_else(|e| e.into_inner())
.remove(&id);
if let Some(tx) = tx {
let _ = tx.send(payload);
} else {
eprintln!("[plugin] V4 reader: unexpected response id={id}");
}
@ -249,8 +263,11 @@ fn reader_thread(
}
}
}));
// Ensure any synchronous waiter is unblocked.
let _ = shared.in_flight.lock().unwrap_or_else(|e| e.into_inner()).take();
shared
.in_flight
.lock()
.unwrap_or_else(|e| e.into_inner())
.clear();
}
/// `HostApi` implementation used inside the plugin process over the V4
@ -264,7 +281,7 @@ pub struct V4PluginHostApi {
next_command_id: Cell<u64>,
record_cache: RefCell<HashMap<(Handle, String), &'static ExtendedDataRecord>>,
doc_view: RefCell<Option<DocumentViewInfo>>,
doc_view_v4: RefCell<Option<DocumentViewInfo>>,
doc_view_v4: RefCell<Option<(u64, DocumentViewInfo)>>,
tab_id_cache: Cell<Option<u64>>,
}
@ -293,7 +310,7 @@ impl V4PluginHostApi {
&self,
req: PluginRequest,
) -> Result<PluginResponse, crate::ipc::transport::TransportError> {
send_plugin_request(&self.shared, req)
send_plugin_request(&self.shared, self.tab_id_cache.get(), req)
}
fn fetch_document(&self) -> CadDocument {
@ -316,23 +333,37 @@ impl V4PluginHostApi {
/// thread-safe [`V4PluginRequestSender`].
fn send_plugin_request(
shared: &Arc<Shared>,
tab_id: Option<u64>,
req: PluginRequest,
) -> Result<PluginResponse, crate::ipc::transport::TransportError> {
let id = shared.next_id.fetch_add(1, Ordering::Relaxed);
let (tx, rx) = mpsc::channel();
*shared.in_flight.lock().unwrap_or_else(|e| e.into_inner()) = Some(tx);
{
shared
.in_flight
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(id, tx);
let send_result = {
let mut writer = shared.writer.lock().unwrap_or_else(|e| e.into_inner());
send(&mut writer, &PluginToHostV4::Request { id, payload: req })?;
send(
&mut writer,
&PluginToHostV4::Request {
id,
tab_id,
payload: req,
},
)
};
if let Err(error) = send_result {
shared
.in_flight
.lock()
.unwrap_or_else(|e| e.into_inner())
.remove(&id);
return Err(error);
}
match rx.recv() {
Ok((resp_id, resp)) if resp_id == id => Ok(resp),
Ok((resp_id, _)) => Err(crate::ipc::transport::TransportError::Io(
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("V4 response id mismatch: expected {id}, got {resp_id}"),
),
)),
Ok(resp) => Ok(resp),
Err(_) => Err(crate::ipc::transport::TransportError::Io(
std::io::Error::new(
std::io::ErrorKind::NotConnected,
@ -343,17 +374,16 @@ fn send_plugin_request(
}
/// Thread-safe wrapper around the V4 connection so worker threads can issue
/// host requests. A per-sender mutex serializes requests, matching the
/// single-in-flight behavior of [`V4PluginHostApi::request`].
/// host requests for the originating document tab.
struct V4PluginRequestSender {
shared: Arc<Shared>,
lock: Mutex<()>,
tab_id: u64,
}
impl PluginRequestSender for V4PluginRequestSender {
fn request(&self, req: PluginRequest) -> Result<PluginResponse, PluginRequestError> {
let _guard = self.lock.lock().map_err(|e| PluginRequestError(e.to_string()))?;
send_plugin_request(&self.shared, req).map_err(|e| PluginRequestError(e.to_string()))
send_plugin_request(&self.shared, Some(self.tab_id), req)
.map_err(|e| PluginRequestError(e.to_string()))
}
}
@ -631,7 +661,7 @@ impl HostApi for V4PluginHostApi {
fn plugin_request_sender(&self) -> Option<Box<dyn PluginRequestSender>> {
Some(Box::new(V4PluginRequestSender {
shared: Arc::clone(&self.shared),
lock: Mutex::new(()),
tab_id: self.tab_id(),
}))
}
@ -655,14 +685,13 @@ impl HostApi for V4PluginHostApi {
}
}
fn document_view_v4(&mut self, _tab_id: u64) -> Option<DocumentViewInfo> {
fn document_view_v4(&mut self, tab_id: u64) -> Option<DocumentViewInfo> {
{
let mut view = self.doc_view_v4.borrow_mut();
if view.is_none() {
let tab_id = self.tab_id();
if view.as_ref().map(|(cached_id, _)| *cached_id) != Some(tab_id) {
match self.request(PluginRequest::OpenDocumentViewV4 { tab_id }) {
Ok(PluginResponse::DocumentViewV4 { path, version }) => {
*view = Some(DocumentViewInfo { path, version });
*view = Some((tab_id, DocumentViewInfo { path, version }));
}
Ok(other) => {
eprintln!("[plugin] unexpected OpenDocumentViewV4 response: {other:?}");
@ -673,13 +702,18 @@ impl HostApi for V4PluginHostApi {
}
}
}
self.doc_view_v4.borrow().clone()
self.doc_view_v4
.borrow()
.as_ref()
.map(|(_, info)| info.clone())
}
fn close_document_view_v4(&mut self, _tab_id: u64) {
let tab_id = self.tab_id();
fn close_document_view_v4(&mut self, tab_id: u64) {
let _ = self.request(PluginRequest::CloseDocumentViewV4 { tab_id });
self.doc_view_v4.borrow_mut().take();
let mut view = self.doc_view_v4.borrow_mut();
if view.as_ref().map(|(cached_id, _)| *cached_id) == Some(tab_id) {
view.take();
}
}
}
@ -694,7 +728,7 @@ impl V4Client {
let shared = Arc::new(Shared {
writer: Mutex::new(stream),
next_id: AtomicU64::new(1),
in_flight: Mutex::new(None),
in_flight: Mutex::new(HashMap::new()),
});
let shared_for_reader = Arc::clone(&shared);
let notifications = Arc::new(Mutex::new(notify_rx));
@ -776,7 +810,11 @@ mod tests {
let runner = thread::spawn(move || {
let req = recv::<PluginToHostV4>(&mut runner_stream).unwrap();
match req {
PluginToHostV4::Request { id, payload: PluginRequest::PushInfo(s) } => {
PluginToHostV4::Request {
id,
tab_id: _,
payload: PluginRequest::PushInfo(s),
} => {
assert_eq!(s, "hello host");
send(
&mut runner_stream,
@ -804,7 +842,11 @@ mod tests {
let runner = thread::spawn(move || {
let req = recv::<PluginToHostV4>(&mut runner_stream).unwrap();
match req {
PluginToHostV4::Request { id, payload: PluginRequest::AddEntities(v) } => {
PluginToHostV4::Request {
id,
tab_id: _,
payload: PluginRequest::AddEntities(v),
} => {
assert_eq!(v.len(), 2);
send(
&mut runner_stream,
@ -828,6 +870,56 @@ mod tests {
runner.join().unwrap();
}
#[test]
fn concurrent_requests_match_out_of_order_responses() {
let (host_stream, mut runner_stream) = connect_pair();
let client = V4Client::from_stream_for_test(host_stream);
let sender = Arc::new(V4PluginRequestSender {
shared: Arc::clone(&client.shared),
tab_id: 42,
});
let runner = thread::spawn(move || {
let mut requests = Vec::new();
for _ in 0..2 {
match recv::<PluginToHostV4>(&mut runner_stream).unwrap() {
PluginToHostV4::Request {
id,
tab_id: Some(42),
payload: PluginRequest::PushInfo(message),
} => requests.push((id, message)),
other => panic!("unexpected: {other:?}"),
}
}
for (id, message) in requests.into_iter().rev() {
send(
&mut runner_stream,
&HostToPluginV4::Response {
id,
payload: PluginResponse::Bool(message == "first"),
},
)
.unwrap();
}
});
let first_sender = Arc::clone(&sender);
let first = thread::spawn(move || {
first_sender
.request(PluginRequest::PushInfo("first".to_string()))
.unwrap()
});
let second = thread::spawn(move || {
sender
.request(PluginRequest::PushInfo("second".to_string()))
.unwrap()
});
assert!(matches!(first.join().unwrap(), PluginResponse::Bool(true)));
assert!(matches!(second.join().unwrap(), PluginResponse::Bool(false)));
runner.join().unwrap();
}
#[test]
fn v4_client_notification_queue_size_env_is_honored() {
let _guard = ENV_LOCK.lock().unwrap();

View file

@ -29,7 +29,11 @@ pub enum HostToPluginV4 {
#[derive(Debug, Serialize, Deserialize)]
#[allow(clippy::large_enum_variant)]
pub enum PluginToHostV4 {
Request { id: u64, payload: PluginRequest },
Request {
id: u64,
tab_id: Option<u64>,
payload: PluginRequest,
},
Response { id: u64, payload: HostResponse },
Notification(NotificationEnvelope<PluginNotification>),
}

View file

@ -27,7 +27,11 @@ macro_rules! vlog {
/// A frame delivered from the V4 reader thread to the host main thread.
pub enum HostIncoming {
Response { id: u64, payload: crate::ipc::protocol::HostResponse },
Request { id: u64, payload: Box<crate::ipc::protocol::PluginRequest> },
Request {
id: u64,
tab_id: Option<u64>,
payload: Box<crate::ipc::protocol::PluginRequest>,
},
}
/// Token-bucket rate limiter.
@ -117,9 +121,17 @@ pub fn run_host_reader_thread(
}
}
}
Ok(PluginToHostV4::Request { id, payload }) => {
Ok(PluginToHostV4::Request {
id,
tab_id,
payload,
}) => {
if incoming
.send(HostIncoming::Request { id, payload: Box::new(payload) })
.send(HostIncoming::Request {
id,
tab_id,
payload: Box::new(payload),
})
.is_err()
{
break;

View file

@ -346,20 +346,20 @@ impl PluginProcess {
}
Ok(Err(e)) => return Err(e.into()),
Err(mpsc::RecvTimeoutError::Timeout) => {
let status = spawn_failure_status(&child, &last_stderr);
if let Some(child) = child.lock().unwrap_or_else(|e| e.into_inner()).take() {
reap(child);
}
let status = spawn_failure_status(&child, &last_stderr);
return Err(PluginError::RunnerCrashed {
request: "spawn/accept",
status,
});
}
Err(mpsc::RecvTimeoutError::Disconnected) => {
let status = spawn_failure_status(&child, &last_stderr);
if let Some(child) = child.lock().unwrap_or_else(|e| e.into_inner()).take() {
reap(child);
}
let status = spawn_failure_status(&child, &last_stderr);
return Err(PluginError::RunnerCrashed {
request: "spawn/accept",
status,
@ -740,6 +740,7 @@ impl PluginProcess {
}
std::thread::sleep(Duration::from_millis(50));
}
reap(child);
}
None
}

View file

@ -218,8 +218,10 @@ impl PluginManager {
return false;
};
let plugin = self.plugins.remove(index);
plugin.process.shutdown_and_wait(Duration::from_secs(5));
true
plugin
.process
.shutdown_and_wait(Duration::from_secs(5))
.is_some()
}
/// Begin asynchronous shutdown of every plugin process.

View file

@ -1,14 +1,15 @@
//! Private V4 connection owned by [`PluginProcess`](crate::process::PluginProcess).
use std::collections::VecDeque;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{mpsc, Arc, Mutex};
use std::sync::{mpsc, Arc, Mutex, TryLockError};
use std::time::{Duration, Instant};
use interprocess::local_socket::Stream;
use interprocess::TryClone;
use crate::host::{CommandSource, ExecutionResult, HostApi, HostNotification, PluginNotification};
use crate::ipc::protocol::{HostRequest, HostResponse};
use crate::ipc::protocol::{HostRequest, HostResponse, PluginRequest};
use crate::ipc::server::handle_plugin_request;
use crate::ipc::transport::send;
use crate::ipc::v4::protocol::{HostToPluginV4, NotificationEnvelope};
@ -77,6 +78,8 @@ fn request_kind(req: &HostRequest) -> &'static str {
pub(crate) struct V4Connection {
shared: Arc<V4HostShared>,
incoming: Mutex<mpsc::Receiver<HostIncoming>>,
deferred: Mutex<VecDeque<(u64, Option<u64>, Box<PluginRequest>)>>,
call_lock: Mutex<()>,
next_id: AtomicU64,
reader_handle: Mutex<Option<std::thread::JoinHandle<()>>>,
}
@ -117,6 +120,8 @@ impl V4Connection {
Ok(Self {
shared,
incoming: Mutex::new(incoming_rx),
deferred: Mutex::new(VecDeque::new()),
call_lock: Mutex::new(()),
next_id: AtomicU64::new(1),
reader_handle: Mutex::new(Some(handle)),
})
@ -157,6 +162,7 @@ impl V4Connection {
req: HostRequest,
on_start_interactive: &mut dyn FnMut(u64),
) -> Result<HostResponse, PluginError> {
let _call_guard = self.call_lock.lock().unwrap_or_else(|e| e.into_inner());
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
let kind = request_kind(&req);
let timeout = request_timeout(kind);
@ -188,16 +194,24 @@ impl V4Connection {
Ok(HostIncoming::Response { payload, .. }) => {
return Err(PluginError::UnexpectedResponse(Box::new(payload)))
}
Ok(HostIncoming::Request { id: rid, payload }) => {
let resp = handle_plugin_request(host, *payload, on_start_interactive);
let mut writer = self.shared.writer.lock().unwrap_or_else(|e| e.into_inner());
let stream = writer.as_mut().ok_or_else(|| {
PluginError::Io(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"V4 connection is shut down",
))
})?;
send(stream, &HostToPluginV4::Response { id: rid, payload: resp })?;
Ok(HostIncoming::Request {
id: rid,
tab_id,
payload,
}) => {
if tab_id.map_or(true, |request_tab| request_tab == host.tab_id()) {
self.respond_to_plugin_request(
host,
rid,
payload,
on_start_interactive,
)?;
} else {
self.deferred
.lock()
.unwrap_or_else(|e| e.into_inner())
.push_back((rid, tab_id, payload));
}
}
Err(mpsc::RecvTimeoutError::Timeout) => {
self.shared.alive.store(false, Ordering::SeqCst);
@ -226,19 +240,50 @@ impl V4Connection {
host: &mut dyn HostApi,
on_start_interactive: &mut dyn FnMut(u64),
) -> Result<(), PluginError> {
let _call_guard = match self.call_lock.try_lock() {
Ok(guard) => guard,
Err(TryLockError::Poisoned(error)) => error.into_inner(),
Err(TryLockError::WouldBlock) => return Ok(()),
};
let current_tab_id = host.tab_id();
let mut ready = VecDeque::new();
{
let mut deferred = self.deferred.lock().unwrap_or_else(|e| e.into_inner());
let mut waiting = VecDeque::new();
while let Some((id, tab_id, payload)) = deferred.pop_front() {
if tab_id.map_or(true, |request_tab| request_tab == current_tab_id) {
ready.push_back((id, payload));
} else {
waiting.push_back((id, tab_id, payload));
}
}
*deferred = waiting;
}
while let Some((id, payload)) = ready.pop_front() {
self.respond_to_plugin_request(host, id, payload, on_start_interactive)?;
}
let incoming = self.incoming.lock().unwrap_or_else(|e| e.into_inner());
loop {
match incoming.try_recv() {
Ok(HostIncoming::Request { id, payload }) => {
let resp = handle_plugin_request(host, *payload, on_start_interactive);
let mut writer = self.shared.writer.lock().unwrap_or_else(|e| e.into_inner());
let stream = writer.as_mut().ok_or_else(|| {
PluginError::Io(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"V4 connection is shut down",
))
})?;
send(stream, &HostToPluginV4::Response { id, payload: resp })?;
Ok(HostIncoming::Request {
id,
tab_id,
payload,
}) => {
if tab_id.map_or(true, |request_tab| request_tab == current_tab_id) {
self.respond_to_plugin_request(
host,
id,
payload,
on_start_interactive,
)?;
} else {
self.deferred
.lock()
.unwrap_or_else(|e| e.into_inner())
.push_back((id, tab_id, payload));
}
}
Ok(HostIncoming::Response { .. }) => {
// Responses without a matching active call should not
@ -256,6 +301,31 @@ impl V4Connection {
}
}
fn respond_to_plugin_request(
&self,
host: &mut dyn HostApi,
id: u64,
payload: Box<PluginRequest>,
on_start_interactive: &mut dyn FnMut(u64),
) -> Result<(), PluginError> {
let response = handle_plugin_request(host, *payload, on_start_interactive);
let mut writer = self.shared.writer.lock().unwrap_or_else(|e| e.into_inner());
let stream = writer.as_mut().ok_or_else(|| {
PluginError::Io(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"V4 connection is shut down",
))
})?;
send(
stream,
&HostToPluginV4::Response {
id,
payload: response,
},
)?;
Ok(())
}
/// Send an `ExecuteCode` request and block until the plugin returns the
/// result. The active document tab index is included so the REPL session is
/// tied to the tab that issued it.
@ -532,6 +602,7 @@ mod tests {
&mut runner_stream,
&PluginToHostV4::Request {
id: 99,
tab_id: None,
payload: PluginRequest::PushInfo("nested".to_string()),
},
)

View file

@ -89,16 +89,14 @@ fn run_v4(
}));
}
match client.recv_runner_frame() {
match client.recv_runner_frame_timeout(std::time::Duration::from_millis(50)) {
Ok(RunnerFrame::Request { id, payload }) => {
if let Some(resp) = handle_host_request_v4(&mut *plugin, interactive, &client, id, payload) {
client.send_response(id, resp)?;
}
}
Err(_) => {
// Host disconnected.
break;
}
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => continue,
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
}
}
Ok(())

View file

@ -8,7 +8,7 @@ publish = false
crate-type = ["cdylib"]
[dependencies]
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "4736846", features = ["serde"] }
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "c28b2cd", features = ["serde"] }
bincode = "1.3"
serde = { version = "1", features = ["derive"] }
console_error_panic_hook = "0.1"

View file

@ -12,6 +12,13 @@ language =
.portuguese = Português (Brasil)
.arabic = العربية
.japanese = 日本語
.korean = 한국어
.czech = Čeština
.italian = Italiano
.finnish = Suomi
.hungarian = Magyar
.polish = Polski
.chinese-traditional = 繁體中文
action =
.close = إغلاق

File diff suppressed because it is too large Load diff

View file

@ -12,6 +12,13 @@ language =
.portuguese = Português (Brasil)
.arabic = العربية
.japanese = 日本語
.korean = 한국어
.czech = Čeština
.italian = Italiano
.finnish = Suomi
.hungarian = Magyar
.polish = Polski
.chinese-traditional = 繁體中文
action =
.close = Schließen

View file

@ -12,6 +12,13 @@ language =
.portuguese = Português (Brasil)
.arabic = العربية
.japanese = 日本語
.korean = 한국어
.czech = Čeština
.italian = Italiano
.finnish = Suomi
.hungarian = Magyar
.polish = Polski
.chinese-traditional = 繁體中文
action =
.close = Close
@ -547,7 +554,7 @@ common =
.layer-state-name-cannot-be-empty = Layer state name cannot be empty.
.layers = Layers
.layout = Layout
.layout-name-silindi = Layout "__ocs_fmt_0__" silindi
.layout-name-silindi = Layout "__ocs_fmt_0__" deleted
.layout-new-name-created-use-mview-to-add-a-vie = Layout "__ocs_fmt_0__" created — use MVIEW to add a viewport
.layout-orig-new-name = Layout "__ocs_fmt_0__" → "__ocs_fmt_1__"
.layout-name-created = Layout '__ocs_fmt_0__' created.

View file

@ -12,6 +12,13 @@ language =
.portuguese = Português (Brasil)
.arabic = العربية
.japanese = 日本語
.korean = 한국어
.czech = Čeština
.italian = Italiano
.finnish = Suomi
.hungarian = Magyar
.polish = Polski
.chinese-traditional = 繁體中文
action =
.close = Cerrar

File diff suppressed because it is too large Load diff

View file

@ -12,6 +12,13 @@ language =
.portuguese = Português (Brasil)
.arabic = العربية
.japanese = 日本語
.korean = 한국어
.czech = Čeština
.italian = Italiano
.finnish = Suomi
.hungarian = Magyar
.polish = Polski
.chinese-traditional = 繁體中文
action =
.close = Fermer

View file

@ -12,6 +12,13 @@ language =
.portuguese = Português (Brasil)
.arabic = العربية
.japanese = 日本語
.korean = 한국어
.czech = Čeština
.italian = Italiano
.finnish = Suomi
.hungarian = Magyar
.polish = Polski
.chinese-traditional = 繁體中文
action =
.close = बंद करें

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -12,6 +12,13 @@ language =
.portuguese = Português (Brasil)
.arabic = العربية
.japanese = 日本語
.korean = 한국어
.czech = Čeština
.italian = Italiano
.finnish = Suomi
.hungarian = Magyar
.polish = Polski
.chinese-traditional = 繁體中文
action =
.close = 閉じる

File diff suppressed because it is too large Load diff

View file

@ -12,6 +12,13 @@ language =
.portuguese = Português (Brasil)
.arabic = العربية
.japanese = 日本語
.korean = 한국어
.czech = Čeština
.italian = Italiano
.finnish = Suomi
.hungarian = Magyar
.polish = Polski
.chinese-traditional = 繁體中文
action =
.close = Sluiten

File diff suppressed because it is too large Load diff

View file

@ -12,6 +12,13 @@ language =
.portuguese = Português (Brasil)
.arabic = العربية
.japanese = 日本語
.korean = 한국어
.czech = Čeština
.italian = Italiano
.finnish = Suomi
.hungarian = Magyar
.polish = Polski
.chinese-traditional = 繁體中文
action =
.close = Fechar

View file

@ -12,6 +12,13 @@ language =
.portuguese = Português (Brasil)
.arabic = العربية
.japanese = 日本語
.korean = 한국어
.czech = Čeština
.italian = Italiano
.finnish = Suomi
.hungarian = Magyar
.polish = Polski
.chinese-traditional = 繁體中文
action =
.close = Замкнуть

View file

@ -12,6 +12,13 @@ language =
.portuguese = Português (Brasil)
.arabic = العربية
.japanese = 日本語
.korean = 한국어
.czech = Čeština
.italian = Italiano
.finnish = Suomi
.hungarian = Magyar
.polish = Polski
.chinese-traditional = 繁體中文
action =
.close = Kapat

View file

@ -12,6 +12,13 @@ language =
.portuguese = Português (Brasil)
.arabic = العربية
.japanese = 日本語
.korean = 한국어
.czech = Čeština
.italian = Italiano
.finnish = Suomi
.hungarian = Magyar
.polish = Polski
.chinese-traditional = 繁體中文
action =
.close = 关闭

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Generate OpenCADStudio-branded GitHub star-history SVG charts."""
"""Generate OpenCADStudio GitHub growth SVG charts."""
from __future__ import annotations
@ -16,11 +16,15 @@ from xml.sax.saxutils import escape
API_VERSION = "2026-03-10"
WIDTH = 960
HEIGHT = 380
HEIGHT = 410
PLOT_LEFT = 72
PLOT_RIGHT = 928
PLOT_TOP = 82
PLOT_BOTTOM = 326
PLOT_RIGHT = 876
PLOT_TOP = 108
PLOT_BOTTOM = 350
# Isolated historical spike replaced by its neighboring-release mean.
DOWNLOAD_OVERRIDES = {"v0.4.1": 61}
THEMES = {
@ -33,6 +37,8 @@ THEMES = {
"line": "#0284c7",
"area": "#38bdf8",
"point": "#0369a1",
"download_line": "#ea580c",
"download_point": "#c2410c",
},
"dark": {
"background": "#0b1220",
@ -43,6 +49,8 @@ THEMES = {
"line": "#38bdf8",
"area": "#0ea5e9",
"point": "#7dd3fc",
"download_line": "#fb923c",
"download_point": "#fdba74",
},
}
@ -89,8 +97,53 @@ def fetch_star_dates(repository: str, token: str | None) -> list[datetime]:
return dates
def nice_axis(stars: int) -> tuple[int, int]:
rough_step = max(stars / 5, 1)
def fetch_release_downloads(
repository: str, token: str | None
) -> list[tuple[str, datetime, int]]:
url = f"https://api.github.com/repos/{repository}/releases?per_page=100"
headers = {
"Accept": "application/vnd.github+json",
"User-Agent": "OpenCADStudio-growth-history",
"X-GitHub-Api-Version": API_VERSION,
}
if token:
headers["Authorization"] = f"Bearer {token}"
releases: list[tuple[str, datetime, int]] = []
for _ in range(100):
request = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(request, timeout=30) as response:
payload = json.load(response)
link = response.headers.get("Link")
for release in payload:
published_at = release.get("published_at")
if release.get("draft") or not published_at:
continue
tag = str(release.get("tag_name", ""))
downloads = sum(
int(asset.get("download_count", 0)) for asset in release.get("assets", [])
)
releases.append(
(
tag,
datetime.fromisoformat(published_at.replace("Z", "+00:00")),
DOWNLOAD_OVERRIDES.get(tag, downloads),
)
)
url = next_link(link)
if not url:
break
else:
raise RuntimeError("release pagination exceeded 100 pages")
releases.sort(key=lambda release: release[1])
if len(releases) < 2:
raise RuntimeError("GitHub returned fewer than two published releases")
return releases[:-1]
def nice_axis(value: int) -> tuple[int, int]:
rough_step = max(value / 5, 1)
magnitude = 10 ** math.floor(math.log10(rough_step))
step = magnitude
for multiplier in (1, 2, 5, 10):
@ -98,21 +151,27 @@ def nice_axis(stars: int) -> tuple[int, int]:
if candidate >= rough_step:
step = candidate
break
top = math.ceil(stars / step) * step
if top <= stars:
top = math.ceil(value / step) * step
if top <= value:
top += step
return int(top), int(step)
def render_svg(repository: str, dates: list[datetime], theme: str) -> str:
def render_svg(
repository: str,
dates: list[datetime],
releases: list[tuple[str, datetime, int]],
theme: str,
) -> str:
colors = THEMES[theme]
now = datetime.now(timezone.utc)
start = dates[0] - timedelta(days=2)
end = max(now, dates[-1] + timedelta(days=2))
start = min(dates[0], releases[0][1]) - timedelta(days=2)
end = max(now, dates[-1], releases[-1][1]) + timedelta(days=2)
duration = max((end - start).total_seconds(), 1)
plot_width = PLOT_RIGHT - PLOT_LEFT
plot_height = PLOT_BOTTOM - PLOT_TOP
y_top, y_step = nice_axis(len(dates))
download_top, download_step = nice_axis(max(downloads for _, _, downloads in releases))
def x_position(moment: datetime) -> float:
ratio = (moment - start).total_seconds() / duration
@ -121,11 +180,21 @@ def render_svg(repository: str, dates: list[datetime], theme: str) -> str:
def y_position(stars: int) -> float:
return PLOT_BOTTOM - (stars / y_top) * plot_height
def download_y_position(downloads: int) -> float:
return PLOT_BOTTOM - (downloads / download_top) * plot_height
line_parts = [f"M {PLOT_LEFT:.1f} {PLOT_BOTTOM:.1f}"]
for count, moment in enumerate(dates, 1):
line_parts.append(f"H {x_position(moment):.1f} V {y_position(count):.1f}")
line_path = " ".join(line_parts)
area_path = f"{line_path} V {PLOT_BOTTOM:.1f} H {PLOT_LEFT:.1f} Z"
download_path = " ".join(
(
"M" if index == 0 else "L"
)
+ f" {x_position(published):.1f} {download_y_position(downloads):.1f}"
for index, (_, published, downloads) in enumerate(releases)
)
grid = []
for value in range(0, y_top + 1, y_step):
@ -139,6 +208,18 @@ def render_svg(repository: str, dates: list[datetime], theme: str) -> str:
f'fill="{colors["muted"]}" font-size="12">{value}</text>'
)
download_axis = []
for value in range(0, download_top + 1, download_step):
y = download_y_position(value)
download_axis.append(
f'<line x1="{PLOT_RIGHT}" y1="{y:.1f}" x2="{PLOT_RIGHT + 6}" y2="{y:.1f}" '
f'stroke="{colors["download_line"]}" stroke-width="1" />'
)
download_axis.append(
f'<text x="{PLOT_RIGHT + 12}" y="{y + 4:.1f}" text-anchor="start" '
f'fill="{colors["download_line"]}" font-size="12">{value}</text>'
)
x_labels = []
for index in range(5):
moment = start + (end - start) * (index / 4)
@ -156,11 +237,24 @@ def render_svg(repository: str, dates: list[datetime], theme: str) -> str:
f'fill="{colors["muted"]}" font-size="12">{escape(label)}</text>'
)
title = f"{repository.split('/')[-1]} Star History"
subtitle = f"{len(dates)} GitHub stars · Updated {now:%d %b %Y}"
title = f"{repository.split('/')[-1]} Growth"
total_downloads = sum(downloads for _, _, downloads in releases)
subtitle = (
f"{len(dates)} GitHub stars · {total_downloads:,} cleaned asset downloads across "
f"{len(releases)} releases · Latest excluded · Updated {now:%d %b %Y}"
)
last_x = x_position(dates[-1])
last_y = y_position(len(dates))
label_y = max(PLOT_TOP + 16, last_y - 14)
last_release, last_published, last_downloads = releases[-1]
last_release_x = x_position(last_published)
last_release_y = download_y_position(last_downloads)
download_points = "".join(
f'<circle cx="{x_position(published):.1f}" cy="{download_y_position(downloads):.1f}" '
f'r="2.5" fill="{colors["download_point"]}" />'
for _, published, downloads in releases
)
return f'''<svg xmlns="http://www.w3.org/2000/svg" width="{WIDTH}" height="{HEIGHT}" viewBox="0 0 {WIDTH} {HEIGHT}" role="img" aria-labelledby="title description">
<title id="title">{escape(title)}</title>
@ -175,12 +269,24 @@ def render_svg(repository: str, dates: list[datetime], theme: str) -> str:
<g font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif">
<text x="{PLOT_LEFT}" y="34" fill="{colors["text"]}" font-size="21" font-weight="700">{escape(title)}</text>
<text x="{PLOT_LEFT}" y="58" fill="{colors["muted"]}" font-size="13">{escape(subtitle)}</text>
<line x1="{PLOT_LEFT}" y1="82" x2="{PLOT_LEFT + 24}" y2="82" stroke="{colors["line"]}" stroke-width="3" />
<text x="{PLOT_LEFT + 32}" y="86" fill="{colors["text"]}" font-size="12">GitHub stars</text>
<line x1="{PLOT_LEFT + 138}" y1="82" x2="{PLOT_LEFT + 162}" y2="82" stroke="{colors["download_line"]}" stroke-width="3" />
<circle cx="{PLOT_LEFT + 150}" cy="82" r="3" fill="{colors["download_point"]}" />
<text x="{PLOT_LEFT + 170}" y="86" fill="{colors["text"]}" font-size="12">Release downloads</text>
<text x="{PLOT_LEFT}" y="{PLOT_TOP - 8}" fill="{colors["line"]}" font-size="12">Stars</text>
<text x="{PLOT_RIGHT}" y="{PLOT_TOP - 8}" text-anchor="end" fill="{colors["download_line"]}" font-size="12">Downloads / release</text>
{''.join(grid)}
{''.join(download_axis)}
{''.join(x_labels)}
<path d="{area_path}" fill="url(#star-area)" />
<path d="{line_path}" fill="none" stroke="{colors["line"]}" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
<path d="{download_path}" fill="none" stroke="{colors["download_line"]}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
{download_points}
<circle cx="{last_x:.1f}" cy="{last_y:.1f}" r="5" fill="{colors["point"]}" stroke="{colors["background"]}" stroke-width="3" />
<text x="{PLOT_RIGHT}" y="{label_y:.1f}" text-anchor="end" fill="{colors["text"]}" font-size="14" font-weight="700">{len(dates)} stars</text>
<circle cx="{last_release_x:.1f}" cy="{last_release_y:.1f}" r="4.5" fill="{colors["download_point"]}" stroke="{colors["background"]}" stroke-width="2" />
<text x="{last_release_x - 28:.1f}" y="{min(PLOT_BOTTOM - 8, last_release_y + 20):.1f}" text-anchor="end" fill="{colors["download_line"]}" font-size="13" font-weight="700">{escape(last_release)} · {last_downloads}</text>
</g>
</svg>
'''
@ -197,11 +303,17 @@ def main() -> None:
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
dates = fetch_star_dates(args.repository, token)
releases = fetch_release_downloads(args.repository, token)
args.output_dir.mkdir(parents=True, exist_ok=True)
for theme in THEMES:
output = args.output_dir / f"star-history-{theme}.svg"
output.write_text(render_svg(args.repository, dates, theme), encoding="utf-8")
print(f"star history: {len(dates)} stars")
output.write_text(
render_svg(args.repository, dates, releases, theme), encoding="utf-8"
)
print(
f"growth history: {len(dates)} stars, "
f"{sum(downloads for _, _, downloads in releases)} downloads"
)
if __name__ == "__main__":

View file

@ -14,7 +14,15 @@ impl OpenCADStudio {
true
}
fn refresh_area_preview(&mut self, i: usize) {
pub(super) fn refresh_area_preview(&mut self, i: usize) {
let hatches = self.tabs[i]
.active_cmd
.as_ref()
.and_then(|command| command.hatch_preview_models());
if let Some(hatches) = hatches {
self.tabs[i].scene.set_command_preview_hatches(hatches);
return;
}
let regions = self.tabs[i]
.active_cmd
.as_ref()
@ -785,6 +793,24 @@ impl OpenCADStudio {
self.commit_undo_delta(i, pd);
}
}
CmdResult::CommitEntitiesAndExit(entities) => {
let label = self.history_label_from_active_cmd(i, "ENTITY");
let delta_safe = entities
.iter()
.all(|entity| self.delta_add_safe(i, entity));
let pending = self.begin_undo(i, label, entities.len(), delta_safe);
for entity in entities {
self.commit_entity(entity);
}
self.tabs[i].dirty = true;
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.restore_pre_cmd_tangent();
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
}
CmdResult::MviewCreate {
viewport,
preserve_view,
@ -1176,10 +1202,27 @@ impl OpenCADStudio {
// Link the leader to its annotation so the pair edits as a unit
// (double-click on the leader resolves to the text entity).
if let (Some(lh), Some(ah)) = (leader_handle, edit_handle) {
if let Some(acadrust::EntityType::Leader(l)) =
let linked = if let Some(acadrust::EntityType::Leader(l)) =
self.tabs[i].scene.document.get_entity_mut(lh)
{
l.annotation_handle = ah;
true
} else {
false
};
if linked {
// The LEADER may already have received its annotation context while
// annotation_handle was still NULL. Refresh it now that the MTEXT link
// is known so the context represents the finished leader.
self.tabs[i]
.scene
.sync_displayed_annotation_context(lh);
self.tabs[i].scene.bump_entities(&[(
lh,
crate::scene::ChangeKind::Modified,
)]);
}
}
self.tabs[i].dirty = true;
@ -1246,7 +1289,67 @@ impl OpenCADStudio {
let label = self.history_label_from_active_cmd(i, "HATCH");
let pending = self.begin_undo(i, label, 1, true);
let layer = self.tabs[i].active_layer.clone();
let new_handle = self.tabs[i].scene.add_hatch(hatch, Some(&layer));
let new_handle = self.tabs[i].scene.add_hatch(hatch, Some(&layer), None);
if !new_handle.is_null() {
self.tabs[i].scene.select_entity(new_handle, true);
}
self.tabs[i].dirty = true;
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.restore_pre_cmd_tangent();
self.refresh_properties();
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
}
CmdResult::CommitStyledHatch {
hatch,
color,
transparency,
} => {
let label = self.history_label_from_active_cmd(i, "HATCH");
let pending = self.begin_undo(i, label, 1, true);
let layer = self.tabs[i].active_layer.clone();
let new_handle = self.tabs[i].scene.add_hatch(
hatch,
Some(&layer),
Some((color, transparency)),
);
if !new_handle.is_null() {
self.tabs[i].scene.select_entity(new_handle, true);
}
self.tabs[i].dirty = true;
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.restore_pre_cmd_tangent();
self.refresh_properties();
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
}
CmdResult::CommitHatchWithBoundaries {
mut hatch,
boundaries,
entity_style,
} => {
let label = self.history_label_from_active_cmd(i, "HATCH");
let pending = self.begin_undo(i, label, boundaries.len() + 1, true);
let mut sources = Vec::with_capacity(boundaries.len());
for boundary in boundaries {
let handles = self
.commit_entity_handle(boundary)
.into_iter()
.collect::<Vec<_>>();
sources.push(handles);
}
hatch.boundary_sources = Some(std::sync::Arc::new(sources));
let layer = self.tabs[i].active_layer.clone();
let new_handle =
self.tabs[i]
.scene
.add_hatch(hatch, Some(&layer), entity_style);
if !new_handle.is_null() {
self.tabs[i].scene.select_entity(new_handle, true);
}
@ -3072,7 +3175,7 @@ impl OpenCADStudio {
// Remove old hatch (entity + GPU model)
self.tabs[i].scene.erase_entities(&[handle]);
// Re-add with updated model
self.tabs[i].scene.add_hatch(model, Some(&layer));
self.tabs[i].scene.add_hatch(model, Some(&layer), None);
self.tabs[i].dirty = true;
self.command_line.push_output(crate::t!("HATCHEDIT: hatch updated.").as_ref());
} else {
@ -3367,6 +3470,45 @@ impl OpenCADStudio {
_ => glam::DVec3::ZERO,
};
self.merge_clipboard_ext_objects(i, &by_index, annotation_delta);
// Source handles stored in the clipboard map one-to-one to the freshly
// pasted handles. Use that map to reconnect LEADER -> copied annotation.
let mut handle_map = rustc_hash::FxHashMap::default();
for (source, &copied) in self.clipboard.iter().zip(by_index.iter()) {
if !copied.is_null() {
handle_map.insert(source.common().handle, copied);
}
}
let leader_links: Vec<(Handle, Handle)> = self
.clipboard
.iter()
.filter_map(|source| {
let acadrust::EntityType::Leader(leader) = source else {
return None;
};
let copied_leader = handle_map.get(&source.common().handle).copied()?;
let copied_annotation = handle_map
.get(&leader.annotation_handle)
.copied()
.unwrap_or(Handle::NULL);
Some((copied_leader, copied_annotation))
})
.collect();
for (leader_handle, annotation_handle) in leader_links {
if let Some(acadrust::EntityType::Leader(leader)) =
self.tabs[i].scene.document.get_entity_mut(leader_handle)
{
leader.annotation_handle = annotation_handle;
}
let _ = self.tabs[i]
.scene
.sync_displayed_annotation_context(leader_handle);
}
// Recreate any group whose whole membership was copied, so a pasted
// group stays grouped — cross-drawing too, since the groups were
// snapshotted into the clipboard at copy time. `by_index` is aligned
@ -3374,12 +3516,6 @@ impl OpenCADStudio {
// its clipboard clone to its new handle. Same shared `recreate_groups`
// the in-drawing COPY path uses. (#440)
if !self.clipboard_deps.groups.is_empty() {
let mut handle_map = rustc_hash::FxHashMap::default();
for (src, &new) in self.clipboard.iter().zip(by_index.iter()) {
if !new.is_null() {
handle_map.insert(src.common().handle, new);
}
}
let groups = self.clipboard_deps.groups.clone();
self.tabs[i].scene.recreate_groups(groups, &handle_map);
}

View file

@ -619,9 +619,25 @@ impl OpenCADStudio {
"HATCH" => {
use crate::modules::draw::draw::hatch::HatchCommand;
let outlines = self.tabs[i].scene.hatch_boundary_outlines();
let new_cmd = HatchCommand::new(outlines);
let boundary_sources = self.tabs[i].scene.hatch_boundary_sources();
let selected = self.tabs[i]
.scene
.selected_entities()
.into_iter()
.map(|(handle, _)| handle)
.collect::<Vec<_>>();
let inherited = selected
.iter()
.find_map(|handle| {
let model = self.tabs[i].scene.hatches.get(handle)?.clone();
let common = self.tabs[i].scene.document.get_entity(*handle)?.common();
Some((model, common.color.clone(), common.transparency))
});
let new_cmd =
HatchCommand::new(outlines, boundary_sources, selected, inherited);
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
self.refresh_area_preview(i);
}
"HATCHEDIT" => {

View file

@ -1881,6 +1881,8 @@ impl OpenCADStudio {
// INSERT reference; refuses anonymous/xref sources and
// invalid/taken names.
self.tabs[i].scene.rename_block(&old_name, &new_name)
} else if type_str == "LAYER" {
self.tabs[i].rename_layer(&old_name, &new_name)
} else if known {
rename_symbol(
&mut self.tabs[i].scene.document,
@ -1892,14 +1894,8 @@ impl OpenCADStudio {
false
};
if ok {
// Renamed references resolve at tessellation time —
// rebuild so nothing renders off a stale name.
self.tabs[i].scene.bump_geometry_no_blocks();
// The per-tab active layer tracks the name.
if type_str == "LAYER"
&& self.tabs[i].active_layer.eq_ignore_ascii_case(&old_name)
{
self.tabs[i].active_layer = new_name.clone();
if type_str != "LAYER" {
self.tabs[i].scene.bump_geometry_no_blocks();
}
if type_str == "UCS" {
if let Some(active) = self.tabs[i].active_ucs.as_mut() {
@ -1910,6 +1906,9 @@ impl OpenCADStudio {
}
}
self.tabs[i].dirty = true;
if type_str == "LAYER" {
self.refresh_layer_panel();
}
self.command_line
.push_output(crate::tf!("RENAME: '{}' → '{}'.", old_name, new_name).as_ref());
} else {
@ -2352,13 +2351,7 @@ impl OpenCADStudio {
}
}
/// RENAME for the name-keyed symbol tables (layer / text style / dim style /
/// linetype / UCS / view). `Table<T>` stores entries in a map keyed by
/// UPPERCASE name, so a rename must remove + re-add — mutating `.name` in
/// place leaves the map keyed by the old name and every later
/// `get(new_name)` lookup silently fails. Name-based references (entity
/// layer/linetype/style fields, layer linetypes, header current-* names) are
/// chased case-insensitively, matching how the tables resolve names.
/// RENAME for the remaining name-keyed symbol tables.
fn rename_symbol(doc: &mut acadrust::CadDocument, ty: &str, old: &str, new: &str) -> bool {
use acadrust::{EntityType, Table, TableEntry};
@ -2366,38 +2359,10 @@ fn rename_symbol(doc: &mut acadrust::CadDocument, ty: &str, old: &str, new: &str
if !crate::scene::valid_block_name(new) {
return false;
}
// A case-only rename reuses its own key; any other target must be free.
if !old.eq_ignore_ascii_case(new) && table.contains(new) {
return false;
}
let Some(mut e) = table.remove(old) else {
return false;
};
e.set_name(new.to_string());
let _ = table.add(e);
true
table.rename(old, new.to_string()).is_ok()
}
match ty {
"LAYER" => {
// Layer 0 and Defpoints are fixed names; xref-dependent ("|")
// layers belong to the referenced file.
if old == "0" || old.eq_ignore_ascii_case("Defpoints") || old.contains('|') {
return false;
}
if !rekey(&mut doc.layers, old, new) {
return false;
}
for e in doc.entities_mut() {
if e.common().layer.eq_ignore_ascii_case(old) {
e.common_mut().layer = new.to_string();
}
}
if doc.header.current_layer_name.eq_ignore_ascii_case(old) {
doc.header.current_layer_name = new.to_string();
}
true
}
"STYLE" | "TEXTSTYLE" => {
if !rekey(&mut doc.text_styles, old, new) {
return false;

View file

@ -7,7 +7,7 @@ use crate::scene::GripDef;
use crate::scene::{ObjectIsolationState, Scene};
use crate::snap::SnapResult;
use crate::ui::{LayerPanel, PropertiesPanel};
use acadrust::tables::Ucs;
use acadrust::tables::{normalize_name, Ucs};
use acadrust::{CadDocument, EntityType, Handle};
use iced;
use crate::t;
@ -222,6 +222,18 @@ pub(super) struct DocumentTab {
}
impl DocumentTab {
pub(super) fn rename_layer(&mut self, old_name: &str, new_name: &str) -> bool {
let active = normalize_name(&self.active_layer) == normalize_name(old_name);
if !self.scene.rename_layer(old_name, new_name) {
return false;
}
if active {
self.active_layer = new_name.to_string();
}
self.dirty = true;
true
}
pub(super) fn active_block_edit_session(&self) -> Option<&BlockEditSession> {
self.active_block_edit
.and_then(|index| self.block_edits.get(index))
@ -546,6 +558,11 @@ impl HistorySnapshot {
)
.saturating_add(d.selected_before.len().saturating_mul(16))
.saturating_add(d.selected_after.len().saturating_mul(16))
.saturating_add(
d.active_layer
.as_ref()
.map_or(0, |(before, after)| before.len().saturating_add(after.len())),
)
.saturating_add(d.label.len()),
HistorySnapshot::ObjectVisibility(v) => v
.before
@ -598,6 +615,7 @@ pub(super) struct DeltaSnapshot {
pub(super) selected_after: Vec<Handle>,
pub(super) dirty_before: bool,
pub(super) dirty_after: bool,
pub(super) active_layer: Option<(String, String)>,
/// Opposite non-entity document state. `apply_delta_state` swaps this with
/// the live structure, so the same allocation shuttles between undo/redo.
pub(super) structure: Option<StructureSnapshot>,
@ -671,6 +689,7 @@ pub(super) struct HistoryState {
pub(super) struct PendingHistorySnapshot {
pub(super) label: String,
pub(super) current_layout: String,
pub(super) active_layer: String,
pub(super) selected_before: Vec<Handle>,
pub(super) dirty_before: bool,
pub(super) structure_before: CadDocument,

View file

@ -223,6 +223,7 @@ impl OpenCADStudio {
selected_after: selected,
dirty_before,
dirty_after: true,
active_layer: None,
structure: (!objects.is_empty()).then_some(StructureSnapshot::Objects(objects)),
label: label.into(),
};
@ -313,10 +314,12 @@ impl OpenCADStudio {
.collect();
let selected_after: Vec<Handle> = self.tabs[i].scene.selected.iter().copied().collect();
let dirty_after = self.tabs[i].dirty;
let active_layer_after = self.tabs[i].active_layer.clone();
if entities.is_empty()
&& !structure_changed
&& pending.selected_before == selected_after
&& pending.dirty_before == dirty_after
&& pending.active_layer == active_layer_after
{
return;
}
@ -328,6 +331,8 @@ impl OpenCADStudio {
selected_after,
dirty_before: pending.dirty_before,
dirty_after,
active_layer: (pending.active_layer != active_layer_after)
.then_some((pending.active_layer, active_layer_after)),
structure: structure_changed.then_some(StructureSnapshot::Full(pending.structure_before)),
label: pending.label,
};
@ -351,6 +356,7 @@ impl OpenCADStudio {
self.tabs[i].history.pending = Some(PendingHistorySnapshot {
label,
current_layout,
active_layer: self.tabs[i].active_layer.clone(),
selected_before,
dirty_before,
structure_before,
@ -433,6 +439,7 @@ impl OpenCADStudio {
selected_after,
dirty_before: pending.dirty_before,
dirty_after: self.tabs[i].dirty,
active_layer: None,
structure: Some(StructureSnapshot::Layers(entries)),
label: pending.label,
};
@ -487,6 +494,7 @@ impl OpenCADStudio {
selected_after: self.tabs[i].scene.selected.iter().copied().collect(),
dirty_before: pending.dirty_before,
dirty_after: self.tabs[i].dirty,
active_layer: None,
structure: Some(StructureSnapshot::TextStyles(entries)),
label: pending.label,
};
@ -541,6 +549,7 @@ impl OpenCADStudio {
selected_after: self.tabs[i].scene.selected.iter().copied().collect(),
dirty_before: pending.dirty_before,
dirty_after: self.tabs[i].dirty,
active_layer: None,
structure: Some(StructureSnapshot::DimStyles(entries)),
label: pending.label,
};
@ -603,6 +612,7 @@ impl OpenCADStudio {
selected_after: self.tabs[i].scene.selected.iter().copied().collect(),
dirty_before: pending.dirty_before,
dirty_after: self.tabs[i].dirty,
active_layer: None,
structure: Some(StructureSnapshot::Objects(entries)),
label: pending.label,
};
@ -629,6 +639,7 @@ impl OpenCADStudio {
selected_after: self.tabs[i].scene.selected.iter().copied().collect(),
dirty_before,
dirty_after: true,
active_layer: None,
structure: Some(StructureSnapshot::Styles {
before,
after,
@ -754,6 +765,7 @@ impl OpenCADStudio {
selected_after,
dirty_before: pending.dirty_before,
dirty_after,
active_layer: None,
structure,
label: pending.label,
};
@ -908,6 +920,9 @@ impl OpenCADStudio {
} else {
d.current_layout_after.clone()
};
if let Some((before, after)) = &d.active_layer {
self.tabs[i].active_layer = if undo { before } else { after }.clone();
}
self.tabs[i].dirty = dirty;
changes
}
@ -1055,7 +1070,7 @@ impl OpenCADStudio {
structure,
StructureSnapshot::Full(_) | StructureSnapshot::Layers(_)
)
}) || d.entities.iter().any(|(_, before, after)| {
}) || d.active_layer.is_some() || d.entities.iter().any(|(_, before, after)| {
before
.as_deref()
.is_some_and(|entity| matches!(entity, EntityType::Viewport(_)))
@ -1110,7 +1125,7 @@ impl OpenCADStudio {
structure,
StructureSnapshot::Full(_) | StructureSnapshot::Layers(_)
)
}) || d.entities.iter().any(|(_, before, after)| {
}) || d.active_layer.is_some() || d.entities.iter().any(|(_, before, after)| {
before
.as_deref()
.is_some_and(|entity| matches!(entity, EntityType::Viewport(_)))

View file

@ -2365,6 +2365,17 @@ pub enum Message {
PropAttrInput { tag: String, value: String },
/// User committed a block-attribute value edit (Enter pressed).
PropAttrCommit(String),
/// Reports the currently keyboard-focused widget (if any) after a
/// [`sync_active_field_task`](crate::ui::properties::sync_active_field_task)
/// sweep, so the update handler can keep the active-row marker reconciled
/// against real focus instead of pointer hover.
PropSyncActive(Option<iced::widget::Id>),
/// A left mouse button was pressed anywhere. The clicked widget has already
/// been given focus by the time this arrives (the widget tree processes the
/// event before the runtime broadcasts it to subscriptions), so a focus
/// sweep — which resolves as `PropSyncActive` — reveals whether a property
/// value field was clicked and can select its whole value.
PropPointerPressed,
/// Toggle the inline color picker dropdown open/closed.
PropColorPickerToggle,
/// Toggle the MTEXT background-colour picker dropdown open/closed.

View file

@ -705,7 +705,7 @@ impl super::OpenCADStudio {
// it keeps the layout's shape, wrapping and alignment identical.
mt.insertion_point = Vector3::new(0.0, 0.0, 0.0);
let entity = EntityType::MText(mt.clone());
let anno = self.tabs[i].scene.annotation_scale;
let anno = 1.0;
let bg = self.tabs[i].scene.bg_color;
let wires: Vec<WireModel> = tessellate::tessellate(
&self.tabs[i].scene.document,

View file

@ -37,12 +37,17 @@ impl<'a> HostSession<'a> {
#[cfg(not(target_arch = "wasm32"))]
pub fn document_view_v4(&mut self, tab_id: u64) -> Option<ocs_plugin_api::shm::DocumentViewInfo> {
if tab_id != self.tab_id() {
return None;
}
v4_support::open_document_view_v4(tab_id, self.document())
}
#[cfg(not(target_arch = "wasm32"))]
pub fn close_document_view_v4(&mut self, tab_id: u64) {
v4_support::close_document_view_v4(tab_id);
if tab_id == self.tab_id() {
v4_support::close_document_view_v4(tab_id);
}
}
pub fn document(&self) -> &CadDocument {

View file

@ -42,6 +42,7 @@ impl OpenCADStudio {
// closes, matching the deselect / reselect / click-away expectation.
let color_palette_open = self.tabs[i].properties.color_palette_open;
let edit_buf = std::mem::take(&mut self.tabs[i].properties.edit_buf);
let active_field = std::mem::take(&mut self.tabs[i].properties.active_field);
// Expanded coordinate groups persist across rebuilds AND selection
// changes — it's a per-user view preference, not per-entity state.
let expanded_groups = std::mem::take(&mut self.tabs[i].properties.expanded_groups);
@ -1431,6 +1432,9 @@ impl OpenCADStudio {
}
}
};
// Precompute the focused-id → field-key map for O(1) lookups on
// `PropSyncActive`; derived from `sections`, so rebuild it here.
panel.field_key_by_id = crate::ui::properties::build_field_key_map(&panel.sections);
panel.color_palette_open = color_palette_open;
let new_handles: Vec<acadrust::Handle> = selected.iter().map(|(h, _)| *h).collect();
// Carry the in-progress edits only when the selection is unchanged
@ -1441,6 +1445,14 @@ impl OpenCADStudio {
} else {
Default::default()
};
// The active-row highlight only survives a rebuild for the same
// selection (like the edit buffer); a selection change clears it so
// an old row isn't marked active against new content.
panel.active_field = if prev_handles == new_handles {
active_field
} else {
None
};
panel.expanded_groups = expanded_groups;
panel.source_handles = new_handles;
panel.prop_vertex = prop_vertex;
@ -1457,7 +1469,12 @@ impl OpenCADStudio {
.all(|handle| self.tabs[i].scene.is_layer_locked(*handle));
if locked_only {
make_sections_read_only(&mut panel.sections);
// Rows demoted to read-only no longer back an editable field;
// drop them from the id→key map so focus can't map onto them.
panel.field_key_by_id =
crate::ui::properties::build_field_key_map(&panel.sections);
panel.edit_buf.clear();
panel.active_field = None;
panel.color_picker_open = false;
panel.color_palette_open = false;
panel.bg_color_picker_open = false;
@ -1618,6 +1635,49 @@ impl OpenCADStudio {
annotation_scale_handle,
);
let mut entity_grips = dispatch::grips(contextual.as_ref());
// Dimension::grips() cannot see the document, so an automatic dimension
// text grip cannot resolve its real DIMSTYLE/annotation-scaled position
// there. Correct it here, where both the document and displayed annotation
// scale are available.
if let acadrust::EntityType::Dimension(dim) = contextual.as_ref() {
if matches!(
dim,
acadrust::entities::Dimension::Linear(_)
| acadrust::entities::Dimension::Aligned(_)
) && !dim.base().text_user_positioned
{
let anno_scale = annotation_scale_handle
.and_then(|handle| {
match self.tabs[i].scene.document.objects.get(&handle) {
Some(acadrust::objects::ObjectType::Scale(scale)) => Some(
scale.inverse_factor()
/ self.tabs[i].scene.annotation_scale_unit_factor(),
),
_ => None,
}
})
.unwrap_or(self.tabs[i].scene.annotation_scale as f64);
if let Some(position) =
crate::entities::dimension::dimension_text_grip_position(
dim,
&self.tabs[i].scene.document,
anno_scale,
)
{
// The text grip is the final native grip of Linear/Aligned dims.
// While the text is still automatic, make this a point/stretch grip
// rather than a midpoint-translate grip. That makes the first drag use
// the displayed automatic position as its absolute starting point instead
// of translating the stale DWG text_middle_point.
if let Some(text_grip) = entity_grips.last_mut() {
text_grip.world =
glam::DVec3::new(position.x, position.y, position.z);
text_grip.is_midpoint = false;
}
}
}
}
entity_grips.extend(crate::scene::model::solid_history::primitive_grips(
&self.tabs[i].scene.document,
handle,

View file

@ -1110,39 +1110,14 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
.get(idx)
.map(|l| l.name.clone())
.unwrap_or_default();
if !new_name.is_empty()
&& new_name != old_name
&& !self.tabs[i].scene.document.layers.contains(&new_name)
{
if !new_name.is_empty() && new_name != old_name {
self.push_undo_snapshot(i, "LAYER RENAME");
// Keep the whole record (handle, color, linetype,
// lineweight, flags) and only change the name, so the
// renamed layer still has a valid handle and survives a
// DWG save (issue #67).
if let Some(mut nl) =
self.tabs[i].scene.document.layers.get(&old_name).cloned()
{
nl.name = new_name.clone();
if !nl.handle.is_valid() {
nl.handle = self.tabs[i].scene.document.allocate_handle();
}
let _ = self.tabs[i].scene.document.layers.add(nl);
if !self.tabs[i].rename_layer(&old_name, &new_name) {
self.discard_last_undo_entry(i);
}
self.tabs[i].scene.document.layers.remove(&old_name);
for e in self.tabs[i].scene.document.entities_mut() {
if e.as_entity().layer() == old_name {
e.as_entity_mut().set_layer(new_name.clone());
}
}
self.tabs[i].dirty = true;
}
let doc_layers = self.tabs[i].scene.document.layers.clone();
let vp_info = self.tabs[i].scene.viewport_list();
self.tabs[i]
.layers
.sync_with_viewports(&doc_layers, vp_info);
self.tabs[i].layers.edit_buf.clear();
self.sync_ribbon_layers();
self.refresh_layer_panel();
}
Task::none()
}
@ -1991,7 +1966,10 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
// selected inserts to the picked definition. A stale
// typed value in the text buffer would mask the pick,
// so drop it; the pick also closes the list.
self.tabs[i].properties.edit_buf.remove("block");
self.tabs[i]
.properties
.edit_buf
.remove(&crate::ui::properties::FieldKey::Geom("block"));
self.tabs[i].properties.edit_choice_open = false;
let canon = self.tabs[i]
.scene
@ -2127,9 +2105,14 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
pub(super) fn on_prop_geom_commit(&mut self, field: &'static str) -> Task<Message> {
let i = self.active_tab;
self.tabs[i].properties.active_field = None;
let handles = self.property_target_handles(i);
if !handles.is_empty() {
if let Some(raw_val) = self.tabs[i].properties.edit_buf.remove(field) {
if let Some(raw_val) = self.tabs[i]
.properties
.edit_buf
.remove(&crate::ui::properties::FieldKey::Geom(field))
{
// Block names are free-form text — a name like "10-5"
// must not be arithmetic-evaluated.
let val = if field == "block" {
@ -2355,6 +2338,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
/// it is not run through the expression evaluator like numeric fields).
pub(super) fn on_prop_attr_commit(&mut self, tag: String) -> Task<Message> {
let i = self.active_tab;
self.tabs[i].properties.active_field = None;
let key = crate::ui::properties::attr_edit_key(&tag);
let handles = self.property_target_handles(i);
if handles.is_empty() {
@ -2609,3 +2593,180 @@ fn block_attr_prompts(
}
map
}
#[cfg(test)]
mod layer_rename_tests {
use crate::app::OpenCADStudio;
use acadrust::entities::Line;
use acadrust::{EntityType, Handle};
fn app_with_editing_layer() -> OpenCADStudio {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
let _ = app.on_layer_new();
app
}
fn rename_layer(app: &mut OpenCADStudio, old_name: &str, new_name: &str) {
let i = app.active_tab;
let idx = app.tabs[i]
.layers
.layers
.iter()
.position(|layer| layer.name == old_name)
.expect("layer exists in panel");
app.tabs[i].layers.editing = Some(idx);
app.tabs[i].layers.edit_buf = new_name.to_string();
let _ = app.on_layer_rename_commit();
}
#[test]
fn layer_rename_allows_ascii_case_only_change() {
let mut app = app_with_editing_layer();
let i = app.active_tab;
let old_name = app.tabs[i].layers.edit_buf.clone();
rename_layer(&mut app, &old_name, "TEST");
let handle = app.tabs[i].scene.document.layers.get("TEST").unwrap().handle;
rename_layer(&mut app, "TEST", "test");
let layer = app.tabs[i].scene.document.layers.get("test").unwrap();
assert_eq!(layer.name, "test");
assert_eq!(layer.handle, handle);
}
#[test]
fn layer_rename_allows_unicode_case_only_change() {
let mut app = app_with_editing_layer();
let i = app.active_tab;
let old_name = app.tabs[i].layers.edit_buf.clone();
rename_layer(&mut app, &old_name, "é");
rename_layer(&mut app, "é", "É");
assert_eq!(
app.tabs[i].scene.document.layers.get("É").unwrap().name,
"É"
);
}
#[test]
fn current_layer_rename_updates_name_and_allocated_handle() {
let mut app = app_with_editing_layer();
let i = app.active_tab;
let idx = app.tabs[i].layers.editing.expect("new layer is being edited");
let old_name = app.tabs[i].layers.layers[idx].name.clone();
app.tabs[i]
.scene
.document
.layers
.get_mut(&old_name)
.unwrap()
.handle = Handle::NULL;
app.tabs[i].layers.selected = Some(idx);
let _ = app.on_layer_set_current();
rename_layer(&mut app, &old_name, "Renamed");
let layer = app.tabs[i]
.scene
.document
.layers
.get("Renamed")
.unwrap();
assert!(layer.handle.is_valid());
assert_eq!(app.tabs[i].active_layer, "Renamed");
assert_eq!(app.tabs[i].layers.current_layer, "Renamed");
assert_eq!(app.ribbon.active_layer, "Renamed");
assert_eq!(
app.tabs[i].scene.document.header.current_layer_name,
"Renamed"
);
assert_eq!(
app.tabs[i].scene.document.header.current_layer_handle,
layer.handle
);
}
#[test]
fn layer_rename_undo_redo_restores_active_layer() {
let mut app = app_with_editing_layer();
let i = app.active_tab;
let idx = app.tabs[i].layers.editing.expect("new layer is being edited");
let old_name = app.tabs[i].layers.layers[idx].name.clone();
app.tabs[i].layers.selected = Some(idx);
let _ = app.on_layer_set_current();
rename_layer(&mut app, &old_name, "Renamed");
app.undo_active_tab();
assert!(app.tabs[i].scene.document.layers.contains(&old_name));
assert_eq!(app.tabs[i].active_layer, old_name);
assert_eq!(app.ribbon.active_layer, app.tabs[i].active_layer);
app.redo_active_tab();
assert!(app.tabs[i].scene.document.layers.contains("Renamed"));
assert_eq!(app.tabs[i].active_layer, "Renamed");
assert_eq!(app.ribbon.active_layer, "Renamed");
}
#[test]
fn layer_rename_keeps_independent_current_layer_mirrors() {
let mut app = app_with_editing_layer();
let i = app.active_tab;
let old_name = app.tabs[i].layers.edit_buf.clone();
rename_layer(&mut app, &old_name, "A");
let _ = app.on_layer_new();
let b_idx = app.tabs[i].layers.editing.expect("new layer is being edited");
app.tabs[i].layers.selected = Some(b_idx);
let _ = app.on_layer_set_current();
let header_name = app.tabs[i].scene.document.header.current_layer_name.clone();
let header_handle = app.tabs[i].scene.document.header.current_layer_handle;
app.tabs[i].active_layer = "A".to_string();
rename_layer(&mut app, "A", "Renamed");
assert_eq!(app.tabs[i].active_layer, "Renamed");
assert_eq!(
app.tabs[i].scene.document.header.current_layer_name,
header_name
);
assert_eq!(
app.tabs[i].scene.document.header.current_layer_handle,
header_handle
);
}
#[test]
fn layer_rename_updates_mixed_case_entity_references() {
let mut app = app_with_editing_layer();
let i = app.active_tab;
let old_name = app.tabs[i].layers.edit_buf.clone();
rename_layer(&mut app, &old_name, "TEST");
let mut line = Line::new();
line.common.layer = "test".to_string();
let handle = app.tabs[i].scene.add_entity(EntityType::Line(line));
app.tabs[i]
.scene
.invalidate_layer_dependencies(&["TEST".to_string()]);
rename_layer(&mut app, "TEST", "Renamed");
assert_eq!(
app.tabs[i]
.scene
.document
.get_entity(handle)
.unwrap()
.common()
.layer,
"Renamed"
);
let epoch = app.tabs[i].scene.geometry_epoch;
app.tabs[i]
.scene
.invalidate_layer_dependencies(&["Renamed".to_string()]);
assert_ne!(app.tabs[i].scene.geometry_epoch, epoch);
}
}

View file

@ -636,6 +636,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
}
DockMsg::DragMove(point) => {
if self.dock_dragging.is_some() {
let avail = self.tabs[self.active_tab].scene.selection.borrow().vp_size.1;
let side = if point.x < self.win_size.0 * 0.5 {
DockSide::Left
} else {
@ -643,8 +644,8 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
};
let index = crate::ui::dock::drop_index(
point.y,
std::cmp::max(self.dock.len(side), 1),
self.win_size.1,
std::cmp::max(self.dock_visible_len(side), 1),
avail,
);
self.dock_drag_target = Some((side, index));
} else if let Some(id) = self.dock_resizing {
@ -680,6 +681,33 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
}
}
/// Whether `id` is currently rendered (not closed, not on the start screen /
/// clean-screen viewport). Mirrors the visibility filter the edge column
/// renderer applies before building each side's stack.
pub(crate) fn dock_panel_visible(&self, id: crate::ui::dock::PanelId) -> bool {
use crate::ui::dock::PanelId;
if self.tabs[self.active_tab].is_start || self.clean_screen {
return false;
}
match id {
PanelId::Properties => self.show_properties,
PanelId::BlockPalette => self.show_block_palette,
}
}
/// Number of panels currently rendered on `side`. Closed panels keep their
/// stack slot but take no vertical space, so drag preview geometry must
/// size slots against only the panels that are actually visible.
pub(crate) fn dock_visible_len(&self, side: crate::app::config::DockSide) -> usize {
let ids: &[crate::ui::dock::PanelId] = match side {
crate::app::config::DockSide::Left => &self.dock.left,
crate::app::config::DockSide::Right => &self.dock.right,
};
ids.iter()
.filter(|id| self.dock_panel_visible(**id))
.count()
}
/// Start placing `name` through the INSERT command, skipping the name prompt.
fn start_block_placement(&mut self, name: &str) {
let i = self.active_tab;
@ -1039,6 +1067,49 @@ mod tests {
assert_eq!(app.block_palette.placing.as_deref(), Some("Fixture"));
}
#[test]
fn blockpalette_reflects_new_block_without_reopen() {
use acadrust::types::Transform;
let mut app = fresh();
let i = app.active_tab;
// Reproduce the user-facing flow: select entities and run the BLOCK
// command, which goes through `create_block_from_entities` (not the
// clipboard / paste-as-block path).
let mut line = Line::new();
line.start = Vector3::ZERO;
line.end = Vector3::new(10.0, 0.0, 0.0);
let first = app.tabs[i].scene.add_entity(EntityType::Line(line));
app.tabs[i].scene.select_entity(first, false);
app.show_block_palette = true;
let ws = Transform::identity();
let id = Transform::identity();
app.tabs[i]
.scene
.create_block_from_entities(&[first], "First", &ws, &id)
.unwrap();
app.refresh_block_palette_if_stale();
assert!(app.block_palette.blocks.iter().any(|b| b.name == "First"));
// Create a second block on the same tab, then re-run the per-update
// stale check. It must pick up the new block WITHOUT reopening.
line = Line::new();
line.start = Vector3::ZERO;
line.end = Vector3::new(9.0, 0.0, 0.0);
let second = app.tabs[i].scene.add_entity(EntityType::Line(line));
app.tabs[i].scene.select_entity(second, false);
app.tabs[i]
.scene
.create_block_from_entities(&[second], "Second", &ws, &id)
.unwrap();
app.refresh_block_palette_if_stale();
assert!(
app.block_palette.blocks.iter().any(|b| b.name == "Second"),
"Second must appear without reopening the panel"
);
}
#[test]
fn blockpalette_pin_toggles_autocollapse_and_close_hides() {
let mut app = fresh();
@ -1077,6 +1148,89 @@ mod tests {
);
}
#[test]
fn dock_visible_len_counts_only_rendered_panels() {
let mut app = fresh();
app.dock = Default::default();
app.dock.left = vec![
crate::ui::dock::PanelId::BlockPalette,
crate::ui::dock::PanelId::Properties,
];
// Left: block palette hidden, properties shown -> 1 visible panel.
app.show_block_palette = false;
app.show_properties = true;
assert_eq!(
app.dock_visible_len(crate::app::config::DockSide::Left),
1
);
// Reveal the block palette -> both count.
app.show_block_palette = true;
assert_eq!(
app.dock_visible_len(crate::app::config::DockSide::Left),
2
);
// A hidden (closed) panel counts for nothing even when stacked.
app.show_block_palette = false;
assert_eq!(
app.dock_visible_len(crate::app::config::DockSide::Left),
1
);
}
#[test]
fn dock_drag_target_ignores_hidden_panels_on_the_side() {
// Reproduce the persisted layout that exposed a half-height ghost: two
// panels live in the left stack but the block palette is hidden
// (show_block_palette=false), so only Properties renders. Drag geometry
// must count only panels that are actually visible.
let mut app = fresh();
app.dock = Default::default();
app.dock.left = vec![
crate::ui::dock::PanelId::BlockPalette,
crate::ui::dock::PanelId::Properties,
];
app.show_block_palette = false;
app.show_properties = true;
let i = app.active_tab;
app.tabs[i].scene.selection.borrow_mut().vp_size = (1600.0, 900.0);
app.win_size = (1600.0, 900.0).into();
let id = crate::ui::dock::PanelId::Properties;
let _ = app.on_dock(crate::ui::dock::DockMsg::DockGrab(id));
// Pointer near the bottom of the left edge: one visible panel means a
// single slot, so every y maps to index 0 (no top/bottom split).
let _ =
app.on_dock(crate::ui::dock::DockMsg::DragMove(iced::Point::new(100.0, 850.0)));
assert_eq!(
app.dock_drag_target,
Some((crate::app::config::DockSide::Left, 0))
);
}
#[test]
fn dock_drag_target_counts_all_visible_panels() {
let mut app = fresh();
app.dock = Default::default();
app.dock.left = vec![
crate::ui::dock::PanelId::BlockPalette,
crate::ui::dock::PanelId::Properties,
];
// Both panels shown: two real slots on the left edge. A pointer near
// the bottom maps to the append slot (index == 2).
app.show_block_palette = true;
app.show_properties = true;
let i = app.active_tab;
app.tabs[i].scene.selection.borrow_mut().vp_size = (1600.0, 900.0);
app.win_size = (1600.0, 900.0).into();
let id = crate::ui::dock::PanelId::Properties;
let _ = app.on_dock(crate::ui::dock::DockMsg::DockGrab(id));
let _ =
app.on_dock(crate::ui::dock::DockMsg::DragMove(iced::Point::new(100.0, 850.0)));
assert_eq!(
app.dock_drag_target,
Some((crate::app::config::DockSide::Left, 2))
);
}
#[test]
fn blockpalette_width_reset() {
let mut app = fresh();

View file

@ -1154,13 +1154,20 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
crate::scene::text::ttf_glyph::clear_fallback_cache();
}
// Current model-space annotation scale comes from the drawing's
// CANNOSCALEVALUE (paper/drawing factor); the multiplier we use
// for text/dim sizing is its inverse (1:50 -> 0.02 -> 50.0).
// CANNOSCALEVALUE (paper/drawing factor). Convert its inverse into
// drawing units as well: metric annotation sizes are paper millimetres
// and imperial annotation sizes are paper inches.
// Current model-space annotation scale comes from the drawing's
// CANNOSCALEVALUE (paper/drawing factor). Convert its paper unit into
// the drawing's INSUNITS as well, so e.g. a metre drawing uses
// 0.001 model units for 1 mm of paper at 1:1.
let cannoscale_value = self.tabs[i].scene.document.header.annotation_scale_value;
let unit_factor = self.tabs[i].scene.annotation_scale_unit_factor();
self.tabs[i].scene.annotation_scale = if cannoscale_value > 1e-9 {
(1.0 / cannoscale_value) as f32
((1.0 / cannoscale_value) / unit_factor) as f32
} else {
1.0
(1.0 / unit_factor) as f32
};
// Open-time breakdown so regressions are visible immediately.

View file

@ -217,6 +217,20 @@ impl OpenCADStudio {
self.reset_modal_geometry();
}
/// Fire the focus-sweep once when a property field is active, so a
/// click-away that moved focus off the field (viewport, toolbar, command
/// line) clears the active-row highlight. Idle (`Task::none()`) unless a
/// field is active, and the sweep itself keeps the marker whenever the
/// active field still holds focus — re-firing it on unrelated messages is
/// cheap and safe.
fn sync_active_field_if_any(&self) -> Task<Message> {
if self.tabs[self.active_tab].properties.active_field.is_some() {
crate::ui::properties::sync_active_field_task()
} else {
Task::none()
}
}
pub fn update(&mut self, msg: Message) -> Task<Message> {
let perf_started = crate::perf::enabled().then(Instant::now);
let perf_label = perf_message_label(&msg);
@ -308,34 +322,31 @@ impl OpenCADStudio {
// user-generated message. Desktop only.
#[cfg(not(target_arch = "wasm32"))]
Message::DrainPluginRequests => {
if self.active_tab < self.tabs.len() {
let mut host = crate::app::plugin_host::HostSession::new(self, self.active_tab);
for tab in 0..self.tabs.len() {
let mut host = crate::app::plugin_host::HostSession::new(self, tab);
crate::plugin::external::with_manager(|mgr| {
mgr.drain_requests(&mut host, &mut |_| {});
});
crate::plugin::external::with_manager(|mgr| {
for line in mgr.drain_io() {
// Skip internal runner/plugin/REPL trace noise when
// verbose logging is enabled; real plugin stdout/stderr
// and non-trace runner errors still pass through.
if line.text.starts_with("[runner]")
|| line.text.starts_with("[plugin] ")
|| line.text.starts_with("[python-repl]")
{
continue;
}
crate::plugin::external::with_manager(|mgr| {
for line in mgr.drain_io() {
if line.text.starts_with("[runner]")
|| line.text.starts_with("[plugin] ")
|| line.text.starts_with("[python-repl]")
{
continue;
}
let prefixed = format!("[{} {}] {}", line.plugin_id, line.source, line.text);
match line.source {
ocs_plugin_api::process::IoStream::Stderr => {
self.command_line.push_error(&prefixed);
}
let prefixed = format!("[{} {}] {}", line.plugin_id, line.source, line.text);
match line.source {
ocs_plugin_api::process::IoStream::Stderr => {
self.command_line.push_error(&prefixed);
}
_ => {
self.command_line.push_output(&prefixed);
}
_ => {
self.command_line.push_output(&prefixed);
}
}
});
}
}
});
Task::none()
}
@ -1195,7 +1206,8 @@ impl OpenCADStudio {
}
Message::RibbonToolClick { tool_id, event } => {
self.on_ribbon_tool_click(tool_id, event)
let sweep = self.sync_active_field_if_any();
Task::batch(vec![sweep, self.on_ribbon_tool_click(tool_id, event)])
}
Message::PluginFileDialogResult { command, path } => {
if let Some(path) = path {
@ -1438,14 +1450,15 @@ impl OpenCADStudio {
// the submit path, which tokenises multi-token lines — so a typed
// token, a pasted `LINE 0,0 10,10`, or API-fed text all run their
// spaces as step separators. A leading `>` keeps spaces literal.
let sweep = self.sync_active_field_if_any();
if !self.command_line.literal_spaces && !s.starts_with('>') && s.contains(' ') {
self.command_line.input = s;
return self.update(Message::CommandSubmit);
return Task::batch(vec![sweep, self.update(Message::CommandSubmit)]);
}
self.command_line.input = s;
self.command_line.autocomplete_cursor = None;
self.command_line.cancel_history_navigation();
Task::none()
Task::batch(vec![sweep, Task::none()])
}
Message::CommandAppendChar(s) => self.on_command_append_char(s),
@ -2628,10 +2641,13 @@ impl OpenCADStudio {
Task::none()
}
Message::PanePress(idx) => {
// A click-away into the drawing area re-syncs the active-row
// highlight against real focus before the pick runs.
let sweep = self.sync_active_field_if_any();
// A fresh press ends any stale (un-dropped) pane move.
self.pane_move_from = None;
self.focus_model_pane(idx);
self.on_viewport_left_press()
Task::batch(vec![sweep, self.on_viewport_left_press()])
}
Message::PaneRelease(idx) => {
// Finishing a pane-move drag: swap the source pane with the one
@ -2666,7 +2682,10 @@ impl OpenCADStudio {
self.update(Message::ViewportScroll(d))
}
Message::ViewportLeftPress => self.on_viewport_left_press(),
Message::ViewportLeftPress => {
let sweep = self.sync_active_field_if_any();
Task::batch(vec![sweep, self.on_viewport_left_press()])
}
Message::ViewportLeftRelease => self.on_viewport_left_release(),
@ -4613,7 +4632,7 @@ impl OpenCADStudio {
self.tabs[self.active_tab]
.properties
.edit_buf
.insert(field.to_string(), value);
.insert(crate::ui::properties::FieldKey::Geom(field), value);
Task::none()
}
@ -4647,6 +4666,38 @@ impl OpenCADStudio {
Message::PropAttrCommit(tag) => self.on_prop_attr_commit(tag),
Message::PropPointerPressed => {
if !self.dock_panel_visible(crate::ui::dock::PanelId::Properties) {
return Task::none();
}
crate::ui::properties::sync_active_field_task()
}
Message::PropSyncActive(focused) => {
let panel = &mut self.tabs[self.active_tab].properties;
if let Some(id) = focused.as_ref() {
if let Some(key) = panel.prop_field_key_for_id(id) {
let changed = panel.active_field.as_ref() != Some(&key);
panel.active_field = Some(key);
// Only select the whole value when focus landed on a
// field that wasn't already the active one; re-focusing
// the same field (or sweeping after a click elsewhere
// left its focus in place) must keep caret placement.
if changed {
return iced::widget::operation::select_all(id.clone());
}
return Task::none();
}
}
if !crate::ui::properties::active_key_focused(
panel.active_field.as_ref(),
focused.as_ref(),
) {
panel.active_field = None;
}
Task::none()
}
Message::PropColorPickerToggle => {
let i = self.active_tab;
self.tabs[i].properties.color_picker_open =
@ -5992,7 +6043,11 @@ impl OpenCADStudio {
{
// Stop the plugin runner first so Windows releases the DLL
// and allows the package directory to be deleted.
crate::plugin::external::remove_plugin(&id);
if !crate::plugin::external::remove_plugin(&id) {
self.marketplace_status =
format!("Uninstall failed: plugin '{id}' did not stop in time");
return Task::none();
}
match crate::plugin::external::uninstall(&id) {
Ok(()) => {
self.marketplace_status =
@ -7516,3 +7571,41 @@ impl OpenCADStudio {
}
}
}
#[cfg(test)]
mod prop_pointer_tests {
use super::Message;
use crate::app::OpenCADStudio;
use crate::ui::dock::PanelId;
fn drawing_app() -> OpenCADStudio {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
app
}
#[test]
fn hidden_properties_panel_skips_the_focus_sweep() {
// A click while the panel is closed must not run the widget-tree sweep:
// the returned task is a bare `Task::none` (units == 0).
let mut app = drawing_app();
assert!(app.dock_panel_visible(PanelId::Properties));
app.show_properties = false;
assert!(!app.dock_panel_visible(PanelId::Properties));
let task = app.update(Message::PropPointerPressed);
assert_eq!(task.units(), 0);
}
#[test]
fn visible_properties_panel_runs_the_focus_sweep() {
// A click while the panel is open must still fire the sweep (units > 0)
// so the select-whole-value feature keeps working.
let mut app = drawing_app();
app.show_properties = true;
assert!(app.dock_panel_visible(PanelId::Properties));
let task = app.update(Message::PropPointerPressed);
assert!(task.units() > 0);
}
}

View file

@ -4206,46 +4206,75 @@ impl OpenCADStudio {
height: vh,
};
// 1) Try direct wire hit — works when the border is clicked.
let wire_hit: Option<acadrust::Handle> = {
let (view_rot, eye) = {
let c = self.tabs[i].scene.camera.borrow();
(c.view_proj_rte(bounds), c.eye())
};
let all_wires = self.tabs[i].scene.hit_test_wires();
let click_world = self.cursor_model_point(i, &edit_cam, p, bounds);
let click_candidates = self.tabs[i].scene.interaction_pick_candidates_near(
all_wires,
click_world,
view_rot,
eye,
bounds,
crate::ui::overlay::pick_box_aperture_px(self.pick_box) * 2.0,
);
scene::pick::hit_test::click_hit(
p,
&click_candidates,
view_rot,
eye,
bounds,
self.tabs[i].scene.document.header.lineweight_display,
crate::ui::overlay::pick_box_aperture_px(self.pick_box),
)
.and_then(|s| Scene::handle_from_wire_name(s))
.and_then(|h| {
if let Some(AcadEntityType::Viewport(vp)) =
self.tabs[i].scene.document.get_entity(h)
{
if Scene::is_content_viewport(vp) {
Some(h)
} else {
None
}
let (view_rot, eye) = {
let c = self.tabs[i].scene.camera.borrow();
(c.view_proj_rte(bounds), c.eye())
};
let all_wires = self.tabs[i].scene.hit_test_wires();
let click_world = self.cursor_model_point(i, &edit_cam, p, bounds);
let click_candidates = self.tabs[i].scene.interaction_pick_candidates_near(
all_wires,
click_world,
view_rot,
eye,
bounds,
crate::ui::overlay::pick_box_aperture_px(self.pick_box) * 2.0,
);
let raw_wire_hit = scene::pick::hit_test::click_hit(
p,
&click_candidates,
view_rot,
eye,
bounds,
self.tabs[i].scene.document.header.lineweight_display,
crate::ui::overlay::pick_box_aperture_px(self.pick_box),
)
.and_then(|s| Scene::handle_from_wire_name(s));
// In paper space, text editing takes precedence over entering MSPACE.
// This mirrors the model-space double-click behaviour.
if let Some(handle) = raw_wire_hit {
let is_editable_text = self.tabs[i]
.scene
.document
.get_entity(handle)
.is_some_and(|entity| {
crate::app::text_inline::read_text_field(entity).is_some()
|| matches!(entity, AcadEntityType::Leader(_))
});
if is_editable_text {
if let Some(layer) = self.tabs[i].scene.locked_layer_name(handle) {
self.command_line.push_info(
crate::tf!(
"Object is on locked layer \"{layer}\" — unlock the layer to edit it."
)
.as_ref(),
);
return Task::none();
}
return self.begin_text_edit(handle);
}
}
// If the double-click was not on editable text, keep the existing
// paper-space behaviour and try to enter the clicked viewport.
let wire_hit: Option<acadrust::Handle> = raw_wire_hit.and_then(|h| {
if let Some(AcadEntityType::Viewport(vp)) =
self.tabs[i].scene.document.get_entity(h)
{
if Scene::is_content_viewport(vp) {
Some(h)
} else {
None
}
})
};
} else {
None
}
});
// 2) Decide which viewport (if any) the click enters,
// keyed on the *visible* on-screen rectangle. Screen-space

View file

@ -1528,13 +1528,17 @@ impl OpenCADStudio {
.unwrap_or(crate::app::config::DockSide::Right),
);
// Dropping onto an edge joins that edge's stack, whose column is as
// wide as its widest panel (the dragged panel included).
// wide as its widest currently-shown panel (the dragged panel
// included). Hidden (closed) panels don't render, so they can't
// widen the column being previewed.
let preview_width = {
let mut widths: Vec<f32> = match side {
crate::app::config::DockSide::Left => self.dock.left.clone(),
crate::app::config::DockSide::Right => self.dock.right.clone(),
crate::app::config::DockSide::Left => &self.dock.left,
crate::app::config::DockSide::Right => &self.dock.right,
}
.into_iter()
.iter()
.copied()
.filter(|&pid| self.dock_panel_visible(pid))
.map(|pid| self.dock.width(pid, self.win_size.0))
.collect();
widths.push(self.dock.width(id, self.win_size.0));
@ -1555,15 +1559,20 @@ impl OpenCADStudio {
}
});
// The ghost is the dragged panel at its real size: its own saved
// width and its 1/N share of the edge (N = stack size after the
// drop), with a header naming which panel is being moved.
// width and its 1/N share of the edge (N = number of panels shown
// on the edge after the drop), with a header naming which panel is
// being moved. Only visibly-docked panels share the column height:
// a hidden (closed) panel keeps its stack slot but no screen space,
// so it must not split the preview.
let was_here = self.dock.location(id).map(|(s, _)| s) == Some(side);
let visible = self.dock_visible_len(side);
let final_count = if was_here {
std::cmp::max(self.dock.len(side), 1)
std::cmp::max(visible, 1)
} else {
self.dock.len(side) + 1
visible + 1
};
let slot_h = self.win_size.1 / final_count as f32;
let edge_h = tab.scene.selection.borrow().vp_size.1;
let slot_h = edge_h / final_count as f32;
let index = self.dock_drag_target.map(|(_, i)| i).unwrap_or(0);
let slot = index.min(final_count.saturating_sub(1));
let ghost_top = slot as f32 * slot_h;
@ -1733,6 +1742,7 @@ impl OpenCADStudio {
self.tabs[self.active_tab].is_start,
self.tabs[self.active_tab].history.undo_stack.len(),
self.tabs[self.active_tab].history.redo_stack.len(),
self.show_block_palette,
));
}
if self.show_file_tabs {
@ -2129,6 +2139,9 @@ impl OpenCADStudio {
event::listen_with(|ev, status, win_id| {
use iced::event::Status;
match ev {
iced::Event::Mouse(iced::mouse::Event::ButtonPressed(
iced::mouse::Button::Left,
)) => Some(Message::PropPointerPressed),
iced::Event::Window(window::Event::CloseRequested) => {
Some(Message::WindowCloseRequested(win_id))
}
@ -2306,7 +2319,14 @@ impl OpenCADStudio {
let tab_strip = (!pinned.is_empty()).then(|| {
let tabs: Vec<Element<'_, Message>> = pinned
.iter()
.map(|id| self.rail_slice(*id, side))
.map(|id| {
self.rail_slice(
*id,
side,
self.dock_expanded == Some(*id),
self.dock_dragging == Some(*id),
)
})
.collect();
column(tabs).width(DOCK_RAIL_W).height(Fill).into()
});
@ -2351,6 +2371,8 @@ impl OpenCADStudio {
&self,
id: crate::ui::dock::PanelId,
side: crate::app::config::DockSide,
is_active: bool,
is_dragging: bool,
) -> Element<'_, Message> {
let label = canvas(VBarLabel {
text: id.title().to_string(),
@ -2358,26 +2380,37 @@ impl OpenCADStudio {
})
.width(Fill)
.height(Fill);
mouse_area(
container(label)
.width(Length::Fixed(DOCK_RAIL_W))
.height(Fill)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(
theme.palette().background.base.color,
)),
border: Border {
color: theme.palette().background.neutral.color,
width: 1.0,
radius: 0.0.into(),
let bg = move |theme: &Theme| {
let palette = theme.palette();
if is_active {
palette.primary.weak.color
} else if is_dragging {
palette.primary.weak.color.scale_alpha(0.55)
} else {
palette.background.base.color
}
};
let tab = container(label)
.width(Length::Fixed(DOCK_RAIL_W))
.height(Fill)
.style(move |theme: &Theme| container::Style {
background: Some(Background::Color(bg(theme))),
border: Border {
color: if is_active {
theme.palette().primary.base.color
} else {
theme.palette().background.neutral.color
},
..Default::default()
}),
)
.interaction(iced::mouse::Interaction::Pointer)
.on_press(Message::Dock(crate::ui::dock::DockMsg::DockGrab(id)))
.on_enter(Message::Dock(crate::ui::dock::DockMsg::Hover(id)))
.into()
width: 1.0,
radius: 0.0.into(),
},
..Default::default()
});
mouse_area(tab)
.interaction(iced::mouse::Interaction::Pointer)
.on_press(Message::Dock(crate::ui::dock::DockMsg::DockGrab(id)))
.on_enter(Message::Dock(crate::ui::dock::DockMsg::Hover(id)))
.into()
}
/// A panel expanded to the full edge column: the panel body plus a

View file

@ -1151,6 +1151,8 @@ pub enum CmdResult {
CommitEntity(EntityType),
/// Commit several acadrust entities in one undo step; keep the command active.
CommitEntities(Vec<EntityType>),
/// Commit several entities in one undo step and end the command.
CommitEntitiesAndExit(Vec<EntityType>),
/// Commit an acadrust entity to the document and end the command.
CommitAndExit(EntityType),
/// Commit a Model-tab 3D solid: the acadrust entity (for selection /
@ -1183,6 +1185,18 @@ pub enum CmdResult {
CopySelected(Vec<Handle>, EntityTransform),
/// Commit a hatch fill (stored in Scene::hatches, not the DXF document).
CommitHatch(HatchModel),
/// Commit a hatch with the selected hatch's entity colour and transparency.
CommitStyledHatch {
hatch: HatchModel,
color: acadrust::types::Color,
transparency: acadrust::types::Transparency,
},
/// Commit a hatch and retain each boundary ring as an entity.
CommitHatchWithBoundaries {
hatch: HatchModel,
boundaries: Vec<EntityType>,
entity_style: Option<(acadrust::types::Color, acadrust::types::Transparency)>,
},
/// Copy selected entities with multiple transforms (e.g. rectangular array); end command.
BatchCopy(Vec<Handle>, Vec<EntityTransform>),
/// Erase `handle` and replace with new entities; command stays active.
@ -1844,6 +1858,12 @@ pub trait CadCommand: Send {
None
}
fn hatch_preview_models(
&self,
) -> Option<Vec<crate::scene::model::hatch_model::HatchModel>> {
None
}
/// Returns `true` when the current step picks a corner of a selection
/// *window* by point (e.g. STRETCH's crossing window). Such a pick must be a
/// free point: applying the Ortho/Polar lock would pin the opposite corner to

View file

@ -853,6 +853,184 @@ fn apply_to_v3(target: &mut acadrust::types::Vector3, apply: &GripApply) {
}
}
#[derive(Clone, Copy)]
struct DimTextRelativePosition {
along_fraction: f64,
perpendicular_offset: f64,
z_offset: f64,
}
fn capture_dim_text_relative_position(dim: &Dimension) -> Option<DimTextRelativePosition> {
let base = dim.base();
// Auto-positioned text is recomputed by the renderer from DIMSTYLE.
// Only an explicitly moved text point needs to follow a grip deformation.
if !base.text_user_positioned {
return None;
}
let text = base.text_middle_point;
if text.x * text.x + text.y * text.y + text.z * text.z <= 1e-16 {
return None;
}
let (first, second, defpt, ax, ay) = match dim {
Dimension::Linear(d) => (
d.first_point,
d.second_point,
d.definition_point,
d.rotation.cos(),
d.rotation.sin(),
),
Dimension::Aligned(d) => {
let dx = d.second_point.x - d.first_point.x;
let dy = d.second_point.y - d.first_point.y;
let len = (dx * dx + dy * dy).sqrt();
if len <= 1e-12 {
return None;
}
(
d.first_point,
d.second_point,
d.definition_point,
dx / len,
dy / len,
)
}
_ => return None,
};
let px = -ay;
let py = ax;
let t1 = (first.x - defpt.x) * ax + (first.y - defpt.y) * ay;
let t2 = (second.x - defpt.x) * ax + (second.y - defpt.y) * ay;
let text_t = (text.x - defpt.x) * ax + (text.y - defpt.y) * ay;
let span = t2 - t1;
let along_fraction = if span.abs() > 1e-12 {
(text_t - t1) / span
} else {
0.5
};
let along = t1 + span * along_fraction;
let line_x = defpt.x + ax * along;
let line_y = defpt.y + ay * along;
let perpendicular_offset =
(text.x - line_x) * px + (text.y - line_y) * py;
Some(DimTextRelativePosition {
along_fraction,
perpendicular_offset,
z_offset: text.z - defpt.z,
})
}
fn restore_dim_text_relative_position(
dim: &mut Dimension,
saved: DimTextRelativePosition,
) {
let (first, second, defpt, ax, ay) = match dim {
Dimension::Linear(d) => (
d.first_point,
d.second_point,
d.definition_point,
d.rotation.cos(),
d.rotation.sin(),
),
Dimension::Aligned(d) => {
let dx = d.second_point.x - d.first_point.x;
let dy = d.second_point.y - d.first_point.y;
let len = (dx * dx + dy * dy).sqrt();
if len <= 1e-12 {
return;
}
(
d.first_point,
d.second_point,
d.definition_point,
dx / len,
dy / len,
)
}
_ => return,
};
let px = -ay;
let py = ax;
let t1 = (first.x - defpt.x) * ax + (first.y - defpt.y) * ay;
let t2 = (second.x - defpt.x) * ax + (second.y - defpt.y) * ay;
let along = t1 + (t2 - t1) * saved.along_fraction;
let base = dim.base_mut();
base.text_middle_point = Vector3::new(
defpt.x + ax * along + px * saved.perpendicular_offset,
defpt.y + ay * along + py * saved.perpendicular_offset,
defpt.z + saved.z_offset,
);
}
fn dimension_line_grip_position(dim: &Dimension) -> Option<DVec3> {
let (first, second, defpt, ax, ay) = match dim {
Dimension::Linear(d) => (
d.first_point,
d.second_point,
d.definition_point,
d.rotation.cos(),
d.rotation.sin(),
),
Dimension::Aligned(d) => {
let dx = d.second_point.x - d.first_point.x;
let dy = d.second_point.y - d.first_point.y;
let len = (dx * dx + dy * dy).sqrt();
if len <= 1e-12 {
return None;
}
(
d.first_point,
d.second_point,
d.definition_point,
dx / len,
dy / len,
)
}
_ => return None,
};
let px = -ay;
let py = ax;
// Project both extension origins onto the current dimension line.
let off1 =
(defpt.x - first.x) * px + (defpt.y - first.y) * py;
let off2 =
(defpt.x - second.x) * px + (defpt.y - second.y) * py;
let p1 = DVec3::new(
first.x + px * off1,
first.y + py * off1,
defpt.z,
);
let p2 = DVec3::new(
second.x + px * off2,
second.y + py * off2,
defpt.z,
);
Some((p1 + p2) * 0.5)
}
impl Grippable for Dimension {
fn grips(&self) -> Vec<GripDef> {
// Auto-placed dimensions carry a zero text_middle_point sentinel; put
@ -870,13 +1048,21 @@ impl Grippable for Dimension {
Dimension::Linear(d) => vec![
square_grip(0, dv3(&d.first_point)),
center_grip(1, dv3(&d.second_point)),
center_grip(2, dv3(&d.definition_point)),
center_grip(
2,
dimension_line_grip_position(self)
.unwrap_or_else(|| dv3(&d.definition_point)),
),
center_grip(3, text),
],
Dimension::Aligned(d) => vec![
square_grip(0, dv3(&d.first_point)),
center_grip(1, dv3(&d.second_point)),
center_grip(2, dv3(&d.definition_point)),
center_grip(
2,
dimension_line_grip_position(self)
.unwrap_or_else(|| dv3(&d.definition_point)),
),
center_grip(3, text),
],
Dimension::Radius(d) => vec![
@ -935,6 +1121,8 @@ impl Grippable for Dimension {
}
}
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
// Last grip always moves the text.
let text_grip = match self {
@ -953,6 +1141,12 @@ impl Grippable for Dimension {
return;
}
// A manually positioned dimension text point is stored in the DWG as an
// absolute coordinate. Capture its relation to the old dimension geometry
// before moving a definition grip so that it can be reconstructed against
// the new geometry afterwards.
let relative_text = capture_dim_text_relative_position(self);
match self {
Dimension::Linear(d) => match grip_id {
0 => apply_to_v3(&mut d.first_point, &apply),
@ -1013,6 +1207,11 @@ impl Grippable for Dimension {
_ => {}
},
}
if let Some(saved) = relative_text {
restore_dim_text_relative_position(self, saved);
}
self.base_mut().actual_measurement = self.measurement();
}
@ -3800,6 +3999,30 @@ pub(crate) fn baked_dimension_text_entity(
Some(ent)
}
pub(crate) fn dimension_text_grip_position(
dim: &Dimension,
document: &CadDocument,
anno_scale: f64,
) -> Option<Vector3> {
// Once the user has moved the text, its saved point is authoritative.
let base = dim.base();
if base.text_user_positioned {
let p = base.text_middle_point;
if p.x * p.x + p.y * p.y + p.z * p.z > 1e-16 {
return Some(p);
}
}
// For automatic text, use the same text-building path used by the
// dimension picture so the grip follows the actual DIMSTYLE placement,
// including annotation scaling and fit behaviour.
match baked_dimension_text_entity(dim, document, anno_scale)? {
EntityType::Text(text) => Some(text.insertion_point),
EntityType::MText(text) => Some(text.insertion_point),
_ => None,
}
}
#[cfg(test)]
mod dimtad_tests {
use super::text_on_dim_line;

View file

@ -159,7 +159,59 @@ fn grips(leader: &Leader) -> Vec<GripDef> {
fn apply_grip(leader: &mut Leader, grip_id: usize, apply: GripApply) {
let n = leader.vertices.len();
if grip_id < n {
if n >= 3 && leader.creation_type == LeaderCreationType::WithText {
// Grip del codo: mueve el codo libremente, pero arrastra también
// el extremo del renglón manteniendo la distancia relativa.
if grip_id == n - 2 {
let old_elbow = leader.vertices[n - 2];
let old_end = leader.vertices[n - 1];
let delta = match apply {
GripApply::Absolute(p) => acadrust::types::Vector3::new(
p.x as f64 - old_elbow.x,
p.y as f64 - old_elbow.y,
p.z as f64 - old_elbow.z,
),
GripApply::Translate(d) => acadrust::types::Vector3::new(
d.x as f64,
d.y as f64,
d.z as f64,
),
};
leader.vertices[n - 2].x = old_elbow.x + delta.x;
leader.vertices[n - 2].y = old_elbow.y + delta.y;
leader.vertices[n - 2].z = old_elbow.z + delta.z;
leader.vertices[n - 1].x = old_end.x + delta.x;
leader.vertices[n - 1].y = old_end.y + delta.y;
leader.vertices[n - 1].z = old_end.z + delta.z;
return;
}
// Grip del extremo horizontal: sólo debe estirar en X;
// Y/Z quedan pegados al codo para que siga horizontal.
if grip_id == n - 1 {
let elbow = leader.vertices[n - 2];
match apply {
GripApply::Absolute(p) => {
leader.vertices[n - 1].x = p.x as f64;
}
GripApply::Translate(d) => {
leader.vertices[n - 1].x += d.x as f64;
}
}
leader.vertices[n - 1].y = elbow.y;
leader.vertices[n - 1].z = elbow.z;
return;
}
}
if let Some(v) = leader.vertices.get_mut(grip_id) {
match apply {
GripApply::Absolute(p) => {
@ -176,7 +228,9 @@ fn apply_grip(leader: &mut Leader, grip_id: usize, apply: GripApply) {
}
} else if let GripApply::Translate(d) = apply {
leader.translate(acadrust::types::Vector3::new(
d.x as f64, d.y as f64, d.z as f64,
d.x as f64,
d.y as f64,
d.z as f64,
));
}
}

View file

@ -49,23 +49,44 @@ pub enum Language {
ArSa,
#[serde(rename = "ja-JP")]
JaJp,
#[serde(rename = "ko-KR")]
KoKr,
#[serde(rename = "cs-CZ")]
CsCz,
#[serde(rename = "it-IT")]
ItIt,
#[serde(rename = "fi-FI")]
FiFi,
#[serde(rename = "hu-HU")]
HuHu,
#[serde(rename = "pl-PL")]
PlPl,
#[serde(rename = "zh-TW")]
ZhTw,
}
impl Language {
pub const ALL: [Language; 13] = [
pub const ALL: [Language; 20] = [
Language::System,
Language::EnUs,
Language::TrTr,
Language::PtBr,
Language::CsCz,
Language::NlNl,
Language::FrFr,
Language::FiFi,
Language::DeDe,
Language::HiIn,
Language::HuHu,
Language::ItIt,
Language::JaJp,
Language::KoKr,
Language::PlPl,
Language::RuRu,
Language::ZhCn,
Language::EsEs,
Language::PtBr,
Language::ZhTw,
Language::TrTr,
Language::HiIn,
Language::ArSa,
Language::JaJp,
];
fn requested(self) -> Vec<i18n_embed::unic_langid::LanguageIdentifier> {
@ -83,6 +104,13 @@ impl Language {
Language::PtBr => vec!["pt-BR".parse().expect("valid locale")],
Language::ArSa => vec!["ar-SA".parse().expect("valid locale")],
Language::JaJp => vec!["ja-JP".parse().expect("valid locale")],
Language::KoKr => vec!["ko-KR".parse().expect("valid locale")],
Language::CsCz => vec!["cs-CZ".parse().expect("valid locale")],
Language::ItIt => vec!["it-IT".parse().expect("valid locale")],
Language::FiFi => vec!["fi-FI".parse().expect("valid locale")],
Language::HuHu => vec!["hu-HU".parse().expect("valid locale")],
Language::PlPl => vec!["pl-PL".parse().expect("valid locale")],
Language::ZhTw => vec!["zh-TW".parse().expect("valid locale")],
}
}
@ -101,6 +129,13 @@ impl Language {
Language::PtBr => crate::tr!("language", "portuguese"),
Language::ArSa => crate::tr!("language", "arabic"),
Language::JaJp => crate::tr!("language", "japanese"),
Language::KoKr => crate::tr!("language", "korean"),
Language::CsCz => crate::tr!("language", "czech"),
Language::ItIt => crate::tr!("language", "italian"),
Language::FiFi => crate::tr!("language", "finnish"),
Language::HuHu => crate::tr!("language", "hungarian"),
Language::PlPl => crate::tr!("language", "polish"),
Language::ZhTw => crate::tr!("language", "chinese-traditional"),
}
}
}

View file

@ -53,6 +53,7 @@ impl LeaderCommand {
} else {
defaults.scale
};
Self {
verts: Vec::new(),
plane: WorkingPlane::default(),
@ -65,6 +66,78 @@ impl LeaderCommand {
annotative: defaults.annotative,
}
}
fn finish(&self) -> CmdResult {
if self.verts.len() < 2 {
return CmdResult::Cancel;
}
let local: Vec<DVec3> = self
.verts
.iter()
.map(|point| self.plane.to_local(*point))
.collect();
let displayed_height = self.text_height * self.display_scale;
let displayed_landing = if self.arrow_size > 1.0e-9 {
self.arrow_size * self.display_scale
} else {
displayed_height * 1.5
};
// The user supplies only the arrow point and elbow. The horizontal
// landing is stored as a real third LEADER vertex so it can have its own grip.
let mut leader_points = local.clone();
let first = local[0];
let elbow = local[1];
let sign = if elbow.x >= first.x { 1.0 } else { -1.0 };
let landing_end = DVec3::new(
elbow.x + sign * displayed_landing,
elbow.y,
elbow.z,
);
leader_points.push(landing_end);
let leader = build_leader(
&leader_points,
Mat4::IDENTITY,
&self.dimension_style,
self.text_height,
self.gap,
self.arrow_size,
);
// The MTEXT starts at the real end of the landing.
let (anchor, attach) =
annotation_anchor(&leader_points, 0.0, Mat4::IDENTITY);
// Store the MTEXT at its native model-space size for the current
// annotation scale. Its annotation contexts then scale it relatively
// when another representation becomes active.
let mtext_height = displayed_height;
let mtext = build_mtext(
"",
anchor,
mtext_height,
attach,
Mat4::IDENTITY,
&self.text_style,
self.annotative,
);
CmdResult::CommitManyAndEditText {
entities: vec![
self.plane.place_entity(EntityType::Leader(leader)),
self.plane.place_entity(EntityType::MText(mtext)),
],
edit_index: 1,
}
}
}
impl CadCommand for LeaderCommand {
@ -80,61 +153,22 @@ impl CadCommand for LeaderCommand {
if self.verts.is_empty() {
t!("LEADER Specify arrowhead point:").into_owned()
} else {
t!(
"LEADER Specify next point [%{count} pts — Enter to place text]:",
count = self.verts.len()
)
.into_owned()
t!("LEADER Specify landing point:").into_owned()
}
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
self.verts.push(pt);
CmdResult::NeedPoint
if self.verts.len() >= 2 {
self.finish()
} else {
CmdResult::NeedPoint
}
}
fn on_enter(&mut self) -> CmdResult {
if self.verts.len() < 2 {
return CmdResult::Cancel;
}
// Place the leader plus an empty MText annotation, link them, then open
// the in-place MText editor so the user types the annotation text.
let local: Vec<DVec3> = self
.verts
.iter()
.map(|point| self.plane.to_local(*point))
.collect();
let leader = build_leader(
&local,
Mat4::IDENTITY,
&self.dimension_style,
self.text_height,
self.gap,
self.arrow_size,
);
let displayed_height = self.text_height * self.display_scale;
let (anchor, attach) = annotation_anchor(&local, displayed_height, Mat4::IDENTITY);
let mtext_height = if self.annotative {
self.text_height
} else {
displayed_height
};
let mtext = build_mtext(
"",
anchor,
mtext_height,
attach,
Mat4::IDENTITY,
&self.text_style,
self.annotative,
);
CmdResult::CommitManyAndEditText {
entities: vec![
self.plane.place_entity(EntityType::Leader(leader)),
self.plane.place_entity(EntityType::MText(mtext)),
],
edit_index: 1,
}
self.finish()
}
fn on_escape(&mut self) -> CmdResult {
@ -189,7 +223,10 @@ fn build_leader(
) -> Leader {
let mut l = Leader::from_vertices(verts.iter().map(|p| dv3(*p)).collect());
l.creation_type = LeaderCreationType::WithText;
l.hookline_enabled = true;
// New leaders store the landing as their final real vertex, so the renderer
// must not append a second synthetic hookline.
l.hookline_enabled = false;
l.dimension_style = dimension_style.to_string();
l.text_height = text_height;
l.dimension_gap = gap;
@ -206,21 +243,28 @@ fn build_leader(
/// left-pointing landing, to the left of a right-pointing one).
fn annotation_anchor(
verts: &[DVec3],
text_height: f64,
landing_length: f64,
ucs: Mat4,
) -> (DVec3, AttachmentPoint) {
let last = *verts.last().unwrap();
let prev = verts[verts.len() - 2];
// Side + landing run along the UCS X axis (identity = world).
let ux = ucs.transform_vector3(Vec3::X).normalize_or(Vec3::X).as_dvec3();
let ux = ucs
.transform_vector3(Vec3::X)
.normalize_or(Vec3::X)
.as_dvec3();
let to_right = (last - prev).dot(ux) >= 0.0;
let sign = if to_right { 1.0_f64 } else { -1.0_f64 };
let anchor = last + ux * (sign * text_height * 1.5);
let anchor = last + ux * (sign * landing_length);
let attach = if to_right {
AttachmentPoint::MiddleLeft
} else {
AttachmentPoint::MiddleRight
};
(anchor, attach)
}

View file

@ -13,6 +13,8 @@ use crate::command::{CadCommand, CmdResult};
use crate::modules::IconKind;
use crate::scene::model::hatch_model::{HatchModel, HatchPattern, PatFamily};
use crate::scene::model::wire_model::WireModel;
use acadrust::Handle;
use cadkernel::geom2d::{bounded_faces, Line, Tolerance};
use glam::DVec3;
use crate::t;
@ -48,6 +50,13 @@ enum Mode {
Manual,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum HatchMode {
PickInside,
SelectObjects,
Manual,
}
// ── CPU point-in-polygon (ray casting) ────────────────────────────────────
fn point_in_polygon(p: [f64; 2], poly: &[[f64; 2]]) -> bool {
@ -195,23 +204,153 @@ fn rte_boundary(pts: impl Iterator<Item = (f64, f64)>) -> (Vec<[f32; 2]>, [f64;
pub struct HatchCommand {
outlines: Vec<Vec<[f64; 2]>>,
mode: Mode,
boundary_sources: rustc_hash::FxHashMap<Handle, Vec<Line>>,
point_regions: Vec<Vec<Vec<[f64; 2]>>>,
object_regions: Vec<Vec<Vec<[f64; 2]>>>,
selected_objects: Vec<Handle>,
mode: HatchMode,
manual_pts: Vec<DVec3>,
missed: bool,
retain_boundaries: bool,
inherited: Option<(
HatchModel,
acadrust::types::Color,
acadrust::types::Transparency,
)>,
}
impl HatchCommand {
pub fn new(outlines: Vec<Vec<[f64; 2]>>) -> Self {
Self {
pub fn new(
outlines: Vec<Vec<[f64; 2]>>,
boundary_sources: rustc_hash::FxHashMap<Handle, Vec<Line>>,
selected_objects: Vec<Handle>,
inherited: Option<(
HatchModel,
acadrust::types::Color,
acadrust::types::Transparency,
)>,
) -> Self {
let selected_objects: Vec<_> = selected_objects
.into_iter()
.filter(|handle| boundary_sources.contains_key(handle))
.collect();
let has_selection = !selected_objects.is_empty();
let mut command = Self {
outlines,
mode: Mode::PickInside,
boundary_sources,
point_regions: Vec::new(),
object_regions: Vec::new(),
selected_objects: Vec::new(),
mode: if has_selection {
HatchMode::SelectObjects
} else {
HatchMode::PickInside
},
manual_pts: vec![],
missed: false,
retain_boundaries: false,
inherited,
};
command.set_object_selection(selected_objects);
command
}
fn set_object_selection(&mut self, handles: Vec<Handle>) {
let mut segments = Vec::new();
for handle in &handles {
if let Some(source) = self.boundary_sources.get(handle) {
segments.extend(source.iter().copied());
}
}
self.object_regions = bounded_faces(&segments, Tolerance::new(1.0e-6))
.into_iter()
.map(|ring| vec![ring])
.collect();
self.missed = !handles.is_empty() && self.object_regions.is_empty();
self.selected_objects = handles;
}
fn add_point_region(&mut self, rings: Vec<Vec<[f64; 2]>>) {
let duplicate = rings.first().is_some_and(|outer| {
self.point_regions
.iter()
.any(|region| region.first() == Some(outer))
});
if !duplicate {
self.point_regions.push(rings);
}
}
fn region_count(&self) -> usize {
self.point_regions.len() + self.object_regions.len()
}
fn combined_rings(&self) -> Vec<Vec<[f64; 2]>> {
let mut rings = Vec::new();
for ring in self
.point_regions
.iter()
.chain(self.object_regions.iter())
.flat_map(|region| region.iter())
{
if !rings.iter().any(|existing| existing == ring) {
rings.push(ring.clone());
}
}
rings
}
fn make_hatch(&self, rings: Vec<Vec<[f64; 2]>>) -> HatchModel {
let (rel, origin, wcs) = pack_rings(&rings);
let exterior = cadkernel::geom2d::ring_nesting_depths(&rings)
.into_iter()
.map(|depth| depth % 2 == 0)
.collect();
let boundary_sources = rings
.iter()
.map(|ring| crate::scene::ring_source_handles(ring, &self.boundary_sources))
.collect();
if let Some((source, _, _)) = &self.inherited {
let mut pattern = source.pattern.clone();
if let HatchPattern::Pattern(families) = &mut pattern {
let scale = if source.scale.abs() > 1.0e-6 {
source.scale
} else {
1.0
};
let (sin, cos) = source.angle_offset.sin_cos();
for family in families {
let base_x = source.world_origin[0]
+ (family.x0 as f64 * cos as f64
- family.y0 as f64 * sin as f64)
* scale as f64;
let base_y = source.world_origin[1]
+ (family.x0 as f64 * sin as f64
+ family.y0 as f64 * cos as f64)
* scale as f64;
let dx = base_x - origin[0];
let dy = base_y - origin[1];
family.x0 = ((dx * cos as f64 + dy * sin as f64) / scale as f64) as f32;
family.y0 = ((-dx * sin as f64 + dy * cos as f64) / scale as f64) as f32;
}
}
return HatchModel {
render_instance: None,
boundary: std::sync::Arc::new(rel),
pattern,
name: source.name.clone(),
color: source.color,
aci: source.aci,
line_weight_px: source.line_weight_px,
angle_offset: source.angle_offset,
scale: source.scale,
world_origin: origin,
boundary_wcs: Some(std::sync::Arc::new(wcs)),
boundary_exterior: Some(std::sync::Arc::new(exterior)),
boundary_sources: Some(std::sync::Arc::new(boundary_sources)),
draw_depth: source.draw_depth,
};
}
// Default: ANSI31 from catalog; fallback to a single 45° family.
let pat_name = "ANSI31";
let families = crate::scene::model::hatch_patterns::find(pat_name)
@ -246,6 +385,8 @@ impl HatchCommand {
scale: 1.0,
world_origin: origin,
boundary_wcs: Some(std::sync::Arc::new(wcs)),
boundary_exterior: Some(std::sync::Arc::new(exterior)),
boundary_sources: Some(std::sync::Arc::new(boundary_sources)),
draw_depth: 0.0,
}
}
@ -258,15 +399,34 @@ impl CadCommand for HatchCommand {
fn prompt(&self) -> String {
match &self.mode {
Mode::PickInside => {
HatchMode::PickInside => {
let miss = if self.missed {
t!(" ⚠ No closed boundary found.").into_owned()
} else {
String::new()
};
t!("HATCH Pick internal point:%{miss}", miss = miss).into_owned()
t!(
"HATCH Pick internal point (%{count} regions selected, Enter to apply):%{miss}",
count = self.region_count(),
miss = miss
)
.into_owned()
}
Mode::Manual => {
HatchMode::SelectObjects => {
let miss = if self.missed {
t!(" ⚠ Selection has no closed boundary.").into_owned()
} else {
String::new()
};
t!(
"HATCH Select boundary objects (%{objects} objects, %{count} regions; Enter to apply):%{miss}",
objects = self.selected_objects.len(),
count = self.region_count(),
miss = miss
)
.into_owned()
}
HatchMode::Manual => {
if self.manual_pts.is_empty() {
t!("HATCH Boundary point 1:").into_owned()
} else {
@ -280,8 +440,43 @@ impl CadCommand for HatchCommand {
fn options(&self) -> Vec<crate::command::CmdOption> {
use crate::command::CmdOption;
match &self.mode {
Mode::PickInside => vec![CmdOption::new(t!("Draw manually").as_ref(), "S")],
Mode::Manual => {
HatchMode::PickInside => {
let mut options = vec![
CmdOption::new(t!("Select objects").as_ref(), "O"),
CmdOption::new(t!("Draw manually").as_ref(), "S"),
CmdOption::new(
if self.retain_boundaries {
"Keep boundaries: on"
} else {
"Keep boundaries: off"
},
"B",
),
];
if self.region_count() > 0 {
options.push(CmdOption::enter(t!("Accept").as_ref()));
}
options
}
HatchMode::SelectObjects => {
let mut options = vec![
CmdOption::new(t!("Pick internal points").as_ref(), "I"),
CmdOption::new(t!("Draw manually").as_ref(), "S"),
CmdOption::new(
if self.retain_boundaries {
"Keep boundaries: on"
} else {
"Keep boundaries: off"
},
"B",
),
];
if self.region_count() > 0 {
options.push(CmdOption::enter(t!("Accept").as_ref()));
}
options
}
HatchMode::Manual => {
// Enter accepts the boundary once at least 3 points are picked.
if self.manual_pts.len() >= 3 {
vec![CmdOption::enter(t!("Accept").as_ref())]
@ -294,12 +489,13 @@ impl CadCommand for HatchCommand {
fn on_point(&mut self, pt: DVec3) -> CmdResult {
match &self.mode {
Mode::PickInside => {
HatchMode::PickInside => {
let xy = [pt.x, pt.y];
match resolve_hatch_rings(&self.outlines, xy) {
Some(rings) => {
self.missed = false;
return CmdResult::CommitHatch(self.make_hatch(rings));
self.add_point_region(rings);
CmdResult::NeedPoint
}
None => {
self.missed = true;
@ -307,7 +503,8 @@ impl CadCommand for HatchCommand {
}
}
}
Mode::Manual => {
HatchMode::SelectObjects => CmdResult::NeedPoint,
HatchMode::Manual => {
// Keep the typed/snapped point exact (issue #311).
self.manual_pts.push(pt);
CmdResult::NeedPoint
@ -316,16 +513,70 @@ impl CadCommand for HatchCommand {
}
fn on_enter(&mut self) -> CmdResult {
match &self.mode {
Mode::PickInside => CmdResult::Cancel,
Mode::Manual => {
if self.manual_pts.len() < 3 {
return CmdResult::Cancel;
}
let wcs = self.manual_pts.iter().map(|p| [p.x, p.y]).collect();
CmdResult::CommitHatch(self.make_hatch(vec![wcs]))
}
if matches!(self.mode, HatchMode::Manual) && self.manual_pts.len() >= 3 {
let ring = self.manual_pts.iter().map(|p| [p.x, p.y]).collect();
self.add_point_region(vec![ring]);
}
let rings = self.combined_rings();
if rings.is_empty() {
CmdResult::Cancel
} else if self.retain_boundaries {
CmdResult::CommitHatchWithBoundaries {
hatch: self.make_hatch(rings.clone()),
boundaries: crate::scene::boundary_entities(&rings),
entity_style: self
.inherited
.as_ref()
.map(|(_, color, transparency)| (color.clone(), *transparency)),
}
} else if let Some((_, color, transparency)) = &self.inherited {
CmdResult::CommitStyledHatch {
hatch: self.make_hatch(rings),
color: color.clone(),
transparency: *transparency,
}
} else {
CmdResult::CommitHatch(self.make_hatch(rings))
}
}
fn is_selection_gathering(&self) -> bool {
matches!(self.mode, HatchMode::SelectObjects)
}
fn selection_forces_add(&self) -> bool {
matches!(self.mode, HatchMode::SelectObjects)
}
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
if matches!(self.mode, HatchMode::SelectObjects) {
self.set_object_selection(handles);
CmdResult::NeedPoint
} else {
CmdResult::Cancel
}
}
fn on_undo_step(&mut self) -> Option<CmdResult> {
if matches!(self.mode, HatchMode::PickInside) && self.point_regions.pop().is_some() {
Some(CmdResult::NeedPoint)
} else {
None
}
}
fn hatch_preview_models(&self) -> Option<Vec<HatchModel>> {
let mut rings = self.combined_rings();
if matches!(self.mode, HatchMode::Manual) && self.manual_pts.len() >= 3 {
rings.push(self.manual_pts.iter().map(|point| [point.x, point.y]).collect());
}
Some(if rings.is_empty() {
Vec::new()
} else {
let mut preview = self.make_hatch(rings);
preview.color = [0.15, 0.55, 1.0, 0.75];
vec![preview]
})
}
fn on_escape(&mut self) -> CmdResult {
@ -333,20 +584,36 @@ impl CadCommand for HatchCommand {
}
fn wants_text_input(&self) -> bool {
matches!(self.mode, Mode::PickInside)
!matches!(self.mode, HatchMode::Manual)
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
if text.trim().eq_ignore_ascii_case("s") {
self.mode = Mode::Manual;
self.missed = false;
return Some(CmdResult::NeedPoint);
match text.trim().to_ascii_uppercase().as_str() {
"O" | "OBJECT" | "OBJECTS" => {
self.mode = HatchMode::SelectObjects;
self.missed = false;
Some(CmdResult::NeedPoint)
}
"I" | "INTERNAL" => {
self.mode = HatchMode::PickInside;
self.missed = false;
Some(CmdResult::NeedPoint)
}
"S" => {
self.mode = HatchMode::Manual;
self.missed = false;
Some(CmdResult::NeedPoint)
}
"B" | "BOUNDARY" | "BOUNDARIES" => {
self.retain_boundaries = !self.retain_boundaries;
Some(CmdResult::NeedPoint)
}
_ => None,
}
None
}
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { let pt = pt.as_vec3();
if let Mode::Manual = &self.mode {
if let HatchMode::Manual = &self.mode {
if self.manual_pts.is_empty() {
return None;
}
@ -416,6 +683,8 @@ impl GradientCommand {
scale: 1.0,
world_origin: origin,
boundary_wcs: Some(std::sync::Arc::new(wcs)),
boundary_exterior: None,
boundary_sources: None,
draw_depth: 0.0,
}
}
@ -613,23 +882,7 @@ impl CadCommand for BoundaryCommand {
match resolve_hatch_rings(&self.outlines, xy) {
Some(rings) => {
self.missed = false;
// Store as a Hatch entity (solid fill) so it is selectable.
let (rel, origin, wcs) = pack_rings(&rings);
let model = HatchModel {
render_instance: None,
boundary: std::sync::Arc::new(rel),
pattern: HatchPattern::Solid,
name: "SOLID".into(),
color: [0.45, 0.45, 0.45, 0.60],
aci: 0,
line_weight_px: 1.0,
angle_offset: 0.0,
scale: 1.0,
world_origin: origin,
boundary_wcs: Some(std::sync::Arc::new(wcs)),
draw_depth: 0.0,
};
CmdResult::CommitHatch(model)
CmdResult::CommitEntitiesAndExit(crate::scene::boundary_entities(&rings))
}
None => {
self.missed = true;

View file

@ -160,18 +160,18 @@ impl CadModule for DrawModule {
label: "Leader",
icon: leader_cmd::ICON,
items: vec![
(
mleader_cmd::tool().id,
mleader_cmd::tool().label,
mleader_cmd::tool().icon,
),
(
leader_cmd::tool().id,
leader_cmd::tool().label,
leader_cmd::tool().icon,
),
(
mleader_cmd::tool().id,
mleader_cmd::tool().label,
mleader_cmd::tool().icon,
),
],
default: "MLEADER",
default: "LEADER",
},
],
},

View file

@ -348,9 +348,13 @@ mod loader {
pub fn remove_plugin(id: &str) -> bool {
MANAGER.with(|m| {
if let Some(manager) = m.borrow_mut().as_mut() {
manager.remove(id)
if manager.ids().iter().any(|loaded| loaded == id) {
manager.remove(id)
} else {
true
}
} else {
false
true
}
})
}

View file

@ -480,7 +480,15 @@ pub fn annotative_offscale_for(
}),
}
}
pub(crate) fn annotation_scale_handles_for_entity(
doc: &CadDocument,
entity_handle: Handle,
) -> Vec<Handle> {
object_scale_memberships(doc, entity_handle)
.into_iter()
.map(|(_, scale_handle)| scale_handle)
.collect()
}
pub fn scale_handle_by_name(doc: &CadDocument, name: &str) -> Option<Handle> {
doc.objects.iter().find_map(|(handle, object)| match object {
ObjectType::Scale(scale)
@ -662,7 +670,19 @@ pub fn effective_annotation_scale_for(
if !context_annotative && (text_like || !style_annotative) {
return 1.0;
}
// Annotative TEXT/MTEXT store their paper text height as the base value.
// `fallback` is the absolute paper-to-model multiplier for the active
// annotation scale and already includes the drawing's INSUNITS conversion.
//
// Example for a metre drawing with 2 mm paper text:
// 1:1 -> 2 * 0.001 = 0.002 m
// 1:100 -> 2 * 0.100 = 0.200 m
//
// Using active/native here would only produce 1 / 100 and would lose the
// millimetre-to-metre conversion entirely.
if matches!(entity, EntityType::Text(_) | EntityType::MText(_)) {
return fallback;
}
if matches!(
entity,
EntityType::Dimension(_)

View file

@ -1,6 +1,9 @@
use super::*;
use cadkernel::geom2d::{bounded_faces, Line, Tolerance};
use cadkernel::geom2d::{
bounded_faces, contains, distance_to, segment_crossing, triangulate, Curve, Line,
SegmentCrossing, Tolerance,
};
/// How far apart two points may be and still be taken for the same one.
///
@ -10,7 +13,292 @@ use cadkernel::geom2d::{bounded_faces, Line, Tolerance};
/// enough not to weld genuinely separate corners together.
const WELD_TOLERANCE: f64 = 1.0e-6;
fn wire_segments(wire: &WireModel) -> Vec<Line> {
let mut segments = Vec::new();
let mut previous: Option<[f64; 2]> = None;
for (index, high) in wire.points.iter().copied().enumerate() {
if !high[0].is_finite() || !high[1].is_finite() {
previous = None;
continue;
}
let low = wire.points_low.get(index).copied().unwrap_or([0.0; 3]);
let current = [
high[0] as f64 + low[0] as f64,
high[1] as f64 + low[1] as f64,
];
if let Some(start) = previous {
let (dx, dy) = (current[0] - start[0], current[1] - start[1]);
if dx.hypot(dy) > WELD_TOLERANCE {
segments.push(Line { start, end: current });
}
}
previous = Some(current);
}
segments
}
fn ring_seed(model: &HatchModel, wanted: usize) -> Option<[f64; 2]> {
let mut ring = Vec::new();
let mut index = 0usize;
for &[x, y] in model.boundary.iter() {
if x.is_finite() && y.is_finite() {
if index == wanted {
ring.push([
model.world_origin[0] + x as f64,
model.world_origin[1] + y as f64,
]);
}
} else if index == wanted {
break;
} else {
index += 1;
}
}
if ring.len() < 3 {
return None;
}
let (points, triangles) = triangulate(&ring, &[]);
if let Some(triangle) = triangles.first() {
let [a, b, c] = triangle.map(|vertex| points[vertex]);
return Some([
(a[0] + b[0] + c[0]) / 3.0,
(a[1] + b[1] + c[1]) / 3.0,
]);
}
Some([
ring.iter().map(|point| point[0]).sum::<f64>() / ring.len() as f64,
ring.iter().map(|point| point[1]).sum::<f64>() / ring.len() as f64,
])
}
fn face_curves(face: &[[f64; 2]]) -> Vec<Curve> {
face.iter()
.copied()
.zip(face.iter().copied().cycle().skip(1))
.take(face.len())
.map(|(start, end)| Curve::Line(Line { start, end }))
.collect()
}
fn matching_face(faces: &[Vec<[f64; 2]>], seed: Option<[f64; 2]>) -> Option<&Vec<[f64; 2]>> {
if faces.len() == 1 {
return faces.first();
}
let seed = seed?;
let tolerance = Tolerance::new(WELD_TOLERANCE);
if let Some(face) = faces.iter().find(|face| {
let curves = face_curves(face);
contains(&curves, seed, tolerance)
}) {
return Some(face);
}
faces.iter().min_by(|a, b| {
let nearest = |face: &Vec<[f64; 2]>| {
face_curves(face)
.iter()
.map(|curve| distance_to(curve, seed))
.fold(f64::INFINITY, f64::min)
};
nearest(a).total_cmp(&nearest(b))
})
}
pub(crate) fn ring_source_handles(
ring: &[[f64; 2]],
sources: &rustc_hash::FxHashMap<acadrust::Handle, Vec<Line>>,
) -> Vec<acadrust::Handle> {
let mut handles = rustc_hash::FxHashSet::default();
for (&start, &end) in ring
.iter()
.zip(ring.iter().cycle().skip(1))
.take(ring.len())
{
let edge = Line { start, end };
let edge_length = (end[0] - start[0]).hypot(end[1] - start[1]);
for (&handle, lines) in sources {
if lines.iter().any(|line| {
matches!(
segment_crossing(edge, *line, Tolerance::new(WELD_TOLERANCE)),
SegmentCrossing::Overlap { a, .. }
if (a[1] - a[0]).abs() * edge_length > WELD_TOLERANCE
)
}) {
handles.insert(handle);
}
}
}
let mut handles: Vec<_> = handles.into_iter().collect();
handles.sort_by_key(|handle| handle.value());
handles
}
pub(crate) fn boundary_entities(rings: &[Vec<[f64; 2]>]) -> Vec<acadrust::EntityType> {
rings
.iter()
.filter_map(|ring| {
let mut points = Vec::with_capacity(ring.len());
for &point in ring {
if point.iter().all(|value| value.is_finite())
&& points.last() != Some(&point)
{
points.push(point);
}
}
if points.len() > 1 && points.first() == points.last() {
points.pop();
}
if points.len() < 3 {
return None;
}
let mut polyline = acadrust::entities::LwPolyline::new();
polyline.is_closed = true;
polyline.vertices = points
.into_iter()
.map(|[x, y]| {
acadrust::entities::LwVertex::new(acadrust::types::Vector2::new(x, y))
})
.collect();
Some(acadrust::EntityType::LwPolyline(polyline))
})
.collect()
}
impl Scene {
fn associative_boundary_segments(&self, handles: &[Handle]) -> Vec<Line> {
self.wire_models_for(handles)
.iter()
.flat_map(wire_segments)
.collect()
}
fn associative_hatch_dependents(
&self,
changed: &rustc_hash::FxHashSet<Handle>,
) -> Vec<Handle> {
if self.associative_hatch_source_cache.borrow().is_none() {
let mut index: HashMap<Handle, Vec<Handle>> = HashMap::default();
for entity in self.document.entities() {
let EntityType::Hatch(hatch) = entity else {
continue;
};
if !hatch.is_associative {
continue;
}
for source in hatch
.paths
.iter()
.flat_map(|path| path.boundary_handles.iter().copied())
{
let dependents = index.entry(source).or_default();
if !dependents.contains(&hatch.common.handle) {
dependents.push(hatch.common.handle);
}
}
}
*self.associative_hatch_source_cache.borrow_mut() = Some(index);
}
let cache = self.associative_hatch_source_cache.borrow();
let index = cache.as_ref().expect("associative hatch index");
let mut handles = rustc_hash::FxHashSet::default();
for source in changed {
if let Some(dependents) = index.get(source) {
handles.extend(dependents.iter().copied());
}
}
handles.into_iter().collect()
}
pub(crate) fn refresh_associative_hatches(
&mut self,
changes: &[(Handle, ChangeKind)],
) -> Vec<(Handle, ChangeKind)> {
if changes.is_empty() {
return Vec::new();
}
let changed: rustc_hash::FxHashSet<_> =
changes.iter().map(|(handle, _)| *handle).collect();
let candidates: Vec<_> = self
.associative_hatch_dependents(&changed)
.into_iter()
.filter_map(|handle| {
let EntityType::Hatch(hatch) = self.document.get_entity(handle)? else {
return None;
};
Some({
let seeds = self
.hatches
.get(&hatch.common.handle)
.map(|model| {
(0..hatch.paths.len())
.map(|index| ring_seed(model, index))
.collect::<Vec<_>>()
})
.unwrap_or_default();
(handle, hatch.clone(), seeds)
})
})
.collect();
let mut refreshed = Vec::new();
for (handle, mut hatch, seeds) in candidates {
let normal = hatch.normal;
if normal.x.abs() > 1.0e-8 || normal.y.abs() > 1.0e-8 {
continue;
}
let mut modified = false;
let mut association_changed = false;
for (index, path) in hatch.paths.iter_mut().enumerate() {
if !path
.boundary_handles
.iter()
.any(|source| changed.contains(source))
{
continue;
}
let old_count = path.boundary_handles.len();
path.boundary_handles
.retain(|source| self.document.get_entity(*source).is_some());
association_changed |= path.boundary_handles.len() != old_count;
modified |= association_changed;
let segments = self.associative_boundary_segments(&path.boundary_handles);
let faces = bounded_faces(&segments, Tolerance::new(WELD_TOLERANCE));
let Some(face) = matching_face(&faces, seeds.get(index).copied().flatten()) else {
continue;
};
path.edges = vec![acadrust::entities::hatch::BoundaryEdge::Polyline(
acadrust::entities::hatch::PolylineEdge::new(
face.iter()
.map(|point| acadrust::types::Vector2::new(point[0], point[1]))
.collect(),
true,
),
)];
modified = true;
}
hatch.is_associative = hatch
.paths
.iter()
.any(|path| !path.boundary_handles.is_empty());
if !modified {
continue;
}
if association_changed {
self.associative_hatch_source_cache.borrow_mut().take();
}
if self.is_recording_undo() {
let before = self.document.get_entity_arc(handle);
self.record_undo_before(handle, before);
}
if let Some(slot) = self.document.get_entity_mut(handle) {
*slot = EntityType::Hatch(hatch);
self.refresh_fill_model(handle);
refreshed.push((handle, ChangeKind::Modified));
}
}
refreshed
}
/// Build closed planar regions from the visible wire geometry.
///
/// Unlike `closed_outlines()`, the source entities do not need to be closed
@ -30,41 +318,26 @@ impl Scene {
let mut segments = Vec::<Line>::new();
for wire in self.entity_wires().iter() {
let mut previous: Option<[f64; 2]> = None;
for (index, high) in wire.points.iter().copied().enumerate() {
// NaNs delimit independent segments inside some WireModels,
// notably polylines stored as A-B | B-C | C-D.
if !high[0].is_finite() || !high[1].is_finite() {
previous = None;
continue;
}
// The wire carries its coordinates as a double-single pair, so
// both halves are needed to recover the f64 the tessellation
// produced. Reading only the high half would put every vertex
// of a survey-coordinate drawing on a grid coarser than the
// weld tolerance.
let low = wire.points_low.get(index).copied().unwrap_or([0.0; 3]);
let current = [
high[0] as f64 + low[0] as f64,
high[1] as f64 + low[1] as f64,
];
if let Some(start) = previous {
let (dx, dy) = (current[0] - start[0], current[1] - start[1]);
if dx.hypot(dy) > WELD_TOLERANCE {
segments.push(Line {
start,
end: current,
});
}
}
previous = Some(current);
}
segments.extend(wire_segments(wire));
}
bounded_faces(&segments, Tolerance::new(WELD_TOLERANCE))
}
/// Tessellated boundary segments grouped by their selectable entity.
pub fn hatch_boundary_sources(
&self,
) -> rustc_hash::FxHashMap<acadrust::Handle, Vec<Line>> {
let mut sources = rustc_hash::FxHashMap::default();
for wire in self.entity_wires().iter() {
let Some(handle) = Self::handle_from_wire_name(&wire.name) else {
continue;
};
sources
.entry(handle)
.or_insert_with(Vec::new)
.extend(wire_segments(wire));
}
sources
}
}

View file

@ -306,6 +306,14 @@ impl Scene {
handle
}
pub fn rename_layer(&mut self, old: &str, new: &str) -> bool {
if self.document.rename_layer(old, new).is_err() {
return false;
}
self.invalidate_dependency_index();
true
}
/// Rename a block definition: re-key its record, update the Block marker's
/// name, and repoint every INSERT that referenced the old name so all
/// instances keep resolving. Returns false if `old` is missing or
@ -737,7 +745,12 @@ impl Scene {
let mut insert = DxfInsert::new(name, acadrust::types::Vector3::ZERO);
acadrust::Entity::apply_transform(&mut insert, block_to_world);
Ok(self.add_entity(EntityType::Insert(insert)))
let insert_handle = self.add_entity(EntityType::Insert(insert));
// A new block definition landed in the document; advance the block
// epoch so consumers (the block palette stale check) notice it even
// when the panel stays open. Mirrors `define_block_from_owned_entities`.
self.bump_geometry();
Ok(insert_handle)
}
/// Define a new block named `name` from `entities` (owned, not yet in the
@ -1401,6 +1414,8 @@ impl Scene {
render_instance,
boundary: Arc::new(boundary),
boundary_wcs: None,
boundary_exterior: None,
boundary_sources: None,
pattern: model::hatch_model::HatchPattern::Solid,
name: "WIPEOUT_FILL".into(),
color,
@ -1505,6 +1520,7 @@ impl Scene {
}
let mut boundary: Vec<[f64; 2]> = Vec::new();
let mut boundary_exterior = Vec::new();
for path in &dxf.paths {
// Skip TEXTBOX boundary paths (flag bit 3). These are text
@ -1538,6 +1554,7 @@ impl Scene {
boundary.truncate(before_path);
continue;
}
boundary_exterior.push(path.flags.is_external() || path.flags.is_outermost());
if boundary.len() >= path_start + 3 {
let first = boundary[path_start];
let last = *boundary.last().unwrap();
@ -1550,6 +1567,11 @@ impl Scene {
if boundary.is_empty() {
return None;
}
if !boundary_exterior.iter().any(|role| *role) {
if let Some(first) = boundary_exterior.first_mut() {
*first = true;
}
}
// The batched hatch renderer keeps boundaries in a GPU storage
// buffer (no fixed length), so a hatch with many island loops must
// retain *every* loop or even-odd island detection breaks. The old
@ -1744,6 +1766,14 @@ impl Scene {
render_instance: None,
boundary: std::sync::Arc::new(boundary_f32),
boundary_wcs: None,
boundary_exterior: Some(std::sync::Arc::new(boundary_exterior)),
boundary_sources: Some(std::sync::Arc::new(
dxf.paths
.iter()
.filter(|path| path.flags.bits() & 8 == 0)
.map(|path| path.boundary_handles.clone())
.collect(),
)),
pattern,
name,
// A gradient starts from its first stop; other fills use the
@ -2058,6 +2088,8 @@ impl Scene {
render_instance: None,
boundary: std::sync::Arc::new(boundary),
boundary_wcs: None,
boundary_exterior: None,
boundary_sources: None,
pattern: model::hatch_model::HatchPattern::Solid,
name: "SOLID".into(),
color,
@ -2070,79 +2102,148 @@ impl Scene {
}
}
pub fn add_hatch(&mut self, model: HatchModel, layer: Option<&str>) -> Handle {
pub fn add_hatch(
&mut self,
model: HatchModel,
layer: Option<&str>,
entity_style: Option<(acadrust::types::Color, acadrust::types::Transparency)>,
) -> Handle {
let mut dxf = DxfHatch::new();
dxf.is_solid = matches!(
model.pattern,
crate::scene::model::hatch_model::HatchPattern::Solid
);
// Prefer the command-supplied exact f64 boundary so a typed vertex is
// persisted without f32 quantization (issue #311). Falling back to the
// render-side `boundary`, the points arrive in local render space
// (world_offset already subtracted), so add `world_origin` back —
// otherwise the boundary wire lands `world_offset` away from the fill.
// Build one DXF boundary path per ring. The command layer separates
// the outer boundary from its holes with NaN sentinels in
// `boundary_wcs`; split on those so nested hatches (e.g. a small
// rectangle inside a big one) persist with real holes instead of a
// single self-intersecting polyline. Only the FIRST ring carries the
// external / outermost flags — every later ring is a hole and must not
// be flagged external, or DXF/DWG consumers treat the inner loop as
// another outer island rather than a hole.
if let Some(wcs) = &model.boundary_wcs {
let mut ring: Vec<Vector2> = Vec::new();
let mut first = true;
let mut push_ring = |r: &mut Vec<Vector2>, is_outer: bool| {
if !r.is_empty() {
let edge = PolylineEdge::new(std::mem::take(r), true);
let mut path = if is_outer {
let mut p = BoundaryPath::external();
p.flags = acadrust::entities::hatch::BoundaryPathFlags::from_bits(
p.flags.bits()
| acadrust::entities::hatch::BoundaryPathFlags::OUTERMOST.bits(),
);
p
} else {
BoundaryPath::new()
};
path.add_edge(BoundaryEdge::Polyline(edge));
dxf.paths.push(path);
}
};
for &[x, y] in wcs.iter() {
if x.is_finite() && y.is_finite() {
ring.push(Vector2::new(x, y));
} else {
let is_outer = first;
first = false;
push_ring(&mut ring, is_outer);
}
}
push_ring(&mut ring, first);
}
if dxf.paths.is_empty() {
let wx = model.world_origin[0];
let wy = model.world_origin[1];
let verts: Vec<Vector2> = model
// Prefer exact command geometry; otherwise reconstruct every ring from
// the render offsets without dropping its separators.
// Build one DXF path per NaN-separated ring and retain each outer/hole role.
let reconstructed_wcs: Vec<[f64; 2]> = if model.boundary_wcs.is_none() {
let [wx, wy] = model.world_origin;
model
.boundary
.iter()
.filter(|v| v[0].is_finite() && v[1].is_finite())
.map(|&[x, y]| Vector2::new(x as f64 + wx, y as f64 + wy))
.collect();
let edge = PolylineEdge::new(verts, true);
let mut path = BoundaryPath::external();
path.add_edge(BoundaryEdge::Polyline(edge));
dxf.paths.push(path);
.map(|&[x, y]| {
if x.is_finite() && y.is_finite() {
[x as f64 + wx, y as f64 + wy]
} else {
[f64::NAN, f64::NAN]
}
})
.collect()
} else {
Vec::new()
};
let wcs = model
.boundary_wcs
.as_deref()
.map(|points| points.as_slice())
.unwrap_or(reconstructed_wcs.as_slice());
let mut ring: Vec<Vector2> = Vec::new();
let mut first = true;
let mut ring_index = 0usize;
let mut push_ring = |r: &mut Vec<Vector2>, is_outer: bool, index: usize| {
if !r.is_empty() {
let edge = PolylineEdge::new(std::mem::take(r), true);
let mut path = if is_outer {
let mut p = BoundaryPath::external();
p.flags = acadrust::entities::hatch::BoundaryPathFlags::from_bits(
p.flags.bits()
| acadrust::entities::hatch::BoundaryPathFlags::OUTERMOST.bits(),
);
p
} else {
BoundaryPath::new()
};
path.add_edge(BoundaryEdge::Polyline(edge));
if let Some(handles) = model
.boundary_sources
.as_deref()
.and_then(|sources| sources.get(index))
{
for &handle in handles {
if handle.is_valid() {
path.add_boundary_handle(handle);
}
}
}
dxf.paths.push(path);
}
};
for &[x, y] in wcs {
if x.is_finite() && y.is_finite() {
ring.push(Vector2::new(x, y));
} else {
let is_outer = model
.boundary_exterior
.as_deref()
.and_then(|roles| roles.get(ring_index))
.copied()
.unwrap_or(first);
first = false;
if !ring.is_empty() {
push_ring(&mut ring, is_outer, ring_index);
ring_index += 1;
}
}
}
if let Some(entry) = crate::scene::model::hatch_patterns::find(&model.name) {
dxf.pattern = crate::scene::model::hatch_patterns::build_dxf_pattern(entry);
}
dxf.pattern_angle = model.angle_offset as f64;
dxf.pattern_scale = if model.scale.abs() > 1e-6 {
let is_outer = model
.boundary_exterior
.as_deref()
.and_then(|roles| roles.get(ring_index))
.copied()
.unwrap_or(first);
push_ring(&mut ring, is_outer, ring_index);
dxf.is_associative = dxf
.paths
.iter()
.any(|path| !path.boundary_handles.is_empty());
let pattern_scale = if model.scale.abs() > 1e-6 {
model.scale as f64
} else {
1.0
};
if let crate::scene::model::hatch_model::HatchPattern::Pattern(families) = &model.pattern {
let mut pattern = acadrust::entities::HatchPattern::new(&model.name);
let rotation = model.angle_offset as f64;
let (rotation_sin, rotation_cos) = rotation.sin_cos();
for family in families {
let family_angle = (family.angle_deg as f64).to_radians();
let angle = family_angle + rotation;
let (family_sin, family_cos) = family_angle.sin_cos();
let local_offset_x = family.dx as f64 * family_cos
- family.dy as f64 * family_sin;
let local_offset_y = family.dx as f64 * family_sin
+ family.dy as f64 * family_cos;
let base_x = family.x0 as f64 * pattern_scale;
let base_y = family.y0 as f64 * pattern_scale;
pattern.lines.push(acadrust::entities::HatchPatternLine {
angle,
base_point: Vector2::new(
model.world_origin[0]
+ base_x * rotation_cos
- base_y * rotation_sin,
model.world_origin[1]
+ base_x * rotation_sin
+ base_y * rotation_cos,
),
offset: Vector2::new(
(local_offset_x * rotation_cos
- local_offset_y * rotation_sin)
* pattern_scale,
(local_offset_x * rotation_sin
+ local_offset_y * rotation_cos)
* pattern_scale,
),
dash_lengths: family
.dashes
.iter()
.map(|dash| *dash as f64 * pattern_scale)
.collect(),
});
}
dxf.pattern = pattern;
}
dxf.pattern_angle = model.angle_offset as f64;
dxf.pattern_scale = pattern_scale;
// A gradient fill must be encoded on the DXF entity itself: the render
// model is rebuilt from the entity below (`add_entity` →
// `hatch_model_from_dxf`), so a gradient kept only on the command's
@ -2198,6 +2299,10 @@ impl Scene {
if let Some(layer) = layer {
entity.as_entity_mut().set_layer(layer.to_string());
}
if let Some((color, transparency)) = entity_style {
entity.common_mut().color = color;
entity.common_mut().transparency = transparency;
}
self.add_entity(entity)
}
@ -2210,6 +2315,7 @@ impl Scene {
self.preview_text = vec![];
self.current_layout = "Model".to_string();
self.hatches = HashMap::default();
self.associative_hatch_source_cache.borrow_mut().take();
self.images = HashMap::default();
self.meshes = HashMap::default();
self.block_meshes = HashMap::default();

View file

@ -33,6 +33,8 @@ mod project;
mod scene_markers;
mod selection;
pub(crate) use boundary::{boundary_entities, ring_source_handles};
// Parallel tessellation free functions live in `convert::tess` (alongside the
// other tessellation code); re-exported here so this root and sibling topic
// modules (each does `use super::*`) keep referencing them unqualified.
@ -147,6 +149,7 @@ use acadrust::entities::{
BoundaryEdge, BoundaryPath, Hatch as DxfHatch, PolylineEdge, Solid as DxfSolid,
};
use acadrust::objects::ObjectType;
use acadrust::tables::normalize_name;
use acadrust::types::Vector2;
use acadrust::{CadDocument, EntityType, Handle, TableEntry};
use glam;
@ -872,10 +875,12 @@ pub fn prepare_open_geometry(
scene.local_center = caches.local_center;
scene.bg_color = model_bg;
let cannoscale_value = scene.document.header.annotation_scale_value;
let unit_factor = scene.annotation_scale_unit_factor();
scene.annotation_scale = if cannoscale_value > 1e-9 {
(1.0 / cannoscale_value) as f32
((1.0 / cannoscale_value) / unit_factor) as f32
} else {
1.0
(1.0 / unit_factor) as f32
};
scene.current_layout = "Model".to_string();
let camera = scene.camera.borrow().clone();
@ -1675,6 +1680,9 @@ pub struct Scene {
/// `geometry_epoch`: a layer colour toggle can reuse the index, invalidate
/// only its dependants, and avoid a whole-document scan on every toggle.
dependency_index_cache: RefCell<Option<SceneDependencyIndex>>,
/// Boundary source → associative hatch handles. Source edits reuse this
/// index instead of scanning the document during every drag step.
associative_hatch_source_cache: RefCell<Option<HashMap<Handle, Vec<Handle>>>>,
/// Tessellated block definitions in block-local coords, keyed by render
/// background and block epoch. Model and Paper adapt black/white colours
/// differently; retaining both variants prevents a full block rebuild on
@ -1891,6 +1899,7 @@ impl Scene {
model_extents_cache: RefCell::new(None),
entity_block_map_cache: RefCell::new(None),
dependency_index_cache: RefCell::new(None),
associative_hatch_source_cache: RefCell::new(None),
block_defn_cache: RefCell::new(HashMap::default()),
entity_index_cache: RefCell::new(None),
last_render_aspect: std::cell::Cell::new(16.0 / 9.0),
@ -2366,6 +2375,21 @@ impl Scene {
}
pub fn bump_entities(&mut self, changes: &[(Handle, ChangeKind)]) {
if changes.iter().any(|(handle, kind)| {
matches!(kind, ChangeKind::Removed)
|| self
.document
.get_entity(*handle)
.is_some_and(|entity| matches!(entity, EntityType::Hatch(_)))
}) {
self.associative_hatch_source_cache.borrow_mut().take();
}
let mut changes = changes.to_vec();
for change in self.refresh_associative_hatches(&changes) {
if !changes.iter().any(|(handle, _)| *handle == change.0) {
changes.push(change);
}
}
if !changes.is_empty() {
// A Modified delta can change layer/style/block references as well
// as coordinates. Rebuild the reverse dependency map lazily on its
@ -2384,14 +2408,14 @@ impl Scene {
.is_some_and(|entity| matches!(entity, EntityType::Light(_)))
});
if cached_light_changed || live_light_changed {
for (handle, _) in changes {
for &(handle, _) in &changes {
let exists = self
.document
.get_entity(*handle)
.get_entity(handle)
.is_some_and(|entity| matches!(entity, EntityType::Light(_)));
crate::entities::object_data::update_light_entity(
&mut self.object_data_cache,
*handle,
handle,
exists,
);
}
@ -2402,7 +2426,7 @@ impl Scene {
{
let mut tm = self.tess_memo.borrow_mut();
let mut rm = self.resident_tess_memo.borrow_mut();
for &(h, k) in changes {
for &(h, k) in &changes {
// A changed or removed entity must re-tessellate; a fresh Add has
// no memo entry yet, so there is nothing to drop.
if matches!(k, ChangeKind::Modified | ChangeKind::Removed) {
@ -2411,7 +2435,7 @@ impl Scene {
}
}
}
self.push_geometry_delta(epoch, changes.to_vec(), false);
self.push_geometry_delta(epoch, changes, false);
}
/// True when any changed handle belongs to an ordinary block definition
@ -3179,6 +3203,8 @@ impl Scene {
world_origin: [0.0, 0.0],
boundary: Arc::new(vec![[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]]),
boundary_wcs: None,
boundary_exterior: None,
boundary_sources: None,
pattern: crate::scene::model::hatch_model::HatchPattern::Solid,
name: "SOLID".to_string(),
color: self.paper_bg_color,
@ -3553,7 +3579,7 @@ impl Scene {
/// The built-in scale factors are defined assuming model and paper use
/// the same base unit. A drawing in metres therefore needs an extra
/// factor of 1000 when its paper side is measured in millimetres.
fn annotation_scale_unit_factor(&self) -> f64 {
pub(crate) fn annotation_scale_unit_factor(&self) -> f64 {
let paper_unit = if self.prefers_imperial_scales() == Some(true) {
1 // Inches
} else {
@ -3836,14 +3862,25 @@ impl Scene {
pub fn set_annotation_scale_named(&mut self, name: &str) -> Option<Handle> {
let handle = self.scale_handle_ensuring(name)?;
let ObjectType::Scale(scale) = self.document.objects.get(&handle)? else {
return None;
let unit_factor = self.annotation_scale_unit_factor();
let (scale_name, scale_factor, multiplier) = {
let ObjectType::Scale(scale) = self.document.objects.get(&handle)? else {
return None;
};
(
scale.name.clone(),
scale.factor(),
scale.inverse_factor() / unit_factor,
)
};
let multiplier = scale.inverse_factor();
self.annotation_scale = multiplier as f32;
self.document.header.current_annotation_scale = scale.name.clone();
self.document.header.annotation_scale_value = scale.factor();
self.document.header.current_annotation_scale = scale_name;
self.document.header.annotation_scale_value = scale_factor;
self.invalidate_annotation_dependencies();
Some(handle)
}
@ -8555,7 +8592,7 @@ impl Scene {
.document
.block_records
.iter()
.map(|record| (record.handle, record.name.to_ascii_uppercase()))
.map(|record| (record.handle, normalize_name(&record.name)))
.collect();
let membership: HashMap<Handle, Handle> = self
.document
@ -8582,7 +8619,7 @@ impl Scene {
let EntityType::Insert(insert) = entity else {
continue;
};
let target = insert.block_name.to_ascii_uppercase();
let target = normalize_name(&insert.block_name);
let common = &insert.common;
let owner = if common.owner_handle.is_null() {
membership
@ -8666,7 +8703,7 @@ impl Scene {
extend_category(&mut index.annotation_geometry);
}
let add = |map: &mut HashMap<String, DependencyTargets>, name: &str| {
let target = map.entry(name.to_ascii_uppercase()).or_default();
let target = map.entry(normalize_name(name)).or_default();
target.render_handles.extend(render_handles.iter().copied());
target.source_handles.insert(common.handle);
target.touches_block_definition |= inside_block;
@ -8805,7 +8842,7 @@ impl Scene {
};
let mut combined = DependencyTargets::default();
for name in names {
let Some(target) = map.get(&name.to_ascii_uppercase()) else {
let Some(target) = map.get(&normalize_name(name)) else {
continue;
};
combined

View file

@ -1,8 +1,5 @@
// HatchModel — CPU-side hatch fill data; rendered entirely on the GPU.
//
// The boundary is a closed polygon in world XY coordinates.
// The GPU fragment shader performs point-in-polygon and hatch-line tests so
// no line geometry is ever tessellated on the CPU.
// CPU-side hatch fill data. The kernel triangulates the boundary and the GPU
// evaluates the pattern inside that mesh.
use std::sync::Arc;
@ -183,6 +180,10 @@ pub struct HatchModel {
/// rebuilt from a DXF entity — `add_hatch` then reconstructs the persisted
/// vertices from `boundary` + `world_origin` instead.
pub boundary_wcs: Option<Arc<Vec<[f64; 2]>>>,
/// Per-ring DXF role, aligned with the NaN-separated boundary paths.
pub boundary_exterior: Option<Arc<Vec<bool>>>,
/// Source entity handles for each boundary ring.
pub boundary_sources: Option<Arc<Vec<Vec<acadrust::Handle>>>>,
/// Fill pattern.
pub pattern: HatchPattern,
/// Catalog name for this pattern (e.g. "ANSI31", "SOLID", "LINEAR").
@ -207,6 +208,40 @@ pub struct HatchModel {
}
impl HatchModel {
/// Indexed local-space fill mesh with even-odd loop containment.
pub(crate) fn fill_mesh(&self) -> (Vec<[f32; 2]>, Vec<u32>) {
let mut rings = Vec::new();
let mut ring = Vec::new();
for &[x, y] in self.boundary.iter() {
if x.is_finite() && y.is_finite() {
ring.push([x as f64, y as f64]);
} else if ring.len() >= 3 {
rings.push(std::mem::take(&mut ring));
} else {
ring.clear();
}
}
if ring.len() >= 3 {
rings.push(ring);
}
let (points, triangles) = cadkernel::geom2d::triangulate_rings(&rings);
let vertices = points
.into_iter()
.map(|[x, y]| [x as f32, y as f32])
.collect();
let mut indices = Vec::with_capacity(triangles.len() * 3);
for triangle in triangles {
for index in triangle {
let Ok(index) = u32::try_from(index) else {
return (Vec::new(), Vec::new());
};
indices.push(index);
}
}
(vertices, indices)
}
/// CPU-side rasteriser for `HatchPattern::Pattern` — produces the line
/// segments inside the boundary so non-GPU consumers (PDF export,
/// `paper_canvas`, print preview) can draw the actual pattern instead

View file

@ -551,11 +551,43 @@ impl Scene {
}
pub fn copy_entities(&mut self, handles: &[Handle], t: &EntityTransform) -> Vec<Handle> {
let copy_handles = self.handles_expanded_for_leader_annotations(handles);
// LEADER + attached MTEXT are a logical pair. Their entity clones must not
// retain the source extension dictionary, otherwise both copies share the
// same annotation-context objects.
let leader_pair_handles: Vec<Handle> = copy_handles
.iter()
.flat_map(|&handle| {
let annotation = match self.document.get_entity(handle) {
Some(EntityType::Leader(leader)) if !leader.annotation_handle.is_null() => {
Some(leader.annotation_handle)
}
_ => None,
};
std::iter::once(handle).chain(annotation)
})
.collect();
// Objects on a locked layer can be selected but not copied.
let clones: Vec<(Handle, EntityType)> = handles
let clones: Vec<(Handle, EntityType, Vec<Handle>)> = copy_handles
.iter()
.filter(|&&h| !self.is_layer_locked(h))
.filter_map(|&h| self.document.get_entity(h).cloned().map(|e| (h, e)))
.filter_map(|&h| {
let entity = self.document.get_entity(h)?.clone();
let annotation_scales = if leader_pair_handles.contains(&h) {
crate::scene::annotative::annotation_scale_handles_for_entity(
&self.document,
h,
)
} else {
Vec::new()
};
Some((h, entity, annotation_scales))
})
.collect();
// MIRRTEXT also governs the copy path (default MIRROR keeps the source
// and adds a mirrored copy): keep the copied text right-reading when the
@ -567,7 +599,7 @@ impl Scene {
let mut new_handles = Vec::with_capacity(clones.len());
let mut handle_map = rustc_hash::FxHashMap::default();
let mut refresh_solid_handles = Vec::new();
for (src_handle, mut entity) in clones {
for (src_handle, mut entity, annotation_scales) in clones {
let text_orient = if preserve_text_orientation {
capture_text_orient(&entity)
} else {
@ -598,9 +630,31 @@ impl Scene {
}
}
Self::reset_clone_subhandles(&mut self.document, &mut entity);
// An annotative LEADER/MTEXT pair must receive a fresh extension dictionary.
// Keeping this handle would make the copy share the source's context tree.
if !annotation_scales.is_empty() {
entity.common_mut().xdictionary_handle = None;
}
entity.common_mut().handle = Handle::NULL;
let h = self.document.add_entity(entity).unwrap_or(Handle::NULL);
if !h.is_null() {
if !annotation_scales.is_empty() {
for scale_handle in annotation_scales {
crate::scene::annotative::create_annotation_context(
&mut self.document,
h,
scale_handle,
);
}
// Annotation contexts add dictionary/object records outside the entity
// delta itself, so keep undo on the safe full-snapshot path.
if self.is_recording_undo() {
self.poison_undo_recording();
}
}
// Delta-undo: a copy's before-image is "nothing" (undo erases it).
if self.is_recording_undo() {
self.record_undo_before(h, None);
@ -640,7 +694,35 @@ impl Scene {
handle_map.insert(src_handle, h);
}
}
// A copied LEADER must reference the copied annotation, never the
// source annotation. Both entities now exist, so remap the stored handle.
let leader_links: Vec<(Handle, Handle)> = handle_map
.iter()
.filter_map(|(&source_handle, &copied_handle)| {
let EntityType::Leader(source_leader) =
self.document.get_entity(source_handle)?
else {
return None;
};
let copied_annotation = handle_map
.get(&source_leader.annotation_handle)
.copied()
.unwrap_or(Handle::NULL);
Some((copied_handle, copied_annotation))
})
.collect();
for (leader_handle, annotation_handle) in leader_links {
if let Some(EntityType::Leader(leader)) =
self.document.get_entity_mut(leader_handle)
{
leader.annotation_handle = annotation_handle;
}
let _ = self.sync_displayed_annotation_context(leader_handle);
}
// Complete group copies record their new Group objects and dictionary
// entry as targeted object deltas inside copy_complete_groups.
self.copy_complete_groups(&handle_map);
@ -956,10 +1038,66 @@ impl Scene {
.get_entity(handle)
.and_then(crate::entities::solid3d::point_of_reference)
.map(|p| [p.x, p.y, p.z]);
// A LEADER's final vertex is the end of its horizontal landing.
// Remember its old position and linked MTEXT so the annotation can follow
// when that grip stretches the landing.
let leader_landing_before = self.document.get_entity(handle).and_then(|entity| {
let EntityType::Leader(leader) = entity else {
return None;
};
let n = leader.vertices.len();
if n < 3 || (grip_id != n - 1 && grip_id != n - 2) || leader.annotation_handle.is_null() {
return None;
}
let point = leader.vertices.last()?;
Some((
leader.annotation_handle,
glam::DVec3::new(point.x, point.y, point.z),
))
});
if let Some(entity) = self.document.get_entity_mut(handle) {
view::dispatch::apply_grip(entity, grip_id, apply);
}
if let Some((annotation_handle, old_landing)) = leader_landing_before {
let new_landing = self.document.get_entity(handle).and_then(|entity| {
let EntityType::Leader(leader) = entity else {
return None;
};
let point = leader.vertices.last()?;
Some(glam::DVec3::new(point.x, point.y, point.z))
});
if let Some(new_landing) = new_landing {
let delta = new_landing - old_landing;
if delta.length_squared() > 1.0e-20 {
if self.is_recording_undo() {
if let Some(before) = self.document.get_entity_arc(annotation_handle) {
self.record_undo_before(annotation_handle, Some(before));
}
}
if let Some(annotation) = self.document.get_entity_mut(annotation_handle) {
view::dispatch::apply_transform(
annotation,
&crate::command::EntityTransform::Translate(delta),
);
}
if self.sync_displayed_annotation_context(annotation_handle) {
self.poison_undo_recording();
}
self.bump_entities(&[(
annotation_handle,
crate::scene::ChangeKind::Modified,
)]);
}
}
}
if self.sync_displayed_annotation_context(handle) {
self.poison_undo_recording();
}

View file

@ -692,7 +692,7 @@ fn mesh_click_result<'a>(
eye: glam::DVec3,
bounds: Rectangle,
) -> Option<(f64, Handle, glam::DVec3)> {
let profile = crate::perf::enabled().then(std::time::Instant::now);
let profile = crate::perf::enabled().then(iced::time::Instant::now);
let mut set_count = 0usize;
let mut bound_hits = 0usize;
let mut exact_triangles = 0usize;

View file

@ -16,9 +16,8 @@ impl DeviceCapabilities {
/// Indexed wires read one per-wire constants storage buffer.
const WIRE_STORAGE_BINDINGS: u32 = 1;
/// Batched hatch uses five storage bindings in one shader stage:
/// instances, boundaries, families, dashes, and visibility.
const HATCH_STORAGE_BINDINGS: u32 = 5;
/// Batched hatch stores instances, families, dashes, and visibility.
const HATCH_STORAGE_BINDINGS: u32 = 4;
pub fn detect(device: &wgpu::Device) -> Self {
Self::from_limits(&device.limits())

View file

@ -34,8 +34,8 @@ impl HatchBackendKind {
enum HatchBackend {
Storage {
resident: Option<StorageHatchBatch>,
preview: Option<StorageHatchBatch>,
resident: Vec<StorageHatchBatch>,
preview: Vec<StorageHatchBatch>,
},
Texture {
resident: Vec<TextureHatch>,
@ -137,8 +137,8 @@ impl HatchGpu {
});
let backend = match backend_kind {
HatchBackendKind::Storage => HatchBackend::Storage {
resident: None,
preview: None,
resident: Vec::new(),
preview: Vec::new(),
},
HatchBackendKind::Texture => HatchBackend::Texture {
resident: Vec::new(),
@ -223,28 +223,29 @@ impl HatchGpu {
queue: &wgpu::Queue,
mut is_visible: impl FnMut([f32; 4]) -> bool,
) -> usize {
let HatchBackend::Storage {
resident: Some(batch),
..
} = &mut self.backend
let HatchBackend::Storage { resident, .. } = &mut self.backend
else {
return 0;
};
for index in 0..batch.unique_source_count {
batch.source_visibility[index] = u32::from(is_visible(batch.source_aabbs[index]));
let mut updated = 0;
for batch in resident {
for index in 0..batch.unique_source_count {
batch.source_visibility[index] = u32::from(is_visible(batch.source_aabbs[index]));
}
for index in batch.unique_source_count..batch.source_visibility.len() {
batch.source_visibility[index] = 1;
}
for (index, aabb) in batch.instance_aabbs.iter().copied().enumerate() {
batch.placements[index].visible = if index == 0 && batch.unique_source_count > 0 {
1
} else {
u32::from(is_visible(aabb))
};
}
batch.upload_visibility(queue);
updated += batch.instance_aabbs.len() + batch.unique_source_count;
}
for index in batch.unique_source_count..batch.source_visibility.len() {
batch.source_visibility[index] = 1;
}
for (index, aabb) in batch.instance_aabbs.iter().copied().enumerate() {
batch.placements[index].visible = if index == 0 && batch.unique_source_count > 0 {
1
} else {
u32::from(is_visible(aabb))
};
}
batch.upload_visibility(queue);
batch.instance_aabbs.len() + batch.unique_source_count
updated
}
pub fn draw<'pass>(
@ -258,12 +259,13 @@ impl HatchGpu {
pass.set_stencil_reference(stencil_reference);
match &self.backend {
HatchBackend::Storage { resident, preview } => {
for batch in [resident.as_ref(), preview.as_ref()].into_iter().flatten() {
for batch in resident.iter().chain(preview) {
pass.set_bind_group(1, &batch.bind_group, &[]);
pass.set_vertex_buffer(0, batch.vertex_buffer.slice(..));
pass.set_vertex_buffer(1, batch.placement_buffer.slice(..));
for (vertices, instances) in &batch.draws {
pass.draw(vertices.clone(), instances.clone());
pass.set_index_buffer(batch.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
for (indices, instances) in &batch.draws {
pass.draw_indexed(indices.clone(), 0, instances.clone());
}
}
}
@ -272,7 +274,11 @@ impl HatchGpu {
pass.set_bind_group(1, &hatch.bind_group, &[]);
pass.set_vertex_buffer(0, hatch.vertex_buffer.slice(..));
pass.set_vertex_buffer(1, hatch.placement_buffer.slice(..));
pass.draw(0..6, 0..hatch.instance_count);
pass.set_index_buffer(
hatch.index_buffer.slice(..),
wgpu::IndexFormat::Uint32,
);
pass.draw_indexed(0..hatch.index_count, 0, 0..hatch.instance_count);
}
}
}
@ -294,11 +300,11 @@ mod tests {
#[test]
fn selects_backend_from_device_limits() {
assert_eq!(
HatchBackendKind::select(capabilities(5), false),
HatchBackendKind::select(capabilities(4), false),
HatchBackendKind::Storage
);
assert_eq!(
HatchBackendKind::select(capabilities(4), false),
HatchBackendKind::select(capabilities(3), false),
HatchBackendKind::Texture
);
}

View file

@ -1,48 +1,5 @@
// Hatch rendering — the single, canonical GPU hatch renderer. One draw
// call for all hatches, per-instance data fetched from storage buffers in
// the vertex shader. There is NO per-family cap here: every pattern line
// family is uploaded (`family_count` per instance) and the fragment shader
// loops over all of them, so complex baked patterns (ROOFING, GRAVEL,
// CAROLINA-LEDGESTONE with dozenshundreds of families) draw in full,
// matching the PDF export. Unavailable on devices that lack vertex-stage
// storage buffers. The old per-hatch renderer with the
// 16-family cap now lives in `wipeout_gpu.rs` and serves only wipeouts.
//
// Data layout — three storage buffers fed from `HatchModel`s:
//
// InstanceBuffer (binding 0) : HatchInstance[] (128 B each)
// color, color2, mode, gradient,
// pattern angle/scale, world_origin,
// boundary_offset, boundary_count,
// family_offset, family_count,
// dash_offset, dash_count, aabb,
// visibility flag (CPU writes; GPU
// skip)
// BoundaryBuffer (binding 1) : vec4<f32>[] (all boundary
// verts concatenated; NaN markers
// preserved as separators just like
// the per-hatch path)
// FamilyBuffer (binding 2) : LineFamilyGpu[] (all line
// families concatenated)
// DashBuffer (binding 3) : f32[] (all dash
// lengths concatenated)
//
// The vertex buffer holds each hatch's tessellated boundary triangles as
// per-vertex local-space positions (`local_xy`) plus a per-vertex
// `instance_index` (u32 attribute) — instance_index lets us avoid relying
// on `@builtin(instance_index)` for portability. An instance whose
// boundary failed to tessellate instead gets an AABB quad here, with
// `poly_test == 1` on its `HatchInstance` so the fragment shader still
// runs the `in_polygon` test to clip the quad to the real shape; on the
// tessellated fast path (`poly_test == 0`) the triangles already bound
// the fill and the fragment shader skips that test. When the visibility
// flag is 0, the vertex shader returns a degenerate position and the
// fragment shader runs zero times for that instance.
//
// Two storage usages — vertex shader reads InstanceBuffer + Boundary
// for the AABB / boundary range; fragment shader reads
// InstanceBuffer + Boundary + Family + Dash. Both stages share group
// 1 with `read_only` access.
// Batched hatch rendering. The kernel triangulates each boundary once; the
// fragment shader only evaluates the fill pattern inside that mesh.
use crate::scene::model::hatch_model::{HatchModel, HatchPattern, PatFamily};
use iced::wgpu;
@ -70,23 +27,16 @@ pub struct HatchInstance {
pub grad_range: f32, // 84
pub mode: u32, // 88 (0=pattern, 1=solid, 2=gradient)
pub visible: u32, // 92 (CPU sets to 0 to skip)
pub boundary_offset: u32, // 96 (first boundary vert index)
pub boundary_count: u32, // 100
pub family_offset: u32, // 104
pub family_count: u32, // 108
pub family_offset: u32, // 96
pub family_count: u32, // 100
/// Signed draw-order depth (-1,1); 0.0 = neutral. Applied as a clip-z
/// bias in the vertex shader so this fill orders against other types.
pub draw_depth: f32, // 112
/// 1 = run the per-fragment `in_polygon` boundary test (fallback path for
/// an instance whose boundary failed to tessellate); 0 = the rasterized
/// triangles already bound the fill, skip the test.
pub poly_test: u32, // 116
pub draw_depth: f32, // 104
/// Gradient shape (`GradientKind::shader_kind`), bit 4 = inverted stops.
pub grad_kind: u32, // 120
pub _pad2: u32, // 124
pub grad_kind: u32, // 108
}
const _: () = assert!(std::mem::size_of::<HatchInstance>() == 128);
const _: () = assert!(std::mem::size_of::<HatchInstance>() == 112);
/// Split a hatch's f64 world-origin anchor into double-single (high, low) f32
/// pairs so the GPU keeps sub-unit precision at UTM-scale coordinates.
@ -120,10 +70,7 @@ pub struct LineFamilyGpu {
const _: () = assert!(std::mem::size_of::<LineFamilyGpu>() == 48);
/// Per-vertex data — tessellated boundary triangles in local (world_origin-
/// relative) space, each carrying its instance index (avoids relying on
/// `@builtin(instance_index)` across backends). Instances whose boundary
/// failed to tessellate emit an AABB quad here instead (see `build`).
/// Kernel mesh vertex in local space with its source instance.
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub(super) struct HatchVertex {
@ -184,6 +131,7 @@ impl HatchPlacement {
/// hatch render pass entirely).
pub(super) struct StorageHatchBatch {
pub vertex_buffer: wgpu::Buffer,
pub index_buffer: wgpu::Buffer,
pub placement_buffer: wgpu::Buffer,
pub draws: Vec<(std::ops::Range<u32>, std::ops::Range<u32>)>,
// The four storage buffers below are referenced via `bind_group` —
@ -191,7 +139,6 @@ pub(super) struct StorageHatchBatch {
// only direct consumer. Keep them as fields to keep ownership in
// one place; `#[allow(dead_code)]` silences the read-never warning.
#[allow(dead_code)] pub instance_buffer: wgpu::Buffer,
#[allow(dead_code)] pub boundary_buffer: wgpu::Buffer,
#[allow(dead_code)] pub family_buffer: wgpu::Buffer,
#[allow(dead_code)] pub dash_buffer: wgpu::Buffer,
/// Per-instance visibility flag (1=draw, 0=skip). Stored in its
@ -205,8 +152,7 @@ pub(super) struct StorageHatchBatch {
#[allow(dead_code)] pub instance_count: u32,
/// CPU mirror — `update_visibility` re-uploads this whole slice
/// when any flag changes. ~4 B per hatch, so 40 KB / 10 k hatches
/// per pan tick. Far cheaper than touching the 128 B-per-instance
/// data.
/// per pan tick. Far cheaper than touching the instance data.
pub placements: Vec<HatchPlacement>,
pub source_visibility: Vec<u32>,
pub source_aabbs: Vec<[f32; 4]>,
@ -218,24 +164,76 @@ pub(super) struct StorageHatchBatch {
pub instance_aabbs: Vec<[f32; 4]>,
}
fn hatch_buffer_cost(hatch: &HatchModel) -> [u64; 7] {
let boundary = hatch.boundary.len() as u64;
let (families, dashes) = match &hatch.pattern {
HatchPattern::Pattern(families) => (
families.len() as u64,
families.iter().map(|family| family.dashes.len() as u64).sum(),
),
_ => (0, 0),
};
[
boundary.saturating_mul(std::mem::size_of::<HatchVertex>() as u64),
boundary.saturating_mul(24),
std::mem::size_of::<HatchInstance>() as u64,
families.saturating_mul(std::mem::size_of::<LineFamilyGpu>() as u64),
dashes.saturating_mul(std::mem::size_of::<f32>() as u64),
std::mem::size_of::<u32>() as u64,
std::mem::size_of::<HatchPlacement>() as u64,
]
}
impl StorageHatchBatch {
/// One-time build from the full hatch list. Re-uploaded only when
/// `geometry_epoch` advances (mirrors the existing per-hatch
/// upload trigger). Per-frame visibility flips go through
/// [`upload_visibility`].
/// Builds whole-hatch chunks within the device's buffer limits.
pub(super) fn build(
device: &wgpu::Device,
bgl: &wgpu::BindGroupLayout,
hatches: &[HatchModel],
) -> Option<Self> {
) -> Vec<Self> {
if hatches.is_empty() {
return None;
return Vec::new();
}
let limits = device.limits();
let chunk_limit = limits
.max_buffer_size
.min(limits.max_storage_buffer_binding_size as u64);
let mut ranges = Vec::new();
let mut start = 0;
let mut used = [0u64; 7];
for (index, hatch) in hatches.iter().enumerate() {
let cost = hatch_buffer_cost(hatch);
let overflow = used
.iter()
.zip(cost)
.any(|(used, cost)| used.saturating_add(cost) > chunk_limit);
if overflow && index > start {
ranges.push(start..index);
start = index;
used = [0; 7];
}
for (used, cost) in used.iter_mut().zip(cost) {
*used = used.saturating_add(cost);
}
}
ranges.push(start..hatches.len());
ranges
.into_iter()
.filter_map(|range| Self::build_chunk(device, bgl, &hatches[range]))
.collect()
}
fn build_chunk(
device: &wgpu::Device,
bgl: &wgpu::BindGroupLayout,
hatches: &[HatchModel],
) -> Option<Self> {
let mut instances: Vec<HatchInstance> = Vec::with_capacity(hatches.len());
let mut boundary: Vec<[f32; 4]> = Vec::new();
let mut families: Vec<LineFamilyGpu> = Vec::new();
let mut dashes: Vec<f32> = Vec::new();
let mut meshes = Vec::with_capacity(hatches.len());
let mut slots = rustc_hash::FxHashMap::default();
let mut groups: Vec<Vec<&HatchModel>> = Vec::new();
for (index, hatch) in hatches.iter().enumerate() {
@ -255,22 +253,9 @@ impl StorageHatchBatch {
for group in &groups {
let h = group[0];
let boundary_offset = boundary.len() as u32;
for &[x, y] in h.boundary.iter() {
// NaN sub-loop separators become the finite sentinel on the
// GPU: Intel drivers fold the shader NaN self-compare to
// `true`, turning separators into vertices and bleeding the
// fill outside its boundary (#386, #416).
if x.is_finite() && y.is_finite() {
boundary.push([x, y, 0.0, 0.0]);
} else {
let s = crate::scene::model::hatch_model::GPU_BOUNDARY_SEP;
boundary.push([s, s, 0.0, 0.0]);
}
}
let boundary_count = boundary.len() as u32 - boundary_offset;
let mesh = h.fill_mesh();
let has_mesh = !mesh.0.is_empty() && !mesh.1.is_empty();
meshes.push(mesh);
let family_offset = families.len() as u32;
let mut family_count = 0u32;
@ -301,11 +286,8 @@ impl StorageHatchBatch {
dashes.push(d);
}
let n_dashes = (dashes.len() as u32 - dash_offset).min(u32::MAX);
// QCAD PAT local-frame convention (mirrors
// the storage batch convention: `dy` is
// the perpendicular spacing, `dx` is the along-line
// phase shift — both in family-local coords. The
// shader applies cos_off/sin_off to rotate them.
// PAT local frame: perpendicular spacing and
// along-line phase.
let perp_step = fam.dy;
let along_step = fam.dx;
// Screen-space derivative drives 1-px line width
@ -366,39 +348,19 @@ impl StorageHatchBatch {
grad_range,
mode,
visible: 0,
boundary_offset,
boundary_count,
family_offset,
family_count,
draw_depth: if group.len() == 1 { h.draw_depth } else { 0.0 },
poly_test: 1,
grad_kind,
_pad2: 0,
});
continue;
}
// Pad the AABB so the quad covers any pattern halo + the
// family origin. Mirrors the per-hatch shader's quad sizing
// logic — `diag * 0.8 + max_spacing * 2 * scale`.
let diag = ((max_x - min_x).powi(2) + (max_y - min_y).powi(2)).sqrt();
// `perp_step.abs()` per family — uses the same QCAD local-
// frame convention as `LineFamilyGpu.perp_step` above so
// the quad padding matches what the shader will sample.
let max_spacing = match &h.pattern {
HatchPattern::Pattern(fs) => fs
.iter()
.map(|f| f.dy.abs())
.fold(0.0f32, f32::max),
_ => 5.0,
};
let pad = (diag * 0.8 + max_spacing * 2.0 * h.scale).max(1.0);
let (wo_hi, wo_lo) = split_origin_ds(h.world_origin);
instances.push(HatchInstance {
color: h.color,
color2,
aabb: [min_x - pad, min_y - pad, max_x + pad, max_y + pad],
aabb: [min_x, min_y, max_x, max_y],
world_origin: wo_hi,
world_origin_low: wo_lo,
angle_offset: h.angle_offset,
@ -408,26 +370,14 @@ impl StorageHatchBatch {
grad_min,
grad_range,
mode,
visible: 1,
boundary_offset,
boundary_count,
visible: u32::from(has_mesh),
family_offset,
family_count,
draw_depth: if group.len() == 1 { h.draw_depth } else { 0.0 },
// Always resolve inside/outside per fragment on the GPU (the
// boundary is uploaded below). Dropping CPU pre-triangulation
// keeps the vertex buffer at 6 verts/hatch regardless of
// boundary complexity, so a hatch-dense drawing can't overflow
// the device buffer limit. Each instance draws its AABB quad.
poly_test: 1,
grad_kind,
_pad2: 0,
});
}
// Empty fallbacks — storage buffers can't be zero-sized.
if boundary.is_empty() {
boundary.push([0.0; 4]);
}
if families.is_empty() {
families.push(LineFamilyGpu::default_filler());
}
@ -435,21 +385,33 @@ impl StorageHatchBatch {
dashes.push(0.0);
}
// Vertex buffer — one AABB quad (two triangles, BL,BR,TL, BR,TR,TL) per
// instance; the GPU shader clips each fragment to the boundary via the
// in_polygon test (poly_test == 1). 6 verts/hatch, independent of
// boundary complexity, so the buffer can never overflow the device
// limit no matter how dense the drawing.
let mut verts: Vec<HatchVertex> = Vec::with_capacity(instances.len() * 6);
for (i, inst) in instances.iter().enumerate() {
let [xmin, ymin, xmax, ymax] = inst.aabb;
let quad = [
[xmin, ymin], [xmax, ymin], [xmin, ymax],
[xmax, ymin], [xmax, ymax], [xmin, ymax],
];
for c in quad {
verts.push(HatchVertex { local_xy: c, instance_index: i as u32 });
let mut verts = Vec::new();
let mut indices = Vec::new();
let mut mesh_ranges = Vec::with_capacity(meshes.len());
for (instance_index, (points, mesh_indices)) in meshes.into_iter().enumerate() {
let Ok(base) = u32::try_from(verts.len()) else {
return None;
};
let Ok(start) = u32::try_from(indices.len()) else {
return None;
};
verts.extend(points.into_iter().map(|local_xy| HatchVertex {
local_xy,
instance_index: instance_index as u32,
}));
for index in mesh_indices {
let Some(index) = base.checked_add(index) else {
return None;
};
indices.push(index);
}
let Ok(end) = u32::try_from(indices.len()) else {
return None;
};
mesh_ranges.push(start..end);
}
if indices.is_empty() {
return None;
}
let mut placements = Vec::new();
let mut draws = Vec::with_capacity(groups.len());
@ -461,7 +423,10 @@ impl StorageHatchBatch {
draw_depth: 0.0,
visible: 1,
});
draws.push((0..unique_source_count as u32 * 6, 0..1));
let end = mesh_ranges[unique_source_count - 1].end;
if end > 0 {
draws.push((0..end, 0..1));
}
let mut union = [f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY];
for inst in instances.iter().take(unique_source_count) {
union[0] = union[0].min(inst.aabb[0] + inst.world_origin[0]);
@ -500,10 +465,32 @@ impl StorageHatchBatch {
inst.aabb[3] + inst.world_origin[1] + high[1],
]);
}
draws.push((
source_index as u32 * 6..source_index as u32 * 6 + 6,
placement_start..placements.len() as u32,
));
if !mesh_ranges[source_index].is_empty() {
draws.push((
mesh_ranges[source_index].clone(),
placement_start..placements.len() as u32,
));
}
}
let visibility: Vec<u32> = instances.iter().map(|instance| instance.visible).collect();
let limits = device.limits();
let buffer_fits = |count: usize, stride: usize| {
(count as u64).saturating_mul(stride as u64) <= limits.max_buffer_size
};
let storage_fits = |count: usize, stride: usize| {
(count as u64).saturating_mul(stride as u64)
<= limits.max_storage_buffer_binding_size as u64
};
if !buffer_fits(verts.len(), std::mem::size_of::<HatchVertex>())
|| !buffer_fits(indices.len(), std::mem::size_of::<u32>())
|| !buffer_fits(placements.len(), std::mem::size_of::<HatchPlacement>())
|| !storage_fits(instances.len(), std::mem::size_of::<HatchInstance>())
|| !storage_fits(families.len(), std::mem::size_of::<LineFamilyGpu>())
|| !storage_fits(dashes.len(), std::mem::size_of::<f32>())
|| !storage_fits(visibility.len(), std::mem::size_of::<u32>())
{
return None;
}
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
@ -511,6 +498,11 @@ impl StorageHatchBatch {
contents: bytemuck::cast_slice(&verts),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("hatch.index"),
contents: bytemuck::cast_slice(&indices),
usage: wgpu::BufferUsages::INDEX,
});
let placement_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("hatch.placements"),
contents: bytemuck::cast_slice(&placements),
@ -521,11 +513,6 @@ impl StorageHatchBatch {
contents: bytemuck::cast_slice(&instances),
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
});
let boundary_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("hatch.boundary"),
contents: bytemuck::cast_slice(&boundary),
usage: wgpu::BufferUsages::STORAGE,
});
let family_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("hatch.families"),
contents: bytemuck::cast_slice(&families),
@ -537,7 +524,6 @@ impl StorageHatchBatch {
usage: wgpu::BufferUsages::STORAGE,
});
let visibility: Vec<u32> = instances.iter().map(|i| i.visible).collect();
let visibility_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("hatch.visibility"),
contents: bytemuck::cast_slice(&visibility),
@ -554,18 +540,14 @@ impl StorageHatchBatch {
},
wgpu::BindGroupEntry {
binding: 1,
resource: boundary_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: family_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
binding: 2,
resource: dash_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 4,
binding: 3,
resource: visibility_buffer.as_entire_binding(),
},
],
@ -582,10 +564,10 @@ impl StorageHatchBatch {
Some(Self {
vertex_buffer,
index_buffer,
placement_buffer,
draws,
instance_buffer,
boundary_buffer,
family_buffer,
dash_buffer,
visibility_buffer,
@ -614,10 +596,7 @@ impl StorageHatchBatch {
);
}
/// Group-1 bind group layout — shared by the pipeline so it can be
/// constructed once at startup. All four bindings are read-only
/// storage and visible to both VS (AABB+visibility lookup) and FS
/// (boundary / family / dash sampling).
/// Shared layout for instance, family, dash, and visibility storage.
#[allow(dead_code)]
pub(super) fn bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
let entry = |binding: u32| wgpu::BindGroupLayoutEntry {
@ -632,7 +611,7 @@ impl StorageHatchBatch {
};
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("hatch.bgl"),
entries: &[entry(0), entry(1), entry(2), entry(3), entry(4)],
entries: &[entry(0), entry(1), entry(2), entry(3)],
})
}
}

View file

@ -1,14 +1,5 @@
// Storage-free hatch renderer — texture-backed, UNCAPPED.
//
// Devices without storage buffers use this per-hatch renderer. It reuses the
// storage-free hatch algorithm (see wipeout.wgsl / hatch_texture.wgsl) but packs the
// variable-length boundary / family / dash arrays into ONE RGBA32F data texture
// read via textureLoad — removing the MAX_FAMILIES / MAX_HATCH_BOUNDARY_VERTS /
// MAX_DASHES caps of the uniform (WipeoutGpu) path. Every hatch type — solid,
// gradient, and arbitrarily complex line patterns — renders in compat mode.
//
// Storage-capable devices use the sibling storage backend; wipeout masks use
// wipeout_gpu.rs.
// Texture-backed hatch renderer for devices without storage buffers. Boundary
// geometry comes from the same kernel mesh as the storage backend.
use crate::scene::model::hatch_model::{HatchModel, HatchPattern};
use iced::wgpu;
@ -71,7 +62,7 @@ struct TextureHatchUniform {
color: [f32; 4], // 0
color2: [f32; 4], // 16
mode: u32, // 32
vcount: u32, // 36
_reserved: u32, // 36
angle_offset: f32, // 40
scale: f32, // 44
grad_cos: f32, // 48
@ -90,7 +81,9 @@ struct TextureHatchUniform {
pub(super) struct TextureHatch {
pub(super) vertex_buffer: wgpu::Buffer,
pub(super) index_buffer: wgpu::Buffer,
pub(super) placement_buffer: wgpu::Buffer,
pub(super) index_count: u32,
pub(super) instance_count: u32,
pub(super) bind_group: wgpu::BindGroup,
/// Reserved for per-frame AABB LOD (mirrors `WipeoutGpu`); not yet wired
@ -125,7 +118,7 @@ impl TextureHatch {
}
groups
.into_iter()
.map(|group| Self::new(device, queue, &group, bgl1))
.filter_map(|group| Self::new(device, queue, &group, bgl1))
.collect()
}
@ -164,7 +157,7 @@ impl TextureHatch {
queue: &wgpu::Queue,
models: &[&HatchModel],
bgl1: &wgpu::BindGroupLayout,
) -> Self {
) -> Option<Self> {
let model = models[0];
// ── Decode pattern mode (mirrors WipeoutGpu::new) ─────────────────
// The gradient shape (kind + invert bit) rides in the mode's high
@ -197,41 +190,26 @@ impl TextureHatch {
max_y = max_y.max(y);
}
let max_spacing = match &model.pattern {
HatchPattern::Pattern(families) => {
families.iter().map(|f| f.dy.abs()).fold(0.0f32, f32::max)
}
_ => 5.0,
};
let diag = ((max_x - min_x).powi(2) + (max_y - min_y).powi(2)).sqrt();
let pad = (diag * 0.8 + max_spacing * 2.0 * model.scale).max(1.0);
// Anchor pattern phase at `world_origin` with the boundary stored raw,
// matching the storage backend — NOT WipeoutGpu,
// whose f64 origin grid-snap is dead code (wipeouts are always solid)
// and would phase-shift every line pattern relative to desktop. No drift.
let origin = model.world_origin;
let drift = [0.0f32, 0.0f32];
let (x0, x1, y0, y1) = (
min_x + drift[0] - pad,
max_x + drift[0] + pad,
min_y + drift[1] - pad,
max_y + drift[1] + pad,
);
let quad = [
HatchVertex { pos: [x0, y0, 0.0], _pad: 0.0 },
HatchVertex { pos: [x1, y0, 0.0], _pad: 0.0 },
HatchVertex { pos: [x1, y1, 0.0], _pad: 0.0 },
HatchVertex { pos: [x0, y0, 0.0], _pad: 0.0 },
HatchVertex { pos: [x1, y1, 0.0], _pad: 0.0 },
HatchVertex { pos: [x0, y1, 0.0], _pad: 0.0 },
];
let (mesh_points, indices) = model.fill_mesh();
if mesh_points.is_empty() || indices.is_empty() {
return None;
}
let vertices: Vec<HatchVertex> = mesh_points
.into_iter()
.map(|[x, y]| HatchVertex { pos: [x, y, 0.0], _pad: 0.0 })
.collect();
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("hatch.texture.vbuf"),
contents: bytemuck::cast_slice(&quad),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("hatch.texture.ibuf"),
contents: bytemuck::cast_slice(&indices),
usage: wgpu::BufferUsages::INDEX,
});
let base = model
.render_instance
.map_or([0.0; 3], |instance| instance.translation);
@ -290,21 +268,8 @@ impl TextureHatch {
(0.0, 1.0)
};
// ── Pack the data texture: boundary | families | dashes ───────────
// ── Pack the data texture: families | dashes ─────────────────────
let mut texels: Vec<[f32; 4]> = Vec::new();
// Boundary section (texels 0..vcount). NaN separators become the
// finite GPU sentinel — the shader NaN self-compare is folded to
// `true` on some drivers (#386, #416).
for &[x, y] in model.boundary.iter() {
if x.is_finite() && y.is_finite() {
texels.push([x + drift[0], y + drift[1], 0.0, 0.0]);
} else {
let s = crate::scene::model::hatch_model::GPU_BOUNDARY_SEP;
texels.push([s, s, 0.0, 0.0]);
}
}
let vcount = texels.len() as u32;
// Family section (3 texels each) + a flat dash pool.
let fam_off = texels.len() as u32;
let mut n_families = 0u32;
@ -320,7 +285,7 @@ impl TextureHatch {
0.0
};
let angle_r = fam.angle_deg.to_radians();
// QCAD PAT convention: perp_step = dy, along_step = dx.
// PAT local frame: perpendicular spacing and along-line phase.
texels.push([angle_r.cos(), angle_r.sin(), fam.x0, fam.y0]);
texels.push([fam.dx, fam.dy, fam.dy, fam.dx]);
// Counts as exact f32 (small integers → no denormal/bitcast risk).
@ -385,7 +350,7 @@ impl TextureHatch {
color: model.color,
color2,
mode,
vcount,
_reserved: 0,
angle_offset: model.angle_offset,
// Clamp like the desktop renderer so scale==0 can't make perp_step 0
// → round(perp/0)=NaN → an invisible hatch.
@ -433,14 +398,16 @@ impl TextureHatch {
[min_x, min_y, max_x, max_y]
};
Self {
Some(Self {
vertex_buffer,
index_buffer,
placement_buffer,
index_count: indices.len() as u32,
instance_count: placements.len() as u32,
bind_group,
world_aabb,
_uniform_buf,
_data_tex: data_tex,
}
})
}
}

View file

@ -1148,7 +1148,7 @@ fn build_instanced_chunk(
parts: &[MeshBatchPart<'_>],
profile_enabled: bool,
) -> Option<(MeshBatchChunk, u64, InstancedBuildProfile)> {
let started = profile_enabled.then(std::time::Instant::now);
let started = profile_enabled.then(iced::time::Instant::now);
let first = parts.first()?;
let source = first.set.instance_source.as_ref()?;
let mesh = first.mesh;
@ -1209,7 +1209,7 @@ fn build_instanced_chunk(
} else {
Vec::new()
};
let vertices_at = profile_enabled.then(std::time::Instant::now);
let vertices_at = profile_enabled.then(iced::time::Instant::now);
let edge_color = first
.set
.visual_style
@ -1250,7 +1250,7 @@ fn build_instanced_chunk(
]);
}
}
let edges_at = profile_enabled.then(std::time::Instant::now);
let edges_at = profile_enabled.then(iced::time::Instant::now);
let transparent = material_is_transparent(material, color);
let face_indices = first.include_faces.then_some(first.indices.as_ref());
let opaque_indices = if transparent {
@ -1298,7 +1298,7 @@ fn build_instanced_chunk(
}
}
}
let instances_at = profile_enabled.then(std::time::Instant::now);
let instances_at = profile_enabled.then(iced::time::Instant::now);
let triangles = if first.include_faces {
(first.indices.len() / 3) as u64 * instances.len() as u64
} else {
@ -1332,7 +1332,7 @@ fn build_instanced_chunk(
.entry(edge_key)
.or_insert_with(|| chunk.edge_vertex_buffer.clone());
}
let uploaded_at = profile_enabled.then(std::time::Instant::now);
let uploaded_at = profile_enabled.then(iced::time::Instant::now);
Some((
chunk,
triangles,
@ -1559,7 +1559,7 @@ pub fn build_mesh_batch_filtered(
sets: &[MeshLodSet],
handles: Option<&rustc_hash::FxHashSet<acadrust::Handle>>,
) -> (Vec<MeshBatchChunk>, u64) {
let perf_started = crate::perf::enabled().then(std::time::Instant::now);
let perf_started = crate::perf::enabled().then(iced::time::Instant::now);
// Derive the caps from the real device limit and vertex size. The previous
// fixed 6 M-vertex cap assumed 40 B/vertex, but `position_low` (RTE) grew
// MeshVertex to 52 B, so 6 M × 52 B = 312 MB blew past the 256 MB cap.
@ -1741,7 +1741,7 @@ pub fn build_mesh_batch_filtered(
});
}
}
let prepared_at = std::time::Instant::now();
let prepared_at = iced::time::Instant::now();
let spatial_bounds = sets.iter().fold(
[
f32::INFINITY,
@ -1828,7 +1828,7 @@ pub fn build_mesh_batch_filtered(
mesh_spatial_key(part.set, spatial_bounds),
)
});
let grouped_at = std::time::Instant::now();
let grouped_at = iced::time::Instant::now();
let ordered = direct_parts;
let mut active_key: Option<MaterialBatchKey> = None;
let mut active_material: Option<&crate::scene::model::material_model::MeshMaterial> = None;
@ -2224,7 +2224,7 @@ pub fn build_mesh_batch_filtered(
None,
));
}
let direct_at = std::time::Instant::now();
let direct_at = iced::time::Instant::now();
let mut instanced_profile = InstancedBuildProfile::default();
let mut instanced_buffers = InstancedBufferCache::default();
for parts in instance_groups.values() {
@ -2249,7 +2249,7 @@ pub fn build_mesh_batch_filtered(
}
}
if let Some(started) = perf_started {
let done = std::time::Instant::now();
let done = iced::time::Instant::now();
crate::perf_record!(
"[perf] mesh-build-detail prepare={:.1} group={:.1} direct={:.1} instanced={:.1}",
prepared_at.duration_since(started).as_secs_f64() * 1000.0,

View file

@ -29,6 +29,10 @@ impl Scene {
self.preview_hatches = std::sync::Arc::new(models);
}
pub fn set_command_preview_hatches(&mut self, models: Vec<HatchModel>) {
self.preview_hatches = std::sync::Arc::new(models);
}
pub fn set_area_preview_regions(&mut self, regions: &[AreaPreviewRegion]) {
let mut models = Vec::new();
for region in regions {
@ -154,6 +158,8 @@ impl Scene {
world_origin: origin,
boundary: std::sync::Arc::new(boundary),
boundary_wcs: None,
boundary_exterior: None,
boundary_sources: None,
pattern: HatchPattern::Solid,
name: "AREA_PREVIEW".into(),
color: [0.0; 4],

View file

@ -3,12 +3,46 @@ use super::*;
impl Scene {
// ── Selection ─────────────────────────────────────────────────────────
/// Treat a classic LEADER and its attached annotation as one logical object.
/// Clicking/copying/deleting either side expands to the complete pair.
pub(crate) fn handles_expanded_for_leader_annotations(
&self,
handles: &[Handle],
) -> Vec<Handle> {
let mut expanded = handles.to_vec();
for &handle in handles {
// LEADER -> annotation.
if let Some(EntityType::Leader(leader)) = self.document.get_entity(handle) {
if !leader.annotation_handle.is_null() {
expanded.push(leader.annotation_handle);
}
}
// Annotation -> LEADER.
expanded.extend(self.document.entities().filter_map(|entity| match entity {
EntityType::Leader(leader)
if !leader.annotation_handle.is_null()
&& leader.annotation_handle == handle =>
{
Some(entity.common().handle)
}
_ => None,
}));
}
expanded.sort_unstable_by_key(|handle| handle.value());
expanded.dedup();
expanded
}
pub fn select_entity(&mut self, handle: Handle, exclusive: bool) {
let handles = self.handles_expanded_for_leader_annotations(&[handle]);
if exclusive {
self.selected.clear();
}
self.selected.insert(handle);
self.selected.extend(handles);
self.bump_selection();
}
@ -21,6 +55,12 @@ impl Scene {
/// only when its contents actually changed. History/file/command paths must
/// use this instead of assigning `selected` directly.
pub(crate) fn replace_selection(&mut self, selected: HashSet<Handle>) {
let handles: Vec<Handle> = selected.iter().copied().collect();
let selected: HashSet<Handle> = self
.handles_expanded_for_leader_annotations(&handles)
.into_iter()
.collect();
if self.selected != selected {
self.selected = selected;
self.bump_selection();
@ -29,7 +69,14 @@ impl Scene {
/// Remove a single entity from the selection (Shift+click subtractive pick).
pub fn deselect_entity(&mut self, handle: Handle) {
if self.selected.remove(&handle) {
let handles = self.handles_expanded_for_leader_annotations(&[handle]);
let mut changed = false;
for handle in handles {
changed |= self.selected.remove(&handle);
}
if changed {
self.bump_selection();
}
}
@ -608,10 +655,14 @@ impl Scene {
// ── Erase ─────────────────────────────────────────────────────────────
pub fn erase_entities(&mut self, handles: &[Handle]) {
let erase_handles = self.handles_expanded_for_leader_annotations(handles);
let mut handle_set: HashSet<Handle> = HashSet::default();
let mut erased: Vec<(Handle, ChangeKind)> = Vec::new();
let mut highlight_changed = false;
for &h in handles {
for &h in &erase_handles {
// Objects on a locked layer can't be erased.
if self.is_layer_locked(h) {
continue;

View file

@ -17,11 +17,12 @@ use std::sync::Arc;
/// A script we ship a Noto subset for. [`script_of`] maps a char to one.
///
/// CJK is split by language — Chinese, Japanese and Korean each get their own
/// file. Their ideographs (Han, U+4E009FFF) share the same code points but
/// differ in glyph shape, so the shared block is routed by the document's
/// language (see [`set_cjk_lang_from_codepage`]); kana is always Japanese and
/// Hangul always Korean.
/// CJK is split by language — Simplified Chinese, Traditional Chinese,
/// Japanese and Korean each get their own file. Their ideographs (Han,
/// U+4E009FFF) share the same code points but differ in glyph shape, so the
/// shared block is routed by the document's language (see
/// [`set_cjk_lang_from_codepage`]); kana is always Japanese and Hangul always
/// Korean.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Script {
Latin,
@ -32,6 +33,7 @@ pub enum Script {
Thai,
Devanagari,
Chinese,
TraditionalChinese,
Japanese,
Korean,
}
@ -49,6 +51,7 @@ impl Script {
Script::Thai => "fonts/thai.ttf",
Script::Devanagari => "fonts/devanagari.ttf",
Script::Chinese => "fonts/chinese.ttf",
Script::TraditionalChinese => "fonts/traditional_chinese.ttf",
Script::Japanese => "fonts/japanese.ttf",
Script::Korean => "fonts/korean.ttf",
}
@ -57,6 +60,7 @@ impl Script {
pub fn family(self) -> &'static str {
match self {
Script::Chinese => "Noto Sans CJK SC",
Script::TraditionalChinese => "Noto Sans CJK TC",
Script::Japanese => "Noto Sans CJK JP",
Script::Korean => "Noto Sans CJK KR",
Script::Latin
@ -80,6 +84,7 @@ impl Script {
7 => Script::Chinese,
8 => Script::Japanese,
9 => Script::Korean,
10 => Script::TraditionalChinese,
_ => Script::Latin,
}
}
@ -97,12 +102,13 @@ impl Script {
Script::Chinese => 7,
Script::Japanese => 8,
Script::Korean => 9,
Script::TraditionalChinese => 10,
}
}
}
/// Language used to render shared Han ideographs: 0 = Chinese, 1 = Japanese,
/// 2 = Korean. Set from the active document's code page.
/// Language used to render shared Han ideographs: 0 = Simplified Chinese,
/// 1 = Japanese, 2 = Korean, 3 = Traditional Chinese.
static CJK_LANG: AtomicU8 = AtomicU8::new(0);
static PRIMARY_SCRIPT: AtomicU8 = AtomicU8::new(0);
static FONT_GENERATION: AtomicU64 = AtomicU64::new(0);
@ -117,22 +123,29 @@ fn bump_generation() {
#[cfg(target_arch = "wasm32")]
pub fn scripts_for_language_tag(language: &str) -> Vec<Script> {
let language = language
let full_language = language.to_ascii_lowercase().replace('_', "-");
let language = full_language
.split(['-', '_'])
.next()
.unwrap_or(language)
.unwrap_or(&full_language)
.to_ascii_lowercase();
let script = match language.as_str() {
"ar" | "fa" | "ur" => Some(Script::Arabic),
"el" => Some(Script::Greek),
"he" | "yi" => Some(Script::Hebrew),
"hi" | "mr" | "ne" => Some(Script::Devanagari),
"ja" => Some(Script::Japanese),
"ko" => Some(Script::Korean),
"ru" | "uk" | "bg" | "sr" => Some(Script::Cyrillic),
"th" => Some(Script::Thai),
"zh" => Some(Script::Chinese),
_ => None,
let script = if matches!(full_language.as_str(), "zh-tw" | "zh-hk")
|| full_language.starts_with("zh-hant")
{
Some(Script::TraditionalChinese)
} else {
match language.as_str() {
"ar" | "fa" | "ur" => Some(Script::Arabic),
"el" => Some(Script::Greek),
"he" | "yi" => Some(Script::Hebrew),
"hi" | "mr" | "ne" => Some(Script::Devanagari),
"ja" => Some(Script::Japanese),
"ko" => Some(Script::Korean),
"ru" | "uk" | "bg" | "sr" => Some(Script::Cyrillic),
"th" => Some(Script::Thai),
"zh" => Some(Script::Chinese),
_ => None,
}
};
vec![script.unwrap_or(Script::Latin)]
}
@ -184,22 +197,24 @@ fn cjk_lang() -> Script {
match CJK_LANG.load(Ordering::Relaxed) {
1 => Script::Japanese,
2 => Script::Korean,
3 => Script::TraditionalChinese,
_ => Script::Chinese,
}
}
/// Point the shared-Han routing at a language based on a DWG/DXF code page
/// (`$DWGCODEPAGE`), e.g. `ANSI_932` → Japanese, `ANSI_949` → Korean, GB/936 or
/// anything else → Chinese. Returns `true` if the language changed (the caller
/// then clears the glyph cache and re-tessellates).
/// (`$DWGCODEPAGE`), e.g. `ANSI_932` → Japanese, `ANSI_949` → Korean and
/// `ANSI_950` → Traditional Chinese. Returns `true` if the language changed.
pub fn set_cjk_lang_from_codepage(code_page: &str) -> bool {
let c = code_page.to_ascii_uppercase();
let lang = if c.contains("932") || c.contains("SJIS") || c.contains("SHIFT") {
1 // Japanese (Shift-JIS)
} else if c.contains("949") || c.contains("KOR") || c.contains("UHC") {
2 // Korean
} else if c.contains("950") || c.contains("BIG5") {
3 // Traditional Chinese
} else {
0 // Chinese (936 / GB / 950 / Big5) or non-CJK default
0 // Simplified Chinese (936 / GB) or non-CJK default
};
CJK_LANG.swap(lang, Ordering::Relaxed) != lang
}

View file

@ -3112,7 +3112,7 @@ impl Scene {
}
let context_scale = match self.document.objects.get(&scale) {
Some(acadrust::objects::ObjectType::Scale(value)) => {
value.inverse_factor() as f32
(value.inverse_factor() / self.annotation_scale_unit_factor()) as f32
}
_ => annotation_scale,
};

View file

@ -1,23 +1,10 @@
// Phase 4-B batched hatch shader. All hatches in one draw call;
// per-instance data fetched from storage buffers indexed by the
// `instance_index` vertex attribute (passed from the per-vertex
// (local_xy, instance_index) stream of tessellated boundary-triangle
// positions so we don't depend on @builtin(instance_index) edge cases
// across backends).
// Batched hatch shader. Kernel mesh vertices carry their source instance.
//
// Layout matches `hatch_gpu/storage.rs`:
// group 1 binding 0 InstanceBuffer HatchInstance[] (128 B / inst)
// group 1 binding 1 BoundaryBuffer vec4<f32>[] (xy in .xy)
// group 1 binding 2 FamilyBuffer LineFamilyGpu[] (48 B / fam)
// group 1 binding 3 DashBuffer f32[]
//
// The vertex shader emits tessellated boundary triangles in local space
// (an instance whose boundary failed to tessellate falls back to an
// AABB quad with `poly_test == 1`, so the fragment shader still runs
// `in_polygon` to clip it to the real shape). A `visible == 0` instance
// gets an out-of-NDC clip position so the fragment shader never runs
// for it that's the GPU-side cull (Phase 4-B equivalent of
// `compute_hatch_lod` writing `hatch_skip_flags`).
// group 1 binding 0 InstanceBuffer HatchInstance[]
// group 1 binding 1 FamilyBuffer LineFamilyGpu[]
// group 1 binding 2 DashBuffer f32[]
// group 1 binding 3 Visibility u32[]
// Group 0: shared frame uniforms (matches hatch.wgsl)
@ -53,14 +40,10 @@ struct HatchInstance {
grad_range: f32,
mode: u32, // 0=pattern, 1=solid, 2=gradient
visible: u32, // 0 = skip (CPU writes via compute_hatch_lod)
boundary_offset: u32,
boundary_count: u32,
family_offset: u32,
family_count: u32,
draw_depth: f32, // signed (-1,1) draw-order bias; 0 = neutral
poly_test: u32, // 1 = run in_polygon (fallback), 0 = skip
grad_kind: u32, // shape (0=linear,1=cyl,2=sph,3=hemi,4=curved), bit4=invert
_pad2: u32,
}
// Draw-order depth bias (see wire.wgsl). Higher draw_depth smaller z
@ -83,14 +66,13 @@ struct LineFamily {
}
@group(1) @binding(0) var<storage, read> instances: array<HatchInstance>;
@group(1) @binding(1) var<storage, read> boundary: array<vec4<f32>>;
@group(1) @binding(2) var<storage, read> families: array<LineFamily>;
@group(1) @binding(3) var<storage, read> dashes: array<f32>;
@group(1) @binding(1) var<storage, read> families: array<LineFamily>;
@group(1) @binding(2) var<storage, read> dashes: array<f32>;
// Per-instance visibility (Phase 4-B sub-pixel + frustum skip).
// CPU writes `1` to draw / `0` to skip every frame; vertex shader
// emits an out-of-NDC clip position for 0-instances so the GPU
// rasterizer culls the primitive before any fragment runs.
@group(1) @binding(4) var<storage, read> visibility: array<u32>;
@group(1) @binding(3) var<storage, read> visibility: array<u32>;
// Vertex shader
@ -144,58 +126,6 @@ struct VOut {
return o;
}
// Point-in-polygon (ray casting) over a sub-range of BoundaryBuffer
fn valid_vertex(p: vec2<f32>) -> bool {
// Sub-loop separators are a huge finite sentinel (1e30, see
// GPU_BOUNDARY_SEP), not NaN: `x == x` NaN detection gets folded to
// `true` by some drivers (Intel Mesa fast math), which made separators
// read as real vertices and bleed fills past their boundary (#386, #416).
return abs(p.x) < 1.0e29 && abs(p.y) < 1.0e29;
}
fn edge_crosses(p: vec2<f32>, a: vec2<f32>, c: vec2<f32>) -> bool {
if (a.y > p.y) != (c.y > p.y) {
let x_int = (c.x - a.x) * (p.y - a.y) / (c.y - a.y) + a.x;
return p.x < x_int;
}
return false;
}
fn in_polygon(p: vec2<f32>, offset: u32, count: u32) -> bool {
var inside = false;
var prev = vec2<f32>(0.0, 0.0);
var first = vec2<f32>(0.0, 0.0);
var have_prev = false;
for (var i = 0u; i < count; i++) {
let vi = boundary[offset + i].xy;
if !valid_vertex(vi) {
// Close the sub-loop that just ended (last first edge). An
// unclosed boundary e.g. a SOLID's 4 corners, which are not
// repeated otherwise miscounts crossings and the fill bleeds
// outside the shape. (#140)
if have_prev && edge_crosses(p, prev, first) {
inside = !inside;
}
have_prev = false;
continue;
}
if have_prev {
if edge_crosses(p, prev, vi) {
inside = !inside;
}
} else {
first = vi;
}
prev = vi;
have_prev = true;
}
if have_prev && edge_crosses(p, prev, first) {
inside = !inside;
}
return inside;
}
// Per-family hatch test (same math as hatch.wgsl, dashes from
// global DashBuffer instead of per-hatch FamilyBatch)
@ -282,15 +212,7 @@ fn check_family(
let inst = instances[v.instance_index];
// 1. Boundary test only on the fallback path (poly_test==1). On the
// tessellated fast path the triangles already bound the fill.
if inst.poly_test == 1u {
if !in_polygon(v.xz, inst.boundary_offset, inst.boundary_count) {
discard;
}
}
// 2. Mode dispatch.
// Mode dispatch.
if inst.mode == 1u {
return inst.color;
} else if inst.mode == 2u {
@ -319,12 +241,11 @@ fn check_family(
t = 1.0 - t;
}
// Radial stops run OUTSIDE-IN: colour 1 at the rim, colour 2 at the
// centre (AutoCAD's SPHERICAL/HEMISPHERICAL convention; the INV bit
// above swaps them back).
// centre; the INV bit above swaps them back.
return mix(inst.color2, inst.color, t);
}
// 3. Pattern LOD. Keep every family visible until all family spacings
// Pattern LOD. Keep every family visible until all family spacings
// project below 2 px, then substitute one solid fill. A single dense
// family must neither hide itself nor turn a complex hatch solid.
var all_families_subpixel = inst.family_count > 0u && u.world_per_pixel > 0.0;
@ -343,7 +264,7 @@ fn check_family(
return inst.color;
}
// 4. Pattern evaluation.
// Pattern evaluation.
let cos_off = cos(inst.angle_offset);
let sin_off = sin(inst.angle_offset);
for (var i = 0u; i < inst.family_count; i++) {

View file

@ -1,15 +1,7 @@
// Hatch texture shader storage-free, UNCAPPED variant of wipeout.wgsl.
//
// Devices without vertex/fragment storage buffers cannot use the batched
// storage-buffer renderer (hatch.wgsl). This shader keeps the exact
// wipeout.wgsl hatch algorithm (in_polygon + per-family line/dash/dot + solid +
// gradient) but reads the variable-length boundary / family / dash arrays from a
// single RGBA32F data texture via textureLoad instead of fixed-size uniforms
// removing the MAX_FAMILIES (16) / MAX_HATCH_BOUNDARY_VERTS (1024) / MAX_DASHES
// (128) caps of the uniform path.
// Texture-backed hatch shader. The kernel mesh bounds the fill; this shader
// evaluates only the pattern, solid, or gradient colour.
//
// Data texture layout (row-major, width = h.tex_width, one vec4 per texel):
// [ 0 .. vcount ) boundary verts, .xy = local XY, NaN = break
// [ fam_off .. fam_off+3*n_fam ) families, 3 texels each (see load_family)
// [ dash_off .. ) dash values, 4 per texel (RGBA)
@ -36,7 +28,7 @@ struct HatchUniforms {
color: vec4<f32>, // 0
color2: vec4<f32>, // 16
mode: u32, // 32: 0=pattern, 1=solid, 2=gradient
vcount: u32, // 36: boundary vertex count
_reserved: u32, // 36
angle_offset: f32, // 40
scale: f32, // 44
grad_cos: f32, // 48
@ -119,56 +111,6 @@ struct VOut {
return o;
}
// Point-in-polygon (ray casting)
fn valid_vertex(p: vec2<f32>) -> bool {
// Sub-loop separators are a huge finite sentinel (1e30, see
// GPU_BOUNDARY_SEP), not NaN: `x == x` NaN detection gets folded to
// `true` by some drivers (Intel Mesa fast math), which made separators
// read as real vertices and bleed fills past their boundary (#386, #416).
return abs(p.x) < 1.0e29 && abs(p.y) < 1.0e29;
}
fn edge_crosses(p: vec2<f32>, a: vec2<f32>, c: vec2<f32>) -> bool {
if (a.y > p.y) != (c.y > p.y) {
let x_int = (c.x - a.x) * (p.y - a.y) / (c.y - a.y) + a.x;
return p.x < x_int;
}
return false;
}
fn in_polygon(p: vec2<f32>) -> bool {
var inside = false;
let n = h.vcount;
var prev = vec2<f32>(0.0, 0.0);
var first = vec2<f32>(0.0, 0.0);
var have_prev = false;
for (var i = 0u; i < n; i++) {
let vi = texel(i).xy;
if !valid_vertex(vi) {
// NaN sentinel closes the current sub-loop (last first edge). (#140)
if have_prev && edge_crosses(p, prev, first) {
inside = !inside;
}
have_prev = false;
continue;
}
if have_prev {
if edge_crosses(p, prev, vi) {
inside = !inside;
}
} else {
first = vi;
}
prev = vi;
have_prev = true;
}
if have_prev && edge_crosses(p, prev, first) {
inside = !inside;
}
return inside;
}
// Per-family hatch test (identical math to wipeout.wgsl)
// `ddx_xz`/`ddy_xz` are screen-space derivatives of `xz`, taken once in
@ -244,8 +186,6 @@ fn check_family(
let ddx_xz = dpdx(v.xz);
let ddy_xz = dpdy(v.xz);
if !in_polygon(v.xz) { discard; }
let base_mode = h.mode & 0xFFu;
let gk = (h.mode >> 8u) & 15u;
let ginv = ((h.mode >> 8u) & 16u) != 0u;

View file

@ -157,7 +157,7 @@ impl DockState {
pub fn width(&self, id: PanelId, win_w: f32) -> f32 {
self.settings(id)
.width
.clamp(DOCK_MIN_W, DOCK_MAX_W.min(win_w * 0.45))
.clamp(DOCK_MIN_W, DOCK_MAX_W.min(win_w * 0.45).max(DOCK_MIN_W))
}
pub fn auto_collapse(&self, id: PanelId) -> bool {
@ -195,11 +195,6 @@ impl DockState {
stack.insert(index, id);
true
}
/// Number of panels currently docked on `side`.
pub fn len(&self, side: DockSide) -> usize {
self.stack(side).len()
}
}
/// Insertion index (0..=total) for dropping a panel whose pointer is at
@ -211,6 +206,11 @@ pub fn drop_index(y: f32, total: usize, avail: f32) -> usize {
}
let h = avail / total as f32;
let idx = (y / h).round() as usize;
// A single full-height panel must not offer a top/bottom split: any drop
// lands in the one available slot.
if total <= 1 {
return 0;
}
idx.min(total)
}
@ -288,6 +288,17 @@ mod tests {
assert_eq!(state.width(PanelId::BlockPalette, 800.0), DOCK_MAX_W.min(360.0));
}
#[test]
fn width_does_not_panic_when_window_minimized() {
let mut state = DockState::default();
state.set_width(PanelId::BlockPalette, 500.0);
// A minimized or not-yet-laid-out window reports width 0; the clamp
// must fall back to DOCK_MIN_W instead of panicking on min > max.
assert_eq!(state.width(PanelId::BlockPalette, 0.0), DOCK_MIN_W);
// Deleted right below the minimum dock width behaves the same way.
assert_eq!(state.width(PanelId::BlockPalette, 100.0), DOCK_MIN_W);
}
#[test]
fn drop_index_maps_between_zero_and_total() {
assert_eq!(drop_index(0.0, 3, 300.0), 0);
@ -298,9 +309,20 @@ mod tests {
}
#[test]
fn len_reports_stack_size() {
let state = DockState::default();
assert_eq!(state.len(DockSide::Left), 1);
assert_eq!(state.len(DockSide::Right), 1);
fn drop_index_single_slot_is_always_zero() {
// One panel sharing the full edge height: any pointer y lands in the
// single slot, so the insertion index must be 0 (no top/bottom split).
assert_eq!(drop_index(5.0, 1, 900.0), 0);
assert_eq!(drop_index(450.0, 1, 900.0), 0);
assert_eq!(drop_index(895.0, 1, 900.0), 0);
}
#[test]
fn drop_index_multi_slot_maps_position() {
// Two panels on an edge produce insertion positions 0..=total: pointer
// near the top lands before the first slot (0); pointer near the bottom
// of the last slot lands after the last (== total, append).
assert_eq!(drop_index(10.0, 2, 900.0), 0);
assert_eq!(drop_index(890.0, 2, 900.0), 2);
}
}

View file

@ -10,6 +10,7 @@ pub mod modal;
pub mod overlay;
pub mod popup;
pub mod properties;
pub mod read_only;
pub mod ribbon;
pub mod side_toolbar;
pub mod statusbar;

Some files were not shown because too many files have changed in this diff Show more