feat: complete freehand sketch workflow

Co-authored-by: ramox81 <184937705+ramox81@users.noreply.github.com>
This commit is contained in:
Hakan Seven 2026-08-21 00:17:44 +03:00
commit 464971969c
32 changed files with 922 additions and 109 deletions

4
Cargo.lock generated
View file

@ -72,7 +72,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
[[package]]
name = "acadrust"
version = "0.4.1"
source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=cf46d04#cf46d047211d1adcb37a079812bc38baa45c98f6"
source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=0908da7#0908da7b6e4f702a6c78359a57f53e2b79cf39eb"
dependencies = [
"ahash 0.8.12",
"anyhow",
@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cadkernel"
version = "0.1.0"
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=b3f3122#b3f3122176e3f3860dd3bbc23f12c92b56f06095"
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=ebeb2ec#ebeb2ecc2d17d92c588b0fa8bbe3864b156bc71f"
dependencies = [
"acadrust",
"cavalier_contours",

View file

@ -27,8 +27,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 = "cf46d04", features = ["serde"] }
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "b3f3122", features = ["acis", "offset"] }
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "0908da7", features = ["serde"] }
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "ebeb2ec", features = ["acis", "offset"] }
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
flate2 = "1"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }

View file

@ -14,7 +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 = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "cf46d04", optional = true, features = ["serde"] }
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "0908da7", optional = true, features = ["serde"] }
# Runtime IPC and serialization (host feature only).
interprocess = { version = "2", optional = true }
@ -37,7 +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.
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "cf46d04", features = ["serde"] }
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "0908da7", features = ["serde"] }
[dev-dependencies]
serde_json = "1"

View file

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

View file

@ -1385,6 +1385,19 @@ draw =
.section-offset-along-z = SECTION الإزاحة على Z:
.sketch-pen-down-move-to-sketch-click-to-lift-e = SKETCH القلم لأسفل — حرّك للرسم، انقر لرفعه، Enter للتسجيل:
.sketch-pen-up-click-to-lower-the-pen-enter-to = SKETCH القلم لأعلى — انقر لإنزاله، Enter للتسجيل:
.sketch-pen = القلم
.sketch-increment = الزيادة
.sketch-record = تسجيل
.sketch-stop-erasing = إيقاف المسح
.sketch-connect = توصيل
.sketch-exit = خروج
.sketch-quit = إلغاء
.sketch-object-type = SKETCH نوع الكائن [خط/متعدد الخطوط/Spline]:
.sketch-record-increment = SKETCH زيادة التسجيل <__ocs_arg_value__>:
.sketch-spline-fit-tolerance = SKETCH تفاوت ملاءمة منحنى Spline <__ocs_arg_value__>:
.sketch-erase-move-backward = SKETCH المسح — تحرّك للخلف فوق الرسم المؤقت:
.sketch-pen-down-move = SKETCH (__ocs_arg_type__، الزيادة __ocs_arg_increment__) القلم لأسفل — حرّك للرسم:
.sketch-pen-up-click = SKETCH (__ocs_arg_type__، الزيادة __ocs_arg_increment__) القلم لأعلى — انقر لإنزاله:
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE محور مستوى القطع [X / Y / Z] (أضف TOP/BOTTOM بالكتابة):
.slice-offset-along-x = SLICE الإزاحة على X:
.slice-offset-along-y = SLICE الإزاحة على Y:

View file

@ -1372,6 +1372,19 @@ draw =
.section-offset-along-z = SECTION отместване по Z:
.sketch-pen-down-move-to-sketch-click-to-lift-e = SKETCH Перо долу — премести за скициране, кликни за вдигане, Enter за записване:
.sketch-pen-up-click-to-lower-the-pen-enter-to = SKETCH Перо горе — кликни за сваляне на перото, Enter за записване:
.sketch-pen = Перо
.sketch-increment = Стъпка
.sketch-record = Запис
.sketch-stop-erasing = Спри изтриването
.sketch-connect = Свържи
.sketch-exit = Изход
.sketch-quit = Отказ
.sketch-object-type = SKETCH Тип обект [Линия/Полилиния/Spline]:
.sketch-record-increment = SKETCH Стъпка на запис <__ocs_arg_value__>:
.sketch-spline-fit-tolerance = SKETCH Толеранс на напасване на Spline <__ocs_arg_value__>:
.sketch-erase-move-backward = SKETCH Изтриване — движете се назад върху временната скица:
.sketch-pen-down-move = SKETCH (__ocs_arg_type__, стъпка __ocs_arg_increment__) Перо долу — движете за скициране:
.sketch-pen-up-click = SKETCH (__ocs_arg_type__, стъпка __ocs_arg_increment__) Перо горе — кликнете за сваляне:
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE ос на режещата равнина [X / Y / Z] (добави TOP/BOTTOM чрез въвеждане):
.slice-offset-along-x = SLICE отместване по X:
.slice-offset-along-y = SLICE отместване по Y:

View file

@ -1372,6 +1372,19 @@ draw =
.section-offset-along-z = Odsazení SECTION podél Z:
.sketch-pen-down-move-to-sketch-click-to-lift-e = SKETCH Perem dolů pohybem na skicu, kliknutím zvedněte, Enter zaznamenáte:
.sketch-pen-up-click-to-lower-the-pen-enter-to = SKETCH Pero nahoru kliknutím spustíte pero, Enter zaznamenáte:
.sketch-pen = Pero
.sketch-increment = Přírůstek
.sketch-record = Zaznamenat
.sketch-stop-erasing = Zastavit mazání
.sketch-connect = Připojit
.sketch-exit = Ukončit
.sketch-quit = Zahodit
.sketch-object-type = SKETCH Typ objektu [Úsečka/Polylinie/Spline]:
.sketch-record-increment = SKETCH Krok záznamu <__ocs_arg_value__>:
.sketch-spline-fit-tolerance = SKETCH Tolerance proložení spline <__ocs_arg_value__>:
.sketch-erase-move-backward = SKETCH Mazání — pohybujte se zpět po dočasné skice:
.sketch-pen-down-move = SKETCH (__ocs_arg_type__, krok __ocs_arg_increment__) Pero dole — kreslete pohybem:
.sketch-pen-up-click = SKETCH (__ocs_arg_type__, krok __ocs_arg_increment__) Pero nahoře — kliknutím jej spusťte:
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE osa řezné roviny [X / Y / Z] (přidejte TOP/BOTTOM zadáním):
.slice-offset-along-x = Odsazení SLICE podél X:
.slice-offset-along-y = Odsazení SLICE podél Y:

View file

@ -1373,6 +1373,19 @@ draw =
.section-offset-along-z = SECTION Versetzt entlang Z:
.sketch-pen-down-move-to-sketch-click-to-lift-e = SKETCH Pen down — zum Skizzieren bewegen, zum Liften klicken, zum Aufzeichnen eingeben:
.sketch-pen-up-click-to-lower-the-pen-enter-to = SKETCH Pen up — klicken Sie auf den Stift zu senken, geben Sie ein, um aufzuzeichnen:
.sketch-pen = Stift
.sketch-increment = Schrittweite
.sketch-record = Aufzeichnen
.sketch-stop-erasing = Radieren beenden
.sketch-connect = Verbinden
.sketch-exit = Beenden
.sketch-quit = Verwerfen
.sketch-object-type = SKETCH Objekttyp [Linie/Polylinie/Spline]:
.sketch-record-increment = SKETCH Aufzeichnungsschritt <__ocs_arg_value__>:
.sketch-spline-fit-tolerance = SKETCH Spline-Anpassungstoleranz <__ocs_arg_value__>:
.sketch-erase-move-backward = SKETCH Radieren — rückwärts über die temporäre Skizze bewegen:
.sketch-pen-down-move = SKETCH (__ocs_arg_type__, Schrittweite __ocs_arg_increment__) Stift unten — zum Skizzieren bewegen:
.sketch-pen-up-click = SKETCH (__ocs_arg_type__, Schrittweite __ocs_arg_increment__) Stift oben — zum Absenken klicken:
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE Schnittebenenachse [X / Y / Z] (fügen Sie TOP/BOTTOM durch Eingabe hinzu):
.slice-offset-along-x = SLICE versetzt entlang X:
.slice-offset-along-y = SLICE längs Y versetzt:

View file

@ -1373,6 +1373,19 @@ draw =
.section-offset-along-z = SECTION offset along Z:
.sketch-pen-down-move-to-sketch-click-to-lift-e = SKETCH Pen down — move to sketch, click to lift, Enter to record:
.sketch-pen-up-click-to-lower-the-pen-enter-to = SKETCH Pen up — click to lower the pen, Enter to record:
.sketch-pen = Pen
.sketch-increment = Increment
.sketch-record = Record
.sketch-stop-erasing = Stop erasing
.sketch-connect = Connect
.sketch-exit = Exit
.sketch-quit = Quit
.sketch-object-type = SKETCH Object type [Line/Polyline/Spline]:
.sketch-record-increment = SKETCH Record increment <__ocs_arg_value__>:
.sketch-spline-fit-tolerance = SKETCH Spline fit tolerance <__ocs_arg_value__>:
.sketch-erase-move-backward = SKETCH Erase — move backward over the temporary sketch:
.sketch-pen-down-move = SKETCH (__ocs_arg_type__, increment __ocs_arg_increment__) Pen down — move to sketch:
.sketch-pen-up-click = SKETCH (__ocs_arg_type__, increment __ocs_arg_increment__) Pen up — click to lower the pen:
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE cutting-plane axis [X / Y / Z] (add TOP/BOTTOM by typing):
.slice-offset-along-x = SLICE offset along X:
.slice-offset-along-y = SLICE offset along Y:

View file

@ -1373,6 +1373,19 @@ draw =
.section-offset-along-z = SECTION desfase a lo largo de Z:
.sketch-pen-down-move-to-sketch-click-to-lift-e = SKETCH Lápiz abajo: mueva para dibujar, pulse para levantarlo, Intro para registrar:
.sketch-pen-up-click-to-lower-the-pen-enter-to = SKETCH Lápiz arriba: pulse para bajarlo, Intro para registrar:
.sketch-pen = Lápiz
.sketch-increment = Incremento
.sketch-record = Registrar
.sketch-stop-erasing = Detener borrado
.sketch-connect = Conectar
.sketch-exit = Salir
.sketch-quit = Descartar
.sketch-object-type = SKETCH Tipo de objeto [Línea/Polilínea/Spline]:
.sketch-record-increment = SKETCH Incremento de registro <__ocs_arg_value__>:
.sketch-spline-fit-tolerance = SKETCH Tolerancia de ajuste de spline <__ocs_arg_value__>:
.sketch-erase-move-backward = SKETCH Borrar: retroceda sobre el boceto temporal:
.sketch-pen-down-move = SKETCH (__ocs_arg_type__, incremento __ocs_arg_increment__) Lápiz abajo: mueva para dibujar:
.sketch-pen-up-click = SKETCH (__ocs_arg_type__, incremento __ocs_arg_increment__) Lápiz arriba: pulse para bajarlo:
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE eje del plano de corte [X / Y / Z] (escriba TOP/BOTTOM para añadirlo):
.slice-offset-along-x = SLICE desfase a lo largo de X:
.slice-offset-along-y = SLICE desfase a lo largo de Y:

View file

@ -1372,6 +1372,19 @@ draw =
.section-offset-along-z = SECTION offset pitkin Z:
.sketch-pen-down-move-to-sketch-click-to-lift-e = SKETCH Kynä alas — siirrä luonnos, napsauta nostaaksesi, Enter tallentaa:
.sketch-pen-up-click-to-lower-the-pen-enter-to = SKETCH Kynä ylös — napsauta laskeaksesi kynää, Enter tallentaaksesi:
.sketch-pen = Kynä
.sketch-increment = Askel
.sketch-record = Tallenna
.sketch-stop-erasing = Lopeta pyyhkiminen
.sketch-connect = Yhdistä
.sketch-exit = Lopeta
.sketch-quit = Hylkää
.sketch-object-type = SKETCH Kohdetyyppi [Viiva/Moniviiva/Spline]:
.sketch-record-increment = SKETCH Tallennusaskel <__ocs_arg_value__>:
.sketch-spline-fit-tolerance = SKETCH Splinen sovitustoleranssi <__ocs_arg_value__>:
.sketch-erase-move-backward = SKETCH Pyyhi — liiku taaksepäin väliaikaisen luonnoksen päällä:
.sketch-pen-down-move = SKETCH (__ocs_arg_type__, askel __ocs_arg_increment__) Kynä alhaalla — piirrä liikuttamalla:
.sketch-pen-up-click = SKETCH (__ocs_arg_type__, askel __ocs_arg_increment__) Kynä ylhäällä — laske napsauttamalla:
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE leikkaustason akseli [X / Y / Z] (lisää TOP/BOTTOM kirjoittamalla):
.slice-offset-along-x = SLICE offset pitkin X:
.slice-offset-along-y = SLICE offset pitkin Y:

View file

@ -1373,6 +1373,19 @@ draw =
.section-offset-along-z = SECTION décalage le long de Z:
.sketch-pen-down-move-to-sketch-click-to-lift-e = SKETCH Pen vers le bas — déplacer vers le croquis, cliquer pour soulever, Entrer pour enregistrer:
.sketch-pen-up-click-to-lower-the-pen-enter-to = SKETCH Pen up — cliquez pour baisser le stylo, entrez pour enregistrer:
.sketch-pen = Stylo
.sketch-increment = Incrément
.sketch-record = Enregistrer
.sketch-stop-erasing = Arrêter deffacer
.sketch-connect = Relier
.sketch-exit = Terminer
.sketch-quit = Abandonner
.sketch-object-type = SKETCH Type dobjet [Ligne/Polyligne/Spline] :
.sketch-record-increment = SKETCH Pas denregistrement <__ocs_arg_value__> :
.sketch-spline-fit-tolerance = SKETCH Tolérance dajustement de la spline <__ocs_arg_value__> :
.sketch-erase-move-backward = SKETCH Effacer — revenez en arrière sur lesquisse temporaire :
.sketch-pen-down-move = SKETCH (__ocs_arg_type__, incrément __ocs_arg_increment__) Stylo baissé — déplacez pour dessiner :
.sketch-pen-up-click = SKETCH (__ocs_arg_type__, incrément __ocs_arg_increment__) Stylo levé — cliquez pour labaisser :
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE axe du plan de coupe [X / Y / Z] (ajouter TOP/BOTTOM par typage):
.slice-offset-along-x = SLICE décalage le long de X:
.slice-offset-along-y = SLICE décalage le long de Y:

View file

@ -1361,6 +1361,19 @@ draw =
.section-offset-along-z = SECTION Z के साथ ऑफसेट:
.sketch-pen-down-move-to-sketch-click-to-lift-e = SKETCH पेन नीचे — स्केच करने के लिए चलाएँ, पेन उठाने के लिए क्लिक करें, रिकॉर्ड करने के लिए Enter दबाएँ:
.sketch-pen-up-click-to-lower-the-pen-enter-to = SKETCH पेन ऊपर — पेन नीचे करने के लिए क्लिक करें, रिकॉर्ड करने के लिए Enter दबाएँ:
.sketch-pen = पेन
.sketch-increment = वृद्धि
.sketch-record = रिकॉर्ड
.sketch-stop-erasing = मिटाना रोकें
.sketch-connect = जोड़ें
.sketch-exit = बाहर निकलें
.sketch-quit = छोड़ें
.sketch-object-type = SKETCH ऑब्जेक्ट प्रकार [रेखा/पॉलीलाइन/Spline]:
.sketch-record-increment = SKETCH रिकॉर्ड वृद्धि <__ocs_arg_value__>:
.sketch-spline-fit-tolerance = SKETCH Spline फ़िट सहनशीलता <__ocs_arg_value__>:
.sketch-erase-move-backward = SKETCH मिटाएँ — अस्थायी स्केच पर पीछे की ओर चलें:
.sketch-pen-down-move = SKETCH (__ocs_arg_type__, वृद्धि __ocs_arg_increment__) पेन नीचे — स्केच करने के लिए चलाएँ:
.sketch-pen-up-click = SKETCH (__ocs_arg_type__, वृद्धि __ocs_arg_increment__) पेन ऊपर — नीचे करने के लिए क्लिक करें:
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE कटिंग-प्लेन अक्ष [X / Y / Z] (Typing द्वारा शीर्ष/BOTTOM को शामिल किया गया):
.slice-offset-along-x = SLICE X के साथ ऑफसेट:
.slice-offset-along-y = SLICE वाई के साथ ऑफसेट:

View file

@ -1372,6 +1372,19 @@ draw =
.section-offset-along-z = SECTION eltolás a Z mentén:
.sketch-pen-down-move-to-sketch-click-to-lift-e = SKETCH Toll lefelé mozgassa a vázlatot, kattintson az emeléshez, Enter a rögzítéshez:
.sketch-pen-up-click-to-lower-the-pen-enter-to = SKETCH Toll felfelé kattintson a toll leengedéséhez, Enter a rögzítéshez:
.sketch-pen = Toll
.sketch-increment = Lépésköz
.sketch-record = Rögzítés
.sketch-stop-erasing = Törlés leállítása
.sketch-connect = Csatlakoztatás
.sketch-exit = Kilépés
.sketch-quit = Elvetés
.sketch-object-type = SKETCH Objektumtípus [Vonal/Vonallánc/Spline]:
.sketch-record-increment = SKETCH Rögzítési lépés <__ocs_arg_value__>:
.sketch-spline-fit-tolerance = SKETCH Spline illesztési tűrése <__ocs_arg_value__>:
.sketch-erase-move-backward = SKETCH Törlés — haladjon visszafelé az ideiglenes vázlaton:
.sketch-pen-down-move = SKETCH (__ocs_arg_type__, lépés __ocs_arg_increment__) Toll lent — mozgassa a rajzoláshoz:
.sketch-pen-up-click = SKETCH (__ocs_arg_type__, lépés __ocs_arg_increment__) Toll fent — kattintson a leengedéshez:
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE vágósík tengelye [X / Y / Z] (adja hozzá a TOP/BOTTOM-t beírással):
.slice-offset-along-x = SLICE eltolás a X mentén:
.slice-offset-along-y = SLICE eltolás a Y mentén:

View file

@ -1372,6 +1372,19 @@ draw =
.section-offset-along-z = Scostamento SECTION lungo Z:
.sketch-pen-down-move-to-sketch-click-to-lift-e = SKETCH Penna giù: sposta sullo schizzo, fai clic per sollevare, Enter per registrare:
.sketch-pen-up-click-to-lower-the-pen-enter-to = SKETCH Penna su: fare clic per abbassare la penna, Enter per registrare:
.sketch-pen = Penna
.sketch-increment = Incremento
.sketch-record = Registra
.sketch-stop-erasing = Interrompi cancellazione
.sketch-connect = Connetti
.sketch-exit = Esci
.sketch-quit = Abbandona
.sketch-object-type = SKETCH Tipo di oggetto [Linea/Polilinea/Spline]:
.sketch-record-increment = SKETCH Incremento di registrazione <__ocs_arg_value__>:
.sketch-spline-fit-tolerance = SKETCH Tolleranza di adattamento spline <__ocs_arg_value__>:
.sketch-erase-move-backward = SKETCH Cancella — spostati indietro sullo schizzo temporaneo:
.sketch-pen-down-move = SKETCH (__ocs_arg_type__, incremento __ocs_arg_increment__) Penna abbassata — spostati per disegnare:
.sketch-pen-up-click = SKETCH (__ocs_arg_type__, incremento __ocs_arg_increment__) Penna sollevata — fai clic per abbassarla:
.slice-cutting-plane-axis-x-y-z-add-top-bottom = Asse del piano di taglio SLICE [X / Y / Z] (aggiungere TOP/BOTTOM digitando):
.slice-offset-along-x = Scostamento SLICE lungo X:
.slice-offset-along-y = Scostamento SLICE lungo Y:

View file

@ -1363,6 +1363,19 @@ draw =
.section-offset-along-z = SECTION Z 方向オフセット:
.sketch-pen-down-move-to-sketch-click-to-lift-e = SKETCH ペンを下げました — 移動してスケッチ、クリックで上げ、Enter で記録:
.sketch-pen-up-click-to-lower-the-pen-enter-to = SKETCH ペンを上げました — クリックで下げ、Enter で記録:
.sketch-pen = ペン
.sketch-increment = 増分
.sketch-record = 記録
.sketch-stop-erasing = 消去を停止
.sketch-connect = 接続
.sketch-exit = 終了
.sketch-quit = 破棄
.sketch-object-type = SKETCH オブジェクト タイプ [線分/ポリライン/スプライン]:
.sketch-record-increment = SKETCH 記録増分 <__ocs_arg_value__>:
.sketch-spline-fit-tolerance = SKETCH スプライン フィット許容差 <__ocs_arg_value__>:
.sketch-erase-move-backward = SKETCH 消去 — 一時スケッチ上を後方へ移動してください:
.sketch-pen-down-move = SKETCH (__ocs_arg_type__、増分 __ocs_arg_increment__) ペンを下げました — 移動してスケッチ:
.sketch-pen-up-click = SKETCH (__ocs_arg_type__、増分 __ocs_arg_increment__) ペンを上げました — クリックで下げます:
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE 切断面の軸 [X / Y / Z]TOP/BOTTOM を追加可能):
.slice-offset-along-x = SLICE X 方向オフセット:
.slice-offset-along-y = SLICE Y 方向オフセット:

View file

@ -1372,6 +1372,19 @@ draw =
.section-offset-along-z = Z를 따라 SECTION 오프셋:
.sketch-pen-down-move-to-sketch-click-to-lift-e = SKETCH 펜 아래로 — 스케치로 이동하고 클릭하여 올리며 Enter로 기록:
.sketch-pen-up-click-to-lower-the-pen-enter-to = SKETCH 펜 업 — 펜을 내리려면 클릭하고 기록하려면 Enter를 클릭하세요.
.sketch-pen = 펜
.sketch-increment = 증분
.sketch-record = 기록
.sketch-stop-erasing = 지우기 중지
.sketch-connect = 연결
.sketch-exit = 종료
.sketch-quit = 취소
.sketch-object-type = SKETCH 객체 유형 [선/폴리선/스플라인]:
.sketch-record-increment = SKETCH 기록 증분 <__ocs_arg_value__>:
.sketch-spline-fit-tolerance = SKETCH 스플라인 맞춤 공차 <__ocs_arg_value__>:
.sketch-erase-move-backward = SKETCH 지우기 — 임시 스케치 위를 뒤로 이동하십시오:
.sketch-pen-down-move = SKETCH (__ocs_arg_type__, 증분 __ocs_arg_increment__) 펜 내림 — 이동하여 스케치:
.sketch-pen-up-click = SKETCH (__ocs_arg_type__, 증분 __ocs_arg_increment__) 펜 올림 — 클릭하여 내리기:
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE 절단 평면 축 [X / Y / Z] (입력하여 TOP/BOTTOM 추가):
.slice-offset-along-x = X를 따라 SLICE 오프셋:
.slice-offset-along-y = Y를 따라 SLICE 오프셋:

View file

@ -1373,6 +1373,19 @@ draw =
.section-offset-along-z = SECTION offset langs Z:
.sketch-pen-down-move-to-sketch-click-to-lift-e = SKETCH Pen naar beneden
.sketch-pen-up-click-to-lower-the-pen-enter-to = SKETCH Pen omhoog
.sketch-pen = Pen
.sketch-increment = Stapgrootte
.sketch-record = Vastleggen
.sketch-stop-erasing = Wissen stoppen
.sketch-connect = Verbinden
.sketch-exit = Afsluiten
.sketch-quit = Annuleren
.sketch-object-type = SKETCH Objecttype [Lijn/Polylijn/Spline]:
.sketch-record-increment = SKETCH Opnamestap <__ocs_arg_value__>:
.sketch-spline-fit-tolerance = SKETCH Spline-aanpassingstolerantie <__ocs_arg_value__>:
.sketch-erase-move-backward = SKETCH Wissen — beweeg terug over de tijdelijke schets:
.sketch-pen-down-move = SKETCH (__ocs_arg_type__, stap __ocs_arg_increment__) Pen omlaag — beweeg om te schetsen:
.sketch-pen-up-click = SKETCH (__ocs_arg_type__, stap __ocs_arg_increment__) Pen omhoog — klik om de pen neer te zetten:
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE snijvlakas [X / Y / Z] (voeg TOP/BOTTOM toe door te typen):
.slice-offset-along-x = SLICE offset langs X:
.slice-offset-along-y = SLICE offset langs Y:

View file

@ -1372,6 +1372,19 @@ draw =
.section-offset-along-z = Przesunięcie SECTION wzdłuż Z:
.sketch-pen-down-move-to-sketch-click-to-lift-e = SKETCH Opuść pióro — przejdź do szkicu, kliknij, aby podnieść, Enter, aby nagrać:
.sketch-pen-up-click-to-lower-the-pen-enter-to = SKETCH Pióro w górę — kliknij, aby opuścić pióro, Enter, aby nagrać:
.sketch-pen = Pióro
.sketch-increment = Przyrost
.sketch-record = Zapisz
.sketch-stop-erasing = Zatrzymaj wymazywanie
.sketch-connect = Połącz
.sketch-exit = Zakończ
.sketch-quit = Porzuć
.sketch-object-type = SKETCH Typ obiektu [Linia/Polilinia/Spline]:
.sketch-record-increment = SKETCH Przyrost zapisu <__ocs_arg_value__>:
.sketch-spline-fit-tolerance = SKETCH Tolerancja dopasowania spline <__ocs_arg_value__>:
.sketch-erase-move-backward = SKETCH Wymazywanie — przesuń się wstecz po szkicu tymczasowym:
.sketch-pen-down-move = SKETCH (__ocs_arg_type__, przyrost __ocs_arg_increment__) Pióro opuszczone — przesuń, aby szkicować:
.sketch-pen-up-click = SKETCH (__ocs_arg_type__, przyrost __ocs_arg_increment__) Pióro podniesione — kliknij, aby opuścić:
.slice-cutting-plane-axis-x-y-z-add-top-bottom = Oś płaszczyzny cięcia SLICE [X / Y / Z] (dodaj TOP/BOTTOM wpisując):
.slice-offset-along-x = Przesunięcie SLICE wzdłuż X:
.slice-offset-along-y = Przesunięcie SLICE wzdłuż Y:

View file

@ -1373,6 +1373,19 @@ draw =
.section-offset-along-z = SECTION deslocamento ao longo de Z:
.sketch-pen-down-move-to-sketch-click-to-lift-e = SKETCH Caneta abaixada — mova para esboçar, clique para levantar, Enter para registrar:
.sketch-pen-up-click-to-lower-the-pen-enter-to = SKETCH Caneta levantada — clique para abaixar, Enter para registrar:
.sketch-pen = Caneta
.sketch-increment = Incremento
.sketch-record = Registrar
.sketch-stop-erasing = Parar de apagar
.sketch-connect = Conectar
.sketch-exit = Sair
.sketch-quit = Descartar
.sketch-object-type = SKETCH Tipo de objeto [Linha/Polilinha/Spline]:
.sketch-record-increment = SKETCH Incremento de registro <__ocs_arg_value__>:
.sketch-spline-fit-tolerance = SKETCH Tolerância de ajuste da spline <__ocs_arg_value__>:
.sketch-erase-move-backward = SKETCH Apagar — mova para trás sobre o esboço temporário:
.sketch-pen-down-move = SKETCH (__ocs_arg_type__, incremento __ocs_arg_increment__) Caneta abaixada — mova para esboçar:
.sketch-pen-up-click = SKETCH (__ocs_arg_type__, incremento __ocs_arg_increment__) Caneta levantada — clique para abaixar:
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE eixo do plano de corte [X / Y / Z] (digite TOP/BOTTOM para adicionar):
.slice-offset-along-x = SLICE deslocamento ao longo de X:
.slice-offset-along-y = SLICE deslocamento ao longo de Y:

View file

@ -1376,6 +1376,19 @@ draw =
.section-offset-along-z = SECTION Смещение вдоль Z:
.sketch-pen-down-move-to-sketch-click-to-lift-e = SKETCH Нажмите вниз — перейдите к эскизу, нажмите, чтобы поднять, Введите, чтобы записать:
.sketch-pen-up-click-to-lower-the-pen-enter-to = SKETCH Pen up — нажмите, чтобы опустить ручку, Введите для записи:
.sketch-pen = Перо
.sketch-increment = Шаг
.sketch-record = Записать
.sketch-stop-erasing = Остановить стирание
.sketch-connect = Соединить
.sketch-exit = Завершить
.sketch-quit = Отменить
.sketch-object-type = SKETCH Тип объекта [Отрезок/Полилиния/Сплайн]:
.sketch-record-increment = SKETCH Шаг записи <__ocs_arg_value__>:
.sketch-spline-fit-tolerance = SKETCH Допуск аппроксимации сплайна <__ocs_arg_value__>:
.sketch-erase-move-backward = SKETCH Стирание — двигайтесь назад по временному эскизу:
.sketch-pen-down-move = SKETCH (__ocs_arg_type__, шаг __ocs_arg_increment__) Перо опущено — двигайте для рисования:
.sketch-pen-up-click = SKETCH (__ocs_arg_type__, шаг __ocs_arg_increment__) Перо поднято — щёлкните, чтобы опустить:
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE ось режущей плоскости [X/Y/Z] (добавить TOP/BOTTOM путем набора текста):
.slice-offset-along-x = SLICE Смещение вдоль X:
.slice-offset-along-y = SLICE смещение вдоль Y:

View file

@ -1355,6 +1355,19 @@ draw =
.section-offset-along-z = SECTION Z boyunca ofset:
.sketch-pen-down-move-to-sketch-click-to-lift-e = SKETCH Kalem aşağıda — çizmek için hareket ettirin, kaldırmak için tıklayın, kaydetmek için Enter:
.sketch-pen-up-click-to-lower-the-pen-enter-to = SKETCH Kalem yukarıda — indirmek için tıklayın, kaydetmek için Enter:
.sketch-pen = Kalem
.sketch-increment = Artış
.sketch-record = Kaydet
.sketch-stop-erasing = Silmeyi durdur
.sketch-connect = Bağla
.sketch-exit = Çıkış
.sketch-quit = Vazgeç
.sketch-object-type = SKETCH Nesne türü [Çizgi/Çoklu çizgi/Spline]:
.sketch-record-increment = SKETCH Kayıt artışı <__ocs_arg_value__>:
.sketch-spline-fit-tolerance = SKETCH Spline uyum toleransı <__ocs_arg_value__>:
.sketch-erase-move-backward = SKETCH Sil — geçici çizim üzerinde geriye doğru hareket edin:
.sketch-pen-down-move = SKETCH (__ocs_arg_type__, artış __ocs_arg_increment__) Kalem aşağıda — çizmek için hareket ettirin:
.sketch-pen-up-click = SKETCH (__ocs_arg_type__, artış __ocs_arg_increment__) Kalem yukarıda — indirmek için tıklayın:
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE Kesme düzlemi ekseni [X / Y / Z] (TOP/BOTTOM yazarak ekleyin):
.slice-offset-along-x = SLICE X boyunca ofset:
.slice-offset-along-y = SLICE Y boyunca ofset:

View file

@ -1360,6 +1360,19 @@ draw =
.section-offset-along-z = SECTION 与 Z 相冲:
.sketch-pen-down-move-to-sketch-click-to-lift-e = SKETCH 落笔 — 移动以绘图,单击抬笔,按 Enter 记录:
.sketch-pen-up-click-to-lower-the-pen-enter-to = SKETCH 抬笔 — 单击落笔,按 Enter 记录:
.sketch-pen = 画笔
.sketch-increment = 增量
.sketch-record = 记录
.sketch-stop-erasing = 停止擦除
.sketch-connect = 连接
.sketch-exit = 退出
.sketch-quit = 放弃
.sketch-object-type = SKETCH 对象类型 [直线/多段线/样条曲线]
.sketch-record-increment = SKETCH 记录增量 <__ocs_arg_value__>
.sketch-spline-fit-tolerance = SKETCH 样条曲线拟合公差 <__ocs_arg_value__>
.sketch-erase-move-backward = SKETCH 擦除 — 沿临时草图向后移动:
.sketch-pen-down-move = SKETCH__ocs_arg_type__增量 __ocs_arg_increment__ 落笔 — 移动以绘图:
.sketch-pen-up-click = SKETCH__ocs_arg_type__增量 __ocs_arg_increment__ 抬笔 — 单击落笔:
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE 剖切平面轴 [X / Y / Z](输入 TOP/BOTTOM 可追加):
.slice-offset-along-x = SLICE 页:1
.slice-offset-along-y = SLICE 与 Y 相冲:

View file

@ -1372,6 +1372,19 @@ draw =
.section-offset-along-z = SECTION 與 Z 相衝:
.sketch-pen-down-move-to-sketch-click-to-lift-e = SKETCH 落筆 — 移動以繪圖,按一下抬筆,按 Enter 記錄:
.sketch-pen-up-click-to-lower-the-pen-enter-to = SKETCH 抬筆 — 按一下落筆,按 Enter 記錄:
.sketch-pen = 畫筆
.sketch-increment = 增量
.sketch-record = 記錄
.sketch-stop-erasing = 停止擦除
.sketch-connect = 連接
.sketch-exit = 結束
.sketch-quit = 放棄
.sketch-object-type = SKETCH 物件類型 [直線/聚合線/雲形線]
.sketch-record-increment = SKETCH 記錄增量 <__ocs_arg_value__>
.sketch-spline-fit-tolerance = SKETCH 雲形線擬合公差 <__ocs_arg_value__>
.sketch-erase-move-backward = SKETCH 擦除 — 沿暫存草圖向後移動:
.sketch-pen-down-move = SKETCH__ocs_arg_type__增量 __ocs_arg_increment__ 落筆 — 移動以繪圖:
.sketch-pen-up-click = SKETCH__ocs_arg_type__增量 __ocs_arg_increment__ 抬筆 — 按一下落筆:
.slice-cutting-plane-axis-x-y-z-add-top-bottom = SLICE 剖切平面軸 [X / Y / Z](輸入 TOP/BOTTOM 可追加):
.slice-offset-along-x = SLICE 頁:1
.slice-offset-along-y = SLICE 與 Y 相衝:

View file

@ -642,6 +642,26 @@ impl OpenCADStudio {
/// Pure-selection commands (SELECTALL, QSELECT, …) run without an active
/// command, so `was_active` is false and their selection is preserved.
pub(super) fn apply_cmd_result(&mut self, result: CmdResult) -> Task<Message> {
let settings = self.tabs[self.active_tab]
.active_cmd
.as_ref()
.and_then(|command| command.sketch_settings());
if let Some((sketch_type, increment, tolerance)) = settings {
let tab = &mut self.tabs[self.active_tab];
let changed = tab.scene.document.header.sketch_type != sketch_type
|| (tab.scene.document.header.sketch_increment - increment).abs() > f64::EPSILON
|| (tab.scene.document.header.sketch_tolerance - tolerance).abs()
> f64::EPSILON;
if changed {
crate::io::set_sketch_settings(
&mut tab.scene.document,
sketch_type,
increment,
tolerance,
);
tab.dirty = true;
}
}
let was_active = self.tabs[self.active_tab].active_cmd.is_some();
let preserve_selection =
matches!(result, CmdResult::Relaunch(..) | CmdResult::Dispatch(..));
@ -785,6 +805,7 @@ impl OpenCADStudio {
self.commit_entity(entity);
}
self.tabs[i].dirty = true;
self.tabs[i].scene.clear_preview_wire();
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
if let Some(p) = prompt {
self.command_line.push_info(&p);

View file

@ -487,7 +487,12 @@ impl OpenCADStudio {
"SKETCH" => {
use crate::modules::draw::draw::sketch::SketchCommand;
let new_cmd = SketchCommand::new();
let header = &self.tabs[i].scene.document.header;
let new_cmd = SketchCommand::new(
header.sketch_type,
header.sketch_increment,
header.sketch_tolerance,
);
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}

View file

@ -526,6 +526,8 @@ inventory::submit!(crate::command::CommandRegistration {
"HALOGAP",
"TRACEWID",
"SKETCHINC",
"SKPOLY",
"SKTOLERANCE",
// Reset selected entities' overrides to follow their layer.
"SETBYLAYER",
// Remove duplicate objects; set drawing base point; audit integrity;

View file

@ -931,6 +931,8 @@ impl OpenCADStudio {
| "HALOGAP"
| "TRACEWID"
| "SKETCHINC"
| "SKPOLY"
| "SKTOLERANCE"
) =>
{
return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i);
@ -953,7 +955,7 @@ impl OpenCADStudio {
let value = it.next().map(|s| s.trim().to_string());
if name.is_empty() || name == "?" {
self.command_line.push_info(
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG ATTREQ ATTDIA DIMASSOC ANGBASE ANGDIR | CLAYER CELTYPE TEXTSTYLE (read-only)",
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG ATTREQ ATTDIA DIMASSOC ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE | CLAYER CELTYPE TEXTSTYLE (read-only)",
);
} else {
let frame_kind = crate::scene::frame::kind_for_name(&name);
@ -1700,15 +1702,41 @@ impl OpenCADStudio {
None => Ok((format!("TRACEWID = {}", h.trace_width), false)),
},
"SKETCHINC" => match &value {
Some(v) => v
.parse::<f64>()
.map(|x| {
Some(v) => match v.parse::<f64>() {
Ok(x) if x.is_finite() && x > 0.0 => {
let changed = h.sketch_increment != x;
h.sketch_increment = x;
(format!("SKETCHINC = {x}"), true)
})
.map_err(|_| "SETVAR: numeric value required.".into()),
Ok((format!("SKETCHINC = {x}"), changed))
}
_ => Err("SETVAR: positive numeric value required.".into()),
},
None => Ok((format!("SKETCHINC = {}", h.sketch_increment), false)),
},
"SKPOLY" => match &value {
Some(v) => match v.parse::<i16>() {
Ok(x @ 0..=2) => {
let changed = h.sketch_type != x;
h.sketch_type = x;
Ok((format!("SKPOLY = {x}"), changed))
}
_ => Err("SETVAR: integer value from 0 to 2 required.".into()),
},
None => Ok((format!("SKPOLY = {}", h.sketch_type), false)),
},
"SKTOLERANCE" => match &value {
Some(v) => match v.parse::<f64>() {
Ok(x) if x.is_finite() && (0.0..=1.0).contains(&x) => {
let changed = h.sketch_tolerance != x;
h.sketch_tolerance = x;
Ok((format!("SKTOLERANCE = {x}"), changed))
}
_ => Err("SETVAR: numeric value from 0 to 1 required.".into()),
},
None => Ok((
format!("SKTOLERANCE = {}", h.sketch_tolerance),
false,
)),
},
"CLAYER" => match &value {
Some(_) => Err(
"SETVAR: CLAYER is read-only here — use the CLAYER command."

View file

@ -1822,6 +1822,11 @@ pub trait CadCommand: Send {
false
}
/// Current drawing-persisted SKETCH settings.
fn sketch_settings(&self) -> Option<(i16, f64, f64)> {
None
}
/// Returns `true` when the active text prompt expects free-form prose
/// that can legitimately contain whitespace (the body of a TEXT /
/// MTEXT / DDEDIT entity, an attribute default value, etc.). For

View file

@ -1784,6 +1784,7 @@ fn fix_current_style_names(doc: &mut CadDocument) {
doc.header.current_mleader_style_name = v;
}
}
reflect_sketch_settings(doc);
}
/// Find the handle of the `DictionaryVariable` registered under `name` in any
@ -1818,6 +1819,32 @@ pub(crate) fn drawing_variable(doc: &CadDocument, name: &str) -> Option<String>
vardict_value(doc, name)
}
fn reflect_sketch_settings(doc: &mut CadDocument) {
let sketch_type = vardict_value(doc, "SKPOLY")
.and_then(|value| value.parse::<i16>().ok())
.unwrap_or(doc.header.sketch_type)
.clamp(0, 2);
let increment = vardict_value(doc, "SKETCHINC")
.and_then(|value| value.parse::<f64>().ok())
.filter(|value| value.is_finite() && *value > 0.0)
.unwrap_or(doc.header.sketch_increment);
let tolerance = vardict_value(doc, "SKTOLERANCE")
.and_then(|value| value.parse::<f64>().ok())
.filter(|value| value.is_finite() && (0.0..=1.0).contains(value))
.unwrap_or(doc.header.sketch_tolerance);
doc.header.sketch_type = sketch_type;
doc.header.sketch_increment = if increment.is_finite() && increment > 0.0 {
increment
} else {
0.1
};
doc.header.sketch_tolerance = if tolerance.is_finite() {
tolerance.clamp(0.0, 1.0)
} else {
0.5
};
}
/// Write a drawing variable, creating the variable dictionary and record when
/// needed so new drawings preserve the value too.
pub(crate) fn set_drawing_variable(doc: &mut CadDocument, name: &str, value: &str) {
@ -1859,6 +1886,31 @@ pub(crate) fn set_drawing_variable(doc: &mut CadDocument, name: &str, value: &st
}
}
pub(crate) fn set_sketch_settings(
doc: &mut CadDocument,
sketch_type: i16,
increment: f64,
tolerance: f64,
) {
let sketch_type = sketch_type.clamp(0, 2);
let increment = if increment.is_finite() && increment > 0.0 {
increment
} else {
0.1
};
let tolerance = if tolerance.is_finite() {
tolerance.clamp(0.0, 1.0)
} else {
0.5
};
doc.header.sketch_type = sketch_type;
doc.header.sketch_increment = increment;
doc.header.sketch_tolerance = tolerance;
set_drawing_variable(doc, "SKPOLY", &sketch_type.to_string());
set_drawing_variable(doc, "SKETCHINC", &increment.to_string());
set_drawing_variable(doc, "SKTOLERANCE", &tolerance.to_string());
}
/// The layout tab that was active when the drawing was saved — the `CTAB`
/// system variable (stored in the variable dictionary as a `DICTIONARYVAR`).
/// `None` when the file recorded no current tab.
@ -1919,6 +1971,10 @@ fn sync_current_styles_on_save(doc: &mut CadDocument) {
set_drawing_variable(doc, "CMLEADERSTYLE", &mleader);
let annotation = doc.header.current_annotation_scale.clone();
set_drawing_variable(doc, "CANNOSCALE", &annotation);
let sketch_type = doc.header.sketch_type;
let increment = doc.header.sketch_increment;
let tolerance = doc.header.sketch_tolerance;
set_sketch_settings(doc, sketch_type, increment, tolerance);
}
// ── Corrupt-entity guard ──────────────────────────────────────────────────

View file

@ -2230,6 +2230,19 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"SETVAR: {name} requires 0, 1, or 2." => Some(("draw", "setvar-frame-range")),
"SHEETSET: Sheet Set Manager not yet implemented." => Some(("view", "sheetset-sheet-set-manager-not-yet-implemented")),
"SHX" => Some(("properties", "shx")),
"Connect" => Some(("draw", "sketch-connect")),
"Exit" => Some(("draw", "sketch-exit")),
"Increment" => Some(("draw", "sketch-increment")),
"Pen" => Some(("draw", "sketch-pen")),
"Quit" => Some(("draw", "sketch-quit")),
"Record" => Some(("draw", "sketch-record")),
"Stop erasing" => Some(("draw", "sketch-stop-erasing")),
"SKETCH Object type [Line/Polyline/Spline]:" => Some(("draw", "sketch-object-type")),
"SKETCH Record increment <%{value}>:" => Some(("draw", "sketch-record-increment")),
"SKETCH Spline fit tolerance <%{value}>:" => Some(("draw", "sketch-spline-fit-tolerance")),
"SKETCH Erase — move backward over the temporary sketch:" => Some(("draw", "sketch-erase-move-backward")),
"SKETCH (%{type}, increment %{increment}) Pen down — move to sketch:" => Some(("draw", "sketch-pen-down-move")),
"SKETCH (%{type}, increment %{increment}) Pen up — click to lower the pen:" => Some(("draw", "sketch-pen-up-click")),
"SKETCH Pen down — move to sketch, click to lift, Enter to record:" => Some(("draw", "sketch-pen-down-move-to-sketch-click-to-lift-e")),
"SKETCH Pen up — click to lower the pen, Enter to record:" => Some(("draw", "sketch-pen-up-click-to-lower-the-pen-enter-to")),
"SLICE cutting-plane axis [X / Y / Z] (add TOP/BOTTOM by typing):" => Some(("draw", "slice-cutting-plane-axis-x-y-z-add-top-bottom")),

View file

@ -1,33 +1,19 @@
// Freehand sketch tool — interactive command.
//
// Command: SKETCH — freehand sketching. A click toggles the pen down/up. While
// the pen is DOWN, moving the cursor records its position into the current
// stroke whenever it has travelled more than a small fixed threshold, and a
// cyan preview wire of the current stroke is shown. A click while the pen is
// down lifts it, ending the current stroke; the next click starts a fresh one.
// Enter commits every recorded stroke (each stroke of two or more points
// becomes one lightweight polyline of straight segments); Esc cancels.
//
// Geometry is built like the wide-line / ring tools: each stroke is one
// LwPolyline whose vertices carry the sampled XY positions and whose elevation
// is the first sample's Z.
// Freehand sketch tool.
use acadrust::entities::{LwPolyline, LwVertex};
use acadrust::entities::{Line, LwPolyline, LwVertex, Spline};
use acadrust::types::{Vector2, Vector3};
use acadrust::EntityType;
use crate::t;
use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
use cadkernel::geom2d::NurbsCurve;
use glam::DVec3;
// Minimum cursor travel (drawing units) before a new sample is recorded.
const SAMPLE_EPSILON: f64 = 0.5;
use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
use crate::t;
// ── Ribbon definition ─────────────────────────────────────────────────────
const MIN_INCREMENT: f64 = 1.0e-9;
#[allow(dead_code)] // ribbon definition ready for wiring; command works via the command line
#[allow(dead_code)]
pub fn tool() -> ToolDef {
ToolDef {
id: "SKETCH",
@ -37,54 +23,336 @@ pub fn tool() -> ToolDef {
}
}
// ── Command implementation ────────────────────────────────────────────────
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SketchType {
Line,
Polyline,
Spline,
}
impl SketchType {
fn from_header(value: i16) -> Self {
match value {
0 => Self::Line,
2 => Self::Spline,
_ => Self::Polyline,
}
}
fn header_value(self) -> i16 {
match self {
Self::Line => 0,
Self::Polyline => 1,
Self::Spline => 2,
}
}
fn label(self) -> &'static str {
match self {
Self::Line => "Line",
Self::Polyline => "Polyline",
Self::Spline => "Spline",
}
}
fn parse(value: &str) -> Option<Self> {
match value.trim().to_ascii_uppercase().as_str() {
"L" | "LINE" => Some(Self::Line),
"P" | "PLINE" | "POLYLINE" => Some(Self::Polyline),
"S" | "SPLINE" => Some(Self::Spline),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum InputStage {
Drawing,
Type,
Increment,
Tolerance,
}
pub struct SketchCommand {
/// All completed strokes plus, while the pen is down, the one being drawn
/// as the last element. Each stroke is its own list of sampled points.
strokes: Vec<Vec<DVec3>>,
/// True while the pen is down and samples accumulate into the current stroke.
pen_down: bool,
erasing: bool,
erase_engaged: bool,
stage: InputStage,
sketch_type: SketchType,
increment: f64,
tolerance: f64,
last_cursor: Option<DVec3>,
last_recorded_endpoint: Option<DVec3>,
plane: WorkingPlane,
}
impl SketchCommand {
pub fn new() -> Self {
pub fn new(sketch_type: i16, increment: f64, tolerance: f64) -> Self {
Self {
strokes: Vec::new(),
pen_down: false,
erasing: false,
erase_engaged: false,
stage: InputStage::Drawing,
sketch_type: SketchType::from_header(sketch_type),
increment: valid_increment(increment),
tolerance: valid_tolerance(tolerance),
last_cursor: None,
last_recorded_endpoint: None,
plane: WorkingPlane::default(),
}
}
/// Build one lightweight polyline from a stroke's sampled points, or `None`
/// when the stroke has too few points to form a segment.
fn build_stroke(&self, points: &[DVec3]) -> Option<EntityType> {
if points.len() < 2 {
return None;
fn begin_stroke(&mut self, point: DVec3) {
self.strokes.push(vec![point]);
self.pen_down = true;
self.erasing = false;
self.erase_engaged = false;
}
let points: Vec<DVec3> = points
/// Record real pointer positions at the configured minimum spacing.
fn sample_to(&mut self, point: DVec3) {
let Some(stroke) = self.strokes.last_mut() else {
return;
};
let Some(cursor) = stroke.last().copied() else {
stroke.push(point);
return;
};
let distance = cursor.distance(point);
if distance.is_finite() && distance + 1.0e-12 >= self.increment {
stroke.push(point);
}
}
fn finish_stroke(&mut self, point: DVec3) {
self.sample_to(point);
if let Some(stroke) = self.strokes.last_mut() {
if stroke
.last()
.is_some_and(|last| last.distance(point) > 1.0e-9)
{
stroke.push(point);
}
}
self.pen_down = false;
}
fn lift_pen(&mut self) {
if !self.pen_down {
return;
}
if let Some(cursor) = self.last_cursor {
self.finish_stroke(cursor);
} else {
self.pen_down = false;
}
}
fn connect(&mut self) {
self.lift_pen();
let endpoint = self
.strokes
.iter()
.rev()
.find_map(|stroke| stroke.last().copied())
.or(self.last_recorded_endpoint);
if let Some(endpoint) = endpoint {
self.begin_stroke(endpoint);
}
}
/// Erasing is armed when the pointer reaches the latest temporary sample.
/// Moving back over the recorded path then removes samples one by one.
fn erase_at(&mut self, point: DVec3) {
let threshold = (self.increment * 0.45).max(1.0e-6);
loop {
let Some(stroke) = self.strokes.last_mut() else {
self.erase_engaged = false;
break;
};
let Some(last) = stroke.last().copied() else {
self.strokes.pop();
continue;
};
if !self.erase_engaged {
if last.distance(point) <= threshold {
self.erase_engaged = true;
} else {
break;
}
}
if last.distance(point) <= threshold {
stroke.pop();
if stroke.is_empty() {
self.strokes.pop();
self.erase_engaged = false;
}
} else {
break;
}
}
}
fn fit_curve(&self, points: &[DVec3]) -> Option<(NurbsCurve, f64)> {
let local: Vec<DVec3> = points
.iter()
.map(|point| self.plane.to_local(*point))
.collect();
let elevation = points[0].z;
let mut pl = LwPolyline::new();
pl.is_closed = false;
pl.elevation = elevation;
pl.vertices = points
.iter()
.map(|p| LwVertex::new(Vector2::new(p.x, p.y)))
.collect();
pl.normal = Vector3::UNIT_Z;
Some(self.plane.place_entity(EntityType::LwPolyline(pl)))
let elevation = local.first()?.z;
let points: Vec<[f64; 2]> = local.iter().map(|point| [point.x, point.y]).collect();
NurbsCurve::fit_polyline(&points, self.fit_distance())
.map(|curve| (curve, elevation))
}
/// All strokes that have enough points, as committable entities.
fn build_all(&self) -> Vec<EntityType> {
self.strokes
fn fit_distance(&self) -> f64 {
self.tolerance * self.increment
}
fn build_stroke(&self, points: &[DVec3]) -> Vec<EntityType> {
if points.len() < 2 {
return Vec::new();
}
let local: Vec<DVec3> = points
.iter()
.filter_map(|stroke| self.build_stroke(stroke))
.collect()
.map(|point| self.plane.to_local(*point))
.collect();
match self.sketch_type {
SketchType::Line => local
.windows(2)
.map(|pair| {
self.plane.place_entity(EntityType::Line(Line::from_points(
to_vector3(pair[0]),
to_vector3(pair[1]),
)))
})
.collect(),
SketchType::Polyline => {
let mut polyline = LwPolyline::new();
polyline.elevation = local[0].z;
polyline.normal = Vector3::UNIT_Z;
polyline.vertices = local
.iter()
.map(|point| LwVertex::new(Vector2::new(point.x, point.y)))
.collect();
vec![self
.plane
.place_entity(EntityType::LwPolyline(polyline))]
}
SketchType::Spline => {
let Some((curve, elevation)) = self.fit_curve(points) else {
return Vec::new();
};
let mut spline = Spline {
degree: curve.degree() as i32,
knots: curve.knots().to_vec(),
control_points: curve
.control_points()
.iter()
.map(|point| Vector3::new(point[0], point[1], elevation))
.collect(),
fit_tolerance: self.fit_distance(),
normal: Vector3::UNIT_Z,
..Default::default()
};
spline.flags.planar = true;
vec![self.plane.place_entity(EntityType::Spline(spline))]
}
}
}
fn take_entities(&mut self) -> Vec<EntityType> {
self.lift_pen();
let mut entities = Vec::new();
let mut endpoint = None;
for stroke in &self.strokes {
let built = self.build_stroke(stroke);
if !built.is_empty() {
endpoint = stroke.last().copied();
entities.extend(built);
}
}
if !entities.is_empty() {
self.last_recorded_endpoint = endpoint;
}
self.strokes.clear();
self.pen_down = false;
self.erasing = false;
self.erase_engaged = false;
entities
}
fn preview(&self, cursor: Option<DVec3>) -> Option<WireModel> {
let mut combined = Vec::<[f64; 3]>::new();
for (index, stroke) in self.strokes.iter().enumerate() {
let mut points = stroke.clone();
if self.pen_down && index + 1 == self.strokes.len() {
if let Some(cursor) = cursor {
if points
.last()
.is_some_and(|last| last.distance(cursor) > 1.0e-9)
{
points.push(cursor);
}
}
}
if points.len() < 2 {
continue;
}
let display = if self.sketch_type == SketchType::Spline {
match self.fit_curve(&points) {
Some((curve, elevation)) => curve
.tessellate(16)
.into_iter()
.map(|point| {
self.plane
.to_world(DVec3::new(point[0], point[1], elevation))
})
.collect(),
None => points,
}
} else {
points
};
if display.len() < 2 {
continue;
}
if !combined.is_empty() {
combined.push([f64::NAN; 3]);
}
combined.extend(display.into_iter().map(|point| [point.x, point.y, point.z]));
}
(combined.len() >= 2).then(|| {
WireModel::solid_f64("sketch_preview".to_string(), combined, WireModel::CYAN, false)
})
}
fn toggle_pen(&mut self) {
if self.pen_down {
self.lift_pen();
} else if let Some(cursor) = self.last_cursor {
self.begin_stroke(cursor);
}
}
fn drawing_options(&self) -> Vec<CmdOption> {
let mut options = vec![
CmdOption::new("Pen", "PEN"),
CmdOption::new("Type", "TYPE"),
CmdOption::new("Increment", "INCREMENT"),
];
if self.sketch_type == SketchType::Spline {
options.push(CmdOption::new("Tolerance", "TOLERANCE"));
}
options.extend([
CmdOption::new("Record", "RECORD"),
CmdOption::new(if self.erasing { "Stop erasing" } else { "Erase" }, "ERASE"),
CmdOption::new("Connect", "CONNECT"),
CmdOption::new("Exit", "EXIT"),
CmdOption::new("Quit", "QUIT"),
]);
options
}
}
@ -98,72 +366,227 @@ impl CadCommand for SketchCommand {
}
fn prompt(&self) -> String {
if self.pen_down {
t!("SKETCH Pen down — move to sketch, click to lift, Enter to record:").into_owned()
} else {
t!("SKETCH Pen up — click to lower the pen, Enter to record:").into_owned()
match self.stage {
InputStage::Type => t!("SKETCH Object type [Line/Polyline/Spline]:").into_owned(),
InputStage::Increment => t!(
"SKETCH Record increment <%{value}>:",
value = self.increment
)
.into_owned(),
InputStage::Tolerance => t!(
"SKETCH Spline fit tolerance <%{value}>:",
value = self.tolerance
)
.into_owned(),
InputStage::Drawing if self.erasing => {
t!("SKETCH Erase — move backward over the temporary sketch:").into_owned()
}
InputStage::Drawing if self.pen_down => t!(
"SKETCH (%{type}, increment %{increment}) Pen down — move to sketch:",
type = t!(self.sketch_type.label()),
increment = self.increment
)
.into_owned(),
InputStage::Drawing => t!(
"SKETCH (%{type}, increment %{increment}) Pen up — click to lower the pen:",
type = t!(self.sketch_type.label()),
increment = self.increment
)
.into_owned(),
}
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
if self.pen_down {
// Lift the pen: the current stroke is finished. A new click later
// begins a fresh stroke.
self.pen_down = false;
fn options(&self) -> Vec<CmdOption> {
match self.stage {
InputStage::Drawing => self.drawing_options(),
InputStage::Type => vec![
CmdOption::new("Line", "LINE"),
CmdOption::new("Polyline", "POLYLINE"),
CmdOption::new("Spline", "SPLINE"),
],
InputStage::Increment | InputStage::Tolerance => Vec::new(),
}
}
fn on_point(&mut self, point: DVec3) -> CmdResult {
self.last_cursor = Some(point);
if self.stage != InputStage::Drawing {
return CmdResult::NeedPoint;
}
if self.erasing {
self.erasing = false;
self.erase_engaged = false;
} else if self.pen_down {
self.finish_stroke(point);
} else {
// Lower the pen: start a new stroke seeded with this point.
self.strokes.push(vec![pt]);
self.pen_down = true;
self.begin_stroke(point);
}
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
let entities = self.build_all();
match entities.len() {
0 => CmdResult::Cancel,
1 => CmdResult::CommitAndExit(entities.into_iter().next().unwrap()),
_ => CmdResult::ReplaceMany(vec![], entities),
if self.stage != InputStage::Drawing {
self.stage = InputStage::Drawing;
return CmdResult::NeedPoint;
}
let entities = self.take_entities();
if entities.is_empty() {
CmdResult::Cancel
} else {
CmdResult::CommitEntitiesAndExit(entities)
}
}
fn on_escape(&mut self) -> CmdResult {
self.strokes.clear();
CmdResult::Cancel
}
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
if !self.pen_down {
return None;
fn wants_text_input(&self) -> bool {
true
}
// The current stroke is the last one pushed while the pen is down.
let stroke = self.strokes.last_mut()?;
// Record the sample only once it has moved past the threshold from the
// last recorded point, so the polyline isn't flooded with near-duplicate
// vertices.
let record = match stroke.last() {
Some(last) => last.distance(pt) > SAMPLE_EPSILON,
None => true,
};
if record {
stroke.push(pt);
fn point_step_accepts_keywords(&self) -> bool {
self.stage == InputStage::Drawing
}
// Preview the current stroke, including the (possibly unrecorded) cursor
// position so the line tracks the pointer smoothly.
let mut pts: Vec<[f64; 3]> = stroke.iter().map(|p| [p.x, p.y, p.z]).collect();
if !record {
pts.push([pt.x, pt.y, pt.z]);
}
if pts.len() < 2 {
return None;
}
Some(WireModel::solid_f64(
"rubber_band".to_string(),
pts,
WireModel::CYAN,
false,
fn sketch_settings(&self) -> Option<(i16, f64, f64)> {
Some((
self.sketch_type.header_value(),
self.increment,
self.tolerance,
))
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let input = text.trim();
if input.is_empty() {
return None;
}
match self.stage {
InputStage::Type => {
if let Some(sketch_type) = SketchType::parse(input) {
self.sketch_type = sketch_type;
self.stage = InputStage::Drawing;
}
Some(CmdResult::NeedPoint)
}
InputStage::Increment => {
if let Ok(value) = input.parse::<f64>() {
if value.is_finite() && value > 0.0 {
self.increment = value.max(MIN_INCREMENT);
self.stage = InputStage::Drawing;
}
}
Some(CmdResult::NeedPoint)
}
InputStage::Tolerance => {
if let Ok(value) = input.parse::<f64>() {
if value.is_finite() && (0.0..=1.0).contains(&value) {
self.tolerance = value;
self.stage = InputStage::Drawing;
}
}
Some(CmdResult::NeedPoint)
}
InputStage::Drawing => {
let keyword = input.to_ascii_uppercase();
let result = match keyword.as_str() {
"P" | "PEN" => {
self.toggle_pen();
CmdResult::NeedPoint
}
"T" | "TYPE" => {
self.lift_pen();
self.erasing = false;
self.stage = InputStage::Type;
CmdResult::NeedPoint
}
"I" | "INCREMENT" => {
self.lift_pen();
self.erasing = false;
self.stage = InputStage::Increment;
CmdResult::NeedPoint
}
"TO" | "TOLERANCE" if self.sketch_type == SketchType::Spline => {
self.lift_pen();
self.erasing = false;
self.stage = InputStage::Tolerance;
CmdResult::NeedPoint
}
"R" | "RECORD" => {
let entities = self.take_entities();
if entities.is_empty() {
CmdResult::NeedPoint
} else {
CmdResult::CommitEntities(entities)
}
}
"E" | "ERASE" => {
self.lift_pen();
self.erasing = !self.erasing;
self.erase_engaged = false;
CmdResult::NeedPoint
}
"C" | "CONNECT" => {
self.connect();
CmdResult::NeedPoint
}
"X" | "EXIT" => {
let entities = self.take_entities();
if entities.is_empty() {
CmdResult::Cancel
} else {
CmdResult::CommitEntitiesAndExit(entities)
}
}
"Q" | "QUIT" => {
self.strokes.clear();
CmdResult::Cancel
}
_ => {
if let Some(sketch_type) = SketchType::parse(&keyword) {
self.sketch_type = sketch_type;
}
CmdResult::NeedPoint
}
};
Some(result)
}
}
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["SKETCH"] }); // SketchCommand
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
self.last_cursor = Some(point);
if self.stage == InputStage::Drawing {
if self.pen_down {
self.sample_to(point);
} else if self.erasing {
self.erase_at(point);
}
}
self.preview(Some(point))
}
}
fn valid_increment(value: f64) -> f64 {
if value.is_finite() && value > 0.0 {
value.max(MIN_INCREMENT)
} else {
0.1
}
}
fn valid_tolerance(value: f64) -> f64 {
if value.is_finite() && (0.0..=1.0).contains(&value) {
value
} else {
0.5
}
}
fn to_vector3(point: DVec3) -> Vector3 {
Vector3::new(point.x, point.y, point.z)
}
inventory::submit!(crate::command::CommandRegistration { names: &["SKETCH"] });