forked from 1b-agent/opencadstudio-upstream
Merge origin/main into codex/extrude-taper-history; kernel patch ef96a46
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
4069d70863
35 files changed
+1140
-170
No files matched your search
Generated
+4
-4
@@ -4205,7 +4205,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "opencadcodec"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=c9ffcac#c9ffcac5ecdc7a504cc4a7e3ecb52ac9d587c8bf"
|
||||
source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=8189f83#8189f833574c544fc57a1d773591014c3a5907c9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.13.1",
|
||||
@@ -4228,7 +4228,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "opencadgraph"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/HakanSeven12/opencadgraph.git?rev=7f6aae7#7f6aae7aff6b7de87cc1dcdb38a06c3e64ae1922"
|
||||
source = "git+https://github.com/HakanSeven12/opencadgraph.git?rev=75c30c2#75c30c294299c5a754a9a8e36d19609af758dc9b"
|
||||
dependencies = [
|
||||
"opencadkernel",
|
||||
"serde_json",
|
||||
@@ -4237,7 +4237,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "opencadkernel"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/ramox81/cadkernel.git?rev=3d0f631#3d0f631075e4d7ab37377ab257dbfdc29583baaa"
|
||||
source = "git+https://github.com/ramox81/cadkernel.git?rev=ef96a46#ef96a46c12db6724a4351813815e4add9b78be82"
|
||||
dependencies = [
|
||||
"cavalier_contours",
|
||||
"opencadcodec",
|
||||
@@ -4248,7 +4248,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "opencadkernel-constraints"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/ramox81/cadkernel.git?rev=3d0f631#3d0f631075e4d7ab37377ab257dbfdc29583baaa"
|
||||
source = "git+https://github.com/ramox81/cadkernel.git?rev=ef96a46#ef96a46c12db6724a4351813815e4add9b78be82"
|
||||
dependencies = [
|
||||
"nalgebra",
|
||||
]
|
||||
|
||||
+6
-6
@@ -34,10 +34,10 @@ rfd = "0.17"
|
||||
clap = { version = "4", features = ["derive", "string"] }
|
||||
env_logger = "0.11"
|
||||
log = "0.4"
|
||||
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "c9ffcac", features = ["serde"] }
|
||||
kernel = { package = "opencadkernel", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "99b6b48", features = ["acis", "offset"] }
|
||||
kernel-constraints = { package = "opencadkernel-constraints", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "99b6b48" }
|
||||
graph = { package = "opencadgraph", git = "https://github.com/HakanSeven12/opencadgraph.git", rev = "7f6aae7" }
|
||||
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "8189f83", features = ["serde"] }
|
||||
kernel = { package = "opencadkernel", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "0764f79", features = ["acis", "offset"] }
|
||||
kernel-constraints = { package = "opencadkernel-constraints", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "0764f79" }
|
||||
graph = { package = "opencadgraph", git = "https://github.com/HakanSeven12/opencadgraph.git", rev = "75c30c2" }
|
||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||
flate2 = "1"
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||
@@ -183,5 +183,5 @@ name = "integration"
|
||||
path = "tests/integration.rs"
|
||||
|
||||
[patch."https://github.com/HakanSeven12/opencadkernel.git"]
|
||||
opencadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "3d0f631" }
|
||||
opencadkernel-constraints = { git = "https://github.com/ramox81/cadkernel.git", rev = "3d0f631" }
|
||||
opencadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "ef96a46" }
|
||||
opencadkernel-constraints = { git = "https://github.com/ramox81/cadkernel.git", rev = "ef96a46" }
|
||||
@@ -15,7 +15,7 @@ serde_json = { version = "1", optional = true }
|
||||
# Pulled in only by the `host` feature, which adds the `opencadcodec`-typed
|
||||
# `HostApi` runtime surface. The default crate stays dependency-free so engine
|
||||
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
|
||||
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "c9ffcac", optional = true, features = ["serde"] }
|
||||
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "8189f83", optional = true, features = ["serde"] }
|
||||
|
||||
# Runtime IPC and serialization (host feature only).
|
||||
interprocess = { version = "2", optional = true }
|
||||
@@ -39,7 +39,7 @@ serde_json = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
cargo-lock = "11"
|
||||
# opencadcodec is scanned at build time to generate the embedded type registry.
|
||||
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "c9ffcac", features = ["serde"] }
|
||||
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "8189f83", features = ["serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1"
|
||||
@@ -8,7 +8,7 @@ publish = false
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "c9ffcac", features = ["serde"] }
|
||||
codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "8189f83", features = ["serde"] }
|
||||
bincode = "1.3"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
console_error_panic_hook = "0.1"
|
||||
|
||||
@@ -2521,6 +2521,12 @@ inquiry =
|
||||
.id-specify-point = ID حدد نقطة:
|
||||
.list-no-entities-selected-select-entities-firs = LIST: لم تُحدد كيانات. حدد الكيانات أولًا.
|
||||
.massprop-no-entities-selected-select-entities = MASSPROP: لم تُحدد كيانات. حدد الكيانات أولًا.
|
||||
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ لا يحتوي على جسم مغلق لقياسه.
|
||||
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ الحجم=__ocs_fmt_1__ مركز الثقل=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||
.moments-of-inertia-x-4-y-4-z-4 = عزوم القصور الذاتي X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.products-of-inertia-xy-4-yz-4-zx-4 = جداءات القصور الذاتي XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||
.radii-of-gyration-x-4-y-4-z-4 = أنصاف أقطار الدوران X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.principal-moments-about-centroid-i-4-j-4-k-4 = العزوم الرئيسية حول مركز الثقل I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||
.measure-select-object-to-measure = MEASURE حدد الكائن المراد قياسه:
|
||||
.measure-specify-segment-length = MEASURE حدد طول المقطع:
|
||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM أدخل خيارًا [مسافة/نصف قطر/زاوية/مساحة]:
|
||||
|
||||
@@ -2508,6 +2508,12 @@ inquiry =
|
||||
.id-specify-point = ID Посочи точка:
|
||||
.list-no-entities-selected-select-entities-firs = LIST: не са избрани обекти. Първо избери обекти.
|
||||
.massprop-no-entities-selected-select-entities = MASSPROP: не са избрани обекти. Първо избери обекти.
|
||||
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ няма затворено тяло за измерване.
|
||||
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Обем=__ocs_fmt_1__ Център на тежестта=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||
.moments-of-inertia-x-4-y-4-z-4 = Инерционни моменти X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.products-of-inertia-xy-4-yz-4-zx-4 = Центробежни моменти XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||
.radii-of-gyration-x-4-y-4-z-4 = Радиуси на инерция X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.principal-moments-about-centroid-i-4-j-4-k-4 = Главни моменти спрямо центъра на тежестта I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||
.measure-select-object-to-measure = MEASURE Избери обект за измерване:
|
||||
.measure-specify-segment-length = MEASURE Посочи дължина на сегмент:
|
||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Въведи опция [Разстояние/Радиус/Ъгъл/Площ]:
|
||||
|
||||
@@ -2508,6 +2508,12 @@ inquiry =
|
||||
.id-specify-point = ID Určete bod:
|
||||
.list-no-entities-selected-select-entities-firs = LIST: nebyly vybrány žádné entity. Nejprve vyberte entity.
|
||||
.massprop-no-entities-selected-select-entities = MASSPROP: nebyly vybrány žádné entity. Nejprve vyberte entity.
|
||||
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ nemá žádné uzavřené těleso k měření.
|
||||
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Objem=__ocs_fmt_1__ Těžiště=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||
.moments-of-inertia-x-4-y-4-z-4 = Momenty setrvačnosti X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.products-of-inertia-xy-4-yz-4-zx-4 = Deviační momenty XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||
.radii-of-gyration-x-4-y-4-z-4 = Poloměry setrvačnosti X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.principal-moments-about-centroid-i-4-j-4-k-4 = Hlavní momenty k těžišti I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||
.measure-select-object-to-measure = MEASURE Vyberte objekt k měření:
|
||||
.measure-specify-segment-length = MEASURE Zadejte délku segmentu:
|
||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter možnost [Vzdálenost/Poloměr/Úhel/Area]:
|
||||
|
||||
@@ -2509,6 +2509,12 @@ inquiry =
|
||||
.id-specify-point = ID Anzugeben ist:
|
||||
.list-no-entities-selected-select-entities-firs = LIST: keine Objekte ausgewählt.
|
||||
.massprop-no-entities-selected-select-entities = MASSPROP: Keine Objekte ausgewählt.
|
||||
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ hat keinen geschlossenen Körper zum Messen.
|
||||
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Volumen=__ocs_fmt_1__ Schwerpunkt=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||
.moments-of-inertia-x-4-y-4-z-4 = Trägheitsmomente X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.products-of-inertia-xy-4-yz-4-zx-4 = Deviationsmomente XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||
.radii-of-gyration-x-4-y-4-z-4 = Trägheitsradien X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.principal-moments-about-centroid-i-4-j-4-k-4 = Hauptträgheitsmomente um den Schwerpunkt I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||
.measure-select-object-to-measure = MEASURE Objekt auswählen, um zu messen:
|
||||
.measure-specify-segment-length = MEASURE Segmentlänge angeben:
|
||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Geben Sie eine Option ein [Distance/Radius/Angle/ARea]:
|
||||
|
||||
@@ -2508,6 +2508,12 @@ inquiry =
|
||||
.id-specify-point = ID Ορίστε σημείο:
|
||||
.list-no-entities-selected-select-entities-firs = LIST: δεν έχουν επιλεγεί οντότητες. Επιλέξτε πρώτα οντότητες.
|
||||
.massprop-no-entities-selected-select-entities = MASSPROP: δεν έχουν επιλεγεί οντότητες. Επιλέξτε πρώτα οντότητες.
|
||||
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ δεν έχει κλειστό σώμα για μέτρηση.
|
||||
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Όγκος=__ocs_fmt_1__ Κέντρο βάρους=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||
.moments-of-inertia-x-4-y-4-z-4 = Ροπές αδράνειας X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.products-of-inertia-xy-4-yz-4-zx-4 = Γινόμενα αδράνειας XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||
.radii-of-gyration-x-4-y-4-z-4 = Ακτίνες αδράνειας X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.principal-moments-about-centroid-i-4-j-4-k-4 = Κύριες ροπές ως προς το κέντρο βάρους I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||
.measure-select-object-to-measure = MEASURE Επιλέξτε αντικείμενο προς μέτρηση:
|
||||
.measure-specify-segment-length = MEASURE Ορίστε μήκος τμήματος:
|
||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Δώστε επιλογή [Απόσταση/Ακτίνα/Γωνία/Εμβαδόν]:
|
||||
|
||||
@@ -2509,6 +2509,12 @@ inquiry =
|
||||
.id-specify-point = ID Specify point:
|
||||
.list-no-entities-selected-select-entities-firs = LIST: no entities selected. Select entities first.
|
||||
.massprop-no-entities-selected-select-entities = MASSPROP: no entities selected. Select entities first.
|
||||
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ has no closed body to measure.
|
||||
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Volume=__ocs_fmt_1__ Centroid=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||
.moments-of-inertia-x-4-y-4-z-4 = Moments of inertia X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.products-of-inertia-xy-4-yz-4-zx-4 = Products of inertia XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||
.radii-of-gyration-x-4-y-4-z-4 = Radii of gyration X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.principal-moments-about-centroid-i-4-j-4-k-4 = Principal moments about centroid I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||
.measure-select-object-to-measure = MEASURE Select object to measure:
|
||||
.measure-specify-segment-length = MEASURE Specify segment length:
|
||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter an option [Distance/Radius/Angle/ARea]:
|
||||
|
||||
@@ -2509,6 +2509,12 @@ inquiry =
|
||||
.id-specify-point = ID Especifique un punto:
|
||||
.list-no-entities-selected-select-entities-firs = LIST: no hay entidades seleccionadas. Seleccione primero las entidades.
|
||||
.massprop-no-entities-selected-select-entities = MASSPROP: no hay entidades seleccionadas. Seleccione primero las entidades.
|
||||
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ no tiene un cuerpo cerrado que medir.
|
||||
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Volumen=__ocs_fmt_1__ Centroide=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||
.moments-of-inertia-x-4-y-4-z-4 = Momentos de inercia X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.products-of-inertia-xy-4-yz-4-zx-4 = Productos de inercia XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||
.radii-of-gyration-x-4-y-4-z-4 = Radios de giro X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.principal-moments-about-centroid-i-4-j-4-k-4 = Momentos principales respecto al centroide I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||
.measure-select-object-to-measure = MEASURE Seleccione el objeto que desea medir:
|
||||
.measure-specify-segment-length = MEASURE Especifique la longitud del segmento:
|
||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Introduzca una opción [Distancia/Radio/Ángulo/Área]:
|
||||
|
||||
@@ -2508,6 +2508,12 @@ inquiry =
|
||||
.id-specify-point = ID Määritä piste:
|
||||
.list-no-entities-selected-select-entities-firs = LIST: entiteettejä ei ole valittu. Valitse ensin entiteetit.
|
||||
.massprop-no-entities-selected-select-entities = MASSPROP: entiteettejä ei ole valittu. Valitse ensin entiteetit.
|
||||
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ ei sisällä suljettua kappaletta mitattavaksi.
|
||||
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Tilavuus=__ocs_fmt_1__ Painopiste=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||
.moments-of-inertia-x-4-y-4-z-4 = Hitausmomentit X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.products-of-inertia-xy-4-yz-4-zx-4 = Tulohitausmomentit XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||
.radii-of-gyration-x-4-y-4-z-4 = Hitaussäteet X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.principal-moments-about-centroid-i-4-j-4-k-4 = Päähitausmomentit painopisteen suhteen I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||
.measure-select-object-to-measure = MEASURE Valitse mitattava kohde:
|
||||
.measure-specify-segment-length = MEASURE Määritä segmentin pituus:
|
||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter vaihtoehto [etäisyys/säde/kulma/pinta-ala]:
|
||||
|
||||
@@ -2509,6 +2509,12 @@ inquiry =
|
||||
.id-specify-point = ID Spécifier le point:
|
||||
.list-no-entities-selected-select-entities-firs = LIST : aucune entité sélectionnée. Sélectionnez d'abord les entités.
|
||||
.massprop-no-entities-selected-select-entities = MASSPROP : aucune entité sélectionnée. Sélectionnez d'abord les entités.
|
||||
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ n'a aucun corps fermé à mesurer.
|
||||
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Volume=__ocs_fmt_1__ Centre de gravité=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||
.moments-of-inertia-x-4-y-4-z-4 = Moments d'inertie X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.products-of-inertia-xy-4-yz-4-zx-4 = Produits d'inertie XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||
.radii-of-gyration-x-4-y-4-z-4 = Rayons de giration X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.principal-moments-about-centroid-i-4-j-4-k-4 = Moments principaux au centre de gravité I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||
.measure-select-object-to-measure = MEASURE Sélectionnez l'objet à mesurer :
|
||||
.measure-specify-segment-length = MEASURE Spécifier la longueur du segment:
|
||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Saisissez une option [Distance/Radius/Angle/ARea] :
|
||||
|
||||
@@ -2497,6 +2497,12 @@ inquiry =
|
||||
.id-specify-point = ID निर्दिष्ट बिंदु:
|
||||
.list-no-entities-selected-select-entities-firs = LIST: कोई ऑब्जेक्ट नहीं चुना गया। पहले ऑब्जेक्ट चुनें।
|
||||
.massprop-no-entities-selected-select-entities = MASSPROP: कोई ऑब्जेक्ट नहीं चुना गया। पहले ऑब्जेक्ट चुनें।
|
||||
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ में मापने के लिए कोई बंद पिंड नहीं है।
|
||||
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ आयतन=__ocs_fmt_1__ केन्द्रक=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||
.moments-of-inertia-x-4-y-4-z-4 = जड़त्व आघूर्ण X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.products-of-inertia-xy-4-yz-4-zx-4 = जड़त्व गुणनफल XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||
.radii-of-gyration-x-4-y-4-z-4 = घूर्णन त्रिज्याएँ X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.principal-moments-about-centroid-i-4-j-4-k-4 = केन्द्रक के सापेक्ष मुख्य आघूर्ण I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||
.measure-select-object-to-measure = MEASURE ऑब्जेक्ट को मापने के लिए चुनें:
|
||||
.measure-specify-segment-length = MEASURE सेगमेंट की लंबाई निर्दिष्ट करें:
|
||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM विकल्प दर्ज करें [दूरी / त्रिज्या / कोण / क्षेत्रफल]:
|
||||
|
||||
@@ -2508,6 +2508,12 @@ inquiry =
|
||||
.id-specify-point = ID Pont megadása:
|
||||
.list-no-entities-selected-select-entities-firs = LIST: nincsenek kiválasztva entitások. Először válassza ki az entitásokat.
|
||||
.massprop-no-entities-selected-select-entities = MASSPROP: nincsenek kiválasztva entitások. Először válassza ki az entitásokat.
|
||||
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ nem tartalmaz mérhető zárt testet.
|
||||
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Térfogat=__ocs_fmt_1__ Súlypont=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||
.moments-of-inertia-x-4-y-4-z-4 = Tehetetlenségi nyomatékok X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.products-of-inertia-xy-4-yz-4-zx-4 = Deviációs nyomatékok XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||
.radii-of-gyration-x-4-y-4-z-4 = Tehetetlenségi sugarak X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.principal-moments-about-centroid-i-4-j-4-k-4 = Fő tehetetlenségi nyomatékok a súlypontra I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||
.measure-select-object-to-measure = MEASURE Válassza ki a mérendő objektumot:
|
||||
.measure-specify-segment-length = MEASURE Adja meg a szegmens hosszát:
|
||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter egy opció [Távolság / Sugár / Szög / Terület]:
|
||||
|
||||
@@ -2508,6 +2508,12 @@ inquiry =
|
||||
.id-specify-point = ID Specificare il punto:
|
||||
.list-no-entities-selected-select-entities-firs = LIST: nessuna entità selezionata. Seleziona prima le entità.
|
||||
.massprop-no-entities-selected-select-entities = MASSPROP: nessuna entità selezionata. Seleziona prima le entità.
|
||||
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ non ha un corpo chiuso da misurare.
|
||||
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Volume=__ocs_fmt_1__ Baricentro=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||
.moments-of-inertia-x-4-y-4-z-4 = Momenti d'inerzia X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.products-of-inertia-xy-4-yz-4-zx-4 = Prodotti d'inerzia XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||
.radii-of-gyration-x-4-y-4-z-4 = Raggi di girazione X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.principal-moments-about-centroid-i-4-j-4-k-4 = Momenti principali rispetto al baricentro I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||
.measure-select-object-to-measure = MEASURE Selezionare l'oggetto da misurare:
|
||||
.measure-specify-segment-length = MEASURE Specificare la lunghezza del segmento:
|
||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter un'opzione [Distanza/Raggio/Angolo/ARea]:
|
||||
|
||||
@@ -2499,6 +2499,12 @@ inquiry =
|
||||
.id-specify-point = ID 点を指定:
|
||||
.list-no-entities-selected-select-entities-firs = LIST: 図形が選択されていません。先に選択してください。
|
||||
.massprop-no-entities-selected-select-entities = MASSPROP: 図形が選択されていません。先に選択してください。
|
||||
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ には計測できる閉じたボディがありません。
|
||||
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ 体積=__ocs_fmt_1__ 重心=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||
.moments-of-inertia-x-4-y-4-z-4 = 慣性モーメント X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.products-of-inertia-xy-4-yz-4-zx-4 = 慣性乗積 XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||
.radii-of-gyration-x-4-y-4-z-4 = 回転半径 X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.principal-moments-about-centroid-i-4-j-4-k-4 = 重心まわりの主慣性モーメント I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||
.measure-select-object-to-measure = MEASURE 計測するオブジェクトを選択:
|
||||
.measure-specify-segment-length = MEASURE 区間長を指定:
|
||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM オプションを入力 [距離/半径/角度/面積]:
|
||||
|
||||
@@ -2508,6 +2508,12 @@ inquiry =
|
||||
.id-specify-point = ID 지점 지정:
|
||||
.list-no-entities-selected-select-entities-firs = LIST: 선택된 엔터티가 없습니다. 먼저 엔터티를 선택하세요.
|
||||
.massprop-no-entities-selected-select-entities = MASSPROP: 선택된 엔터티가 없습니다. 먼저 엔터티를 선택하세요.
|
||||
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ 에 측정할 닫힌 바디가 없습니다.
|
||||
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ 부피=__ocs_fmt_1__ 도심=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||
.moments-of-inertia-x-4-y-4-z-4 = 관성 모멘트 X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.products-of-inertia-xy-4-yz-4-zx-4 = 관성 곱 XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||
.radii-of-gyration-x-4-y-4-z-4 = 회전 반경 X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.principal-moments-about-centroid-i-4-j-4-k-4 = 도심에 대한 주 모멘트 I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||
.measure-select-object-to-measure = MEASURE 측정할 물체 선택:
|
||||
.measure-specify-segment-length = MEASURE 세그먼트 길이 지정:
|
||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter 옵션 [거리/반경/각도/지역]:
|
||||
|
||||
@@ -2509,6 +2509,12 @@ inquiry =
|
||||
.id-specify-point = ID ກຳນົດຈຸດ:
|
||||
.list-no-entities-selected-select-entities-firs = LIST: ບໍ່ມີອົງປະກອບຖືກເລືອກ. ກະລຸນາເລືອກອົງປະກອບກ່ອນ.
|
||||
.massprop-no-entities-selected-select-entities = MASSPROP: ບໍ່ມີອົງປະກອບຖືກເລືອກ. ກະລຸນາເລືອກອົງປະກອບກ່ອນ.
|
||||
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ ບໍ່ມີຕົວປິດໃຫ້ວັດແທກ.
|
||||
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ ປະລິມາດ=__ocs_fmt_1__ ຈຸດສູນກາງມວນ=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||
.moments-of-inertia-x-4-y-4-z-4 = ໂມເມັນຄວາມເສື່ອຍ X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.products-of-inertia-xy-4-yz-4-zx-4 = ຜົນຄູນຄວາມເສື່ອຍ XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||
.radii-of-gyration-x-4-y-4-z-4 = ລັດສະໝີການໝູນ X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.principal-moments-about-centroid-i-4-j-4-k-4 = ໂມເມັນຫຼັກອ້ອມຈຸດສູນກາງມວນ I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||
.measure-select-object-to-measure = MEASURE ເລືອກວັດຖຸທີ່ຈະວັດແທກ:
|
||||
.measure-specify-segment-length = MEASURE ກຳນົດຄວາມຍາວຂອງສ່ວນແບ່ງ:
|
||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM ປ້ອນຕົວເລືອກ [Distance/Radius/Angle/ARea]:
|
||||
|
||||
@@ -2509,6 +2509,12 @@ inquiry =
|
||||
.id-specify-point = ID Specificeer punt:
|
||||
.list-no-entities-selected-select-entities-firs = LIST: geen objecten geselecteerd. Selecteer eerst objecten.
|
||||
.massprop-no-entities-selected-select-entities = MASSPROP: geen objecten geselecteerd. Selecteer eerst objecten.
|
||||
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ heeft geen gesloten lichaam om te meten.
|
||||
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Volume=__ocs_fmt_1__ Zwaartepunt=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||
.moments-of-inertia-x-4-y-4-z-4 = Traagheidsmomenten X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.products-of-inertia-xy-4-yz-4-zx-4 = Deviatiemomenten XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||
.radii-of-gyration-x-4-y-4-z-4 = Gyratiestralen X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.principal-moments-about-centroid-i-4-j-4-k-4 = Hoofdtraagheidsmomenten om het zwaartepunt I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||
.measure-select-object-to-measure = MEASURE Selecteer object om mee te meten:
|
||||
.measure-specify-segment-length = MEASURE segmentlengte specificeren:
|
||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Voer een optie in [Afstand / Straal / Hoek / Oppervlakte]:
|
||||
|
||||
@@ -2508,6 +2508,12 @@ inquiry =
|
||||
.id-specify-point = ID Określ punkt:
|
||||
.list-no-entities-selected-select-entities-firs = LIST: nie wybrano żadnych elementów. Najpierw wybierz elementy.
|
||||
.massprop-no-entities-selected-select-entities = MASSPROP: nie wybrano żadnych elementów. Najpierw wybierz elementy.
|
||||
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ nie ma zamkniętej bryły do zmierzenia.
|
||||
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Objętość=__ocs_fmt_1__ Środek ciężkości=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||
.moments-of-inertia-x-4-y-4-z-4 = Momenty bezwładności X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.products-of-inertia-xy-4-yz-4-zx-4 = Momenty dewiacyjne XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||
.radii-of-gyration-x-4-y-4-z-4 = Promienie bezwładności X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.principal-moments-about-centroid-i-4-j-4-k-4 = Główne momenty względem środka ciężkości I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||
.measure-select-object-to-measure = MEASURE Wybierz obiekt do pomiaru:
|
||||
.measure-specify-segment-length = MEASURE Określ długość segmentu:
|
||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Enter opcja [Odległość/Promień/Kąt/Powierzchnia]:
|
||||
|
||||
@@ -2509,6 +2509,12 @@ inquiry =
|
||||
.id-specify-point = ID Especifique o ponto:
|
||||
.list-no-entities-selected-select-entities-firs = LIST: nenhuma entidade selecionada. Selecione primeiro as entidades.
|
||||
.massprop-no-entities-selected-select-entities = MASSPROP: nenhuma entidade selecionada. Selecione primeiro as entidades.
|
||||
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ não tem um corpo fechado para medir.
|
||||
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Volume=__ocs_fmt_1__ Centroide=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||
.moments-of-inertia-x-4-y-4-z-4 = Momentos de inércia X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.products-of-inertia-xy-4-yz-4-zx-4 = Produtos de inércia XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||
.radii-of-gyration-x-4-y-4-z-4 = Raios de giração X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.principal-moments-about-centroid-i-4-j-4-k-4 = Momentos principais em relação ao centroide I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||
.measure-select-object-to-measure = MEASURE Selecione o objeto a medir:
|
||||
.measure-specify-segment-length = MEASURE Especifique o comprimento do segmento:
|
||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Insira uma opção [Distância/Raio/Ângulo/ÁRea]:
|
||||
|
||||
@@ -2512,6 +2512,12 @@ inquiry =
|
||||
.id-specify-point = ID Укажите точку:
|
||||
.list-no-entities-selected-select-entities-firs = LIST: не выбрано ни одного объекта.
|
||||
.massprop-no-entities-selected-select-entities = MASSPROP: не выбрано ни одного объекта.
|
||||
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ не имеет замкнутого тела для измерения.
|
||||
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Объём=__ocs_fmt_1__ Центр масс=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||
.moments-of-inertia-x-4-y-4-z-4 = Моменты инерции X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.products-of-inertia-xy-4-yz-4-zx-4 = Центробежные моменты XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||
.radii-of-gyration-x-4-y-4-z-4 = Радиусы инерции X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.principal-moments-about-centroid-i-4-j-4-k-4 = Главные моменты относительно центра масс I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||
.measure-select-object-to-measure = MEASURE Выберите объект для измерения:
|
||||
.measure-specify-segment-length = MEASURE Укажите длину сегмента:
|
||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Введите опцию [Расстояние/Радиус/Англия/Арея]:
|
||||
|
||||
@@ -2490,6 +2490,12 @@ inquiry =
|
||||
.id-specify-point = ID Noktayı belirtin:
|
||||
.list-no-entities-selected-select-entities-firs = LIST: Nesne seçilmedi. Önce nesneleri seçin.
|
||||
.massprop-no-entities-selected-select-entities = MASSPROP: Nesne seçilmedi. Önce nesneleri seçin.
|
||||
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ için ölçülecek kapalı gövde yok.
|
||||
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ Hacim=__ocs_fmt_1__ Ağırlık merkezi=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||
.moments-of-inertia-x-4-y-4-z-4 = Eylemsizlik momentleri X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.products-of-inertia-xy-4-yz-4-zx-4 = Eylemsizlik çarpımları XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||
.radii-of-gyration-x-4-y-4-z-4 = Jirasyon yarıçapları X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.principal-moments-about-centroid-i-4-j-4-k-4 = Ağırlık merkezine göre asal momentler I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||
.measure-select-object-to-measure = MEASURE Ölçülecek nesneyi seçin:
|
||||
.measure-specify-segment-length = MEASURE Parça uzunluğunu belirtin:
|
||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM Bir seçenek girin [Mesafe/Yarıçap/Açı/Alan]:
|
||||
|
||||
@@ -2496,6 +2496,12 @@ inquiry =
|
||||
.id-specify-point = ID 指定点:
|
||||
.list-no-entities-selected-select-entities-firs = LIST:没有选择实体。首先选择实体。
|
||||
.massprop-no-entities-selected-select-entities = MASSPROP:没有选择实体。首先选择实体。
|
||||
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ 没有可测量的封闭实体。
|
||||
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ 体积=__ocs_fmt_1__ 质心=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||
.moments-of-inertia-x-4-y-4-z-4 = 惯性矩 X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.products-of-inertia-xy-4-yz-4-zx-4 = 惯性积 XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||
.radii-of-gyration-x-4-y-4-z-4 = 回转半径 X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.principal-moments-about-centroid-i-4-j-4-k-4 = 关于质心的主力矩 I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||
.measure-select-object-to-measure = MEASURE 选择要定距等分的对象:
|
||||
.measure-specify-segment-length = MEASURE 指定线段长度:
|
||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM 输入选项 [距离/半径/角度/面积]:
|
||||
|
||||
@@ -2508,6 +2508,12 @@ inquiry =
|
||||
.id-specify-point = ID 指定點:
|
||||
.list-no-entities-selected-select-entities-firs = LIST:沒有選擇實體。首先選擇實體。
|
||||
.massprop-no-entities-selected-select-entities = MASSPROP:沒有選擇實體。首先選擇實體。
|
||||
.massprop-has-no-closed-body-to-measure = MASSPROP: __ocs_fmt_0__ 沒有可量測的封閉實體。
|
||||
.volume-4-centroid-4-4-4 = __ocs_fmt_0__ 體積=__ocs_fmt_1__ 質心=(__ocs_fmt_2__,__ocs_fmt_3__,__ocs_fmt_4__)
|
||||
.moments-of-inertia-x-4-y-4-z-4 = 慣性矩 X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.products-of-inertia-xy-4-yz-4-zx-4 = 慣性積 XY=__ocs_fmt_0__ YZ=__ocs_fmt_1__ ZX=__ocs_fmt_2__
|
||||
.radii-of-gyration-x-4-y-4-z-4 = 迴轉半徑 X=__ocs_fmt_0__ Y=__ocs_fmt_1__ Z=__ocs_fmt_2__
|
||||
.principal-moments-about-centroid-i-4-j-4-k-4 = 關於質心的主力矩 I=__ocs_fmt_0__ J=__ocs_fmt_1__ K=__ocs_fmt_2__
|
||||
.measure-select-object-to-measure = MEASURE 選擇要定距等分的物件:
|
||||
.measure-specify-segment-length = MEASURE 指定線段長度:
|
||||
.measuregeom-enter-an-option-distance-radius-an = MEASUREGEOM 輸入選項 [距離/半徑/角度/面積]:
|
||||
|
||||
Generated
+1
-1
@@ -1264,7 +1264,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "opencadcodec"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=c9ffcac#c9ffcac5ecdc7a504cc4a7e3ecb52ac9d587c8bf"
|
||||
source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=8189f83#8189f833574c544fc57a1d773591014c3a5907c9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
|
||||
@@ -1114,6 +1114,63 @@ impl OpenCADStudio {
|
||||
for (handle, _) in &selected {
|
||||
if let Some(entity) = self.tabs[i].scene.document.get_entity(*handle) {
|
||||
use crate::entities::traits::EntityTypeOps;
|
||||
// A solid's properties come from its B-rep: exact
|
||||
// where the kernel knows the shape, a fine mesh
|
||||
// otherwise — not the display mesh.
|
||||
let acis = match entity {
|
||||
codec::EntityType::Solid3D(solid) => Some(&solid.acis_data),
|
||||
codec::EntityType::Body(body) => Some(&body.acis_data),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(acis) = acis {
|
||||
let properties = crate::scene::convert::solid3d_tess::kernel_acis_body(acis)
|
||||
.and_then(|body| kernel::brep::mass_properties(&body));
|
||||
let Some(p) = properties else {
|
||||
self.command_line.push_error(crate::tf!(
|
||||
"MASSPROP: {} has no closed body to measure.",
|
||||
crate::entities::names::dxf_name(entity),
|
||||
).as_ref());
|
||||
continue;
|
||||
};
|
||||
let [x, y, z] = p.centroid;
|
||||
let [ix, iy, iz] = p.moment_of_inertia;
|
||||
let [pxy, pyz, pzx] = p.product_of_inertia;
|
||||
let [rx, ry, rz] = p.radii_of_gyration;
|
||||
let [i1, i2, i3] = p.principal_moments;
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"{} Volume={:.4} Centroid=({:.4},{:.4},{:.4})",
|
||||
crate::entities::names::dxf_name(entity),
|
||||
p.volume,
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
).as_ref());
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"Moments of inertia X={:.4} Y={:.4} Z={:.4}",
|
||||
ix,
|
||||
iy,
|
||||
iz,
|
||||
).as_ref());
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"Products of inertia XY={:.4} YZ={:.4} ZX={:.4}",
|
||||
pxy,
|
||||
pyz,
|
||||
pzx,
|
||||
).as_ref());
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"Radii of gyration X={:.4} Y={:.4} Z={:.4}",
|
||||
rx,
|
||||
ry,
|
||||
rz,
|
||||
).as_ref());
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"Principal moments about centroid I={:.4} J={:.4} K={:.4}",
|
||||
i1,
|
||||
i2,
|
||||
i3,
|
||||
).as_ref());
|
||||
continue;
|
||||
}
|
||||
if let Some(props) = entity.mass_props() {
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"{} Area={:.4} Perimeter={:.4} Centroid=({:.4},{:.4})",
|
||||
|
||||
@@ -1109,6 +1109,14 @@ pub(super) struct OpenCADStudio {
|
||||
/// Snapshot of the dialog's settings taken when it opened, restored by the
|
||||
/// `<previous>` list entry.
|
||||
plot_prev: Option<crate::ui::window::plot::PlotDialogState>,
|
||||
/// Tab/layout that last owned the Plot dialog. Reopening Plot in the same
|
||||
/// context keeps the user's last paper, scale, CTB and output choices
|
||||
/// instead of reapplying the layout page setup over them.
|
||||
plot_prev_context: Option<(usize, String)>,
|
||||
/// Printer whose capability reply is allowed to reconcile the current
|
||||
/// sheet. Set only after an explicit printer/page-setup choice; merely
|
||||
/// opening Plot must never replace the drawing's stored paper size.
|
||||
plot_reconcile_printer_media: Option<String>,
|
||||
/// Full source settings behind the fields currently shown in Plot.
|
||||
plot_setup_template: Option<codec::objects::PlotSettings>,
|
||||
/// Paper layouts shown by Print All, in tab order with their selection.
|
||||
@@ -4400,6 +4408,8 @@ impl OpenCADStudio {
|
||||
plot_orientation: crate::io::paper_catalog::Orientation::Landscape,
|
||||
plot_dialog: crate::ui::window::plot::PlotDialogState::default(),
|
||||
plot_prev: None,
|
||||
plot_prev_context: None,
|
||||
plot_reconcile_printer_media: None,
|
||||
plot_setup_template: None,
|
||||
print_all_layouts: Vec::new(),
|
||||
print_all_options: false,
|
||||
|
||||
+664
-57
@@ -408,10 +408,16 @@ fn plot_content_extents(content: &PlotContent) -> Option<(f64, f64, f64, f64)> {
|
||||
}
|
||||
|
||||
/// Stamp the owning entity's effective color index onto a plot wire so CTB
|
||||
/// plot-style lookups (`wire.aci > 0`) apply: an entity's own ACI 1-255 is
|
||||
/// used as-is, ByLayer resolves through the layer table, and true-color
|
||||
/// objects keep `aci = 0` ("0 means true-color" — no CTB mapping). Wires
|
||||
/// whose owner cannot be resolved keep the index they carry.
|
||||
/// plot-style lookups (`wire.aci > 0`) apply.
|
||||
///
|
||||
/// Direct entities are resolved here from their authored color/layer. Expanded
|
||||
/// block wires are different: `BlockCache` has already resolved their effective
|
||||
/// ACI through ByBlock and layer-0/ByLayer inheritance. Their wire name points
|
||||
/// at the host INSERT rather than the child entity that produced the geometry,
|
||||
/// so resolving that INSERT again would destroy the child's effective ACI.
|
||||
///
|
||||
/// In particular, a true-color INSERT on an indexed-color layer would overwrite
|
||||
/// an inherited child ACI with 0 and bypass the CTB entirely.
|
||||
fn plot_owner_aci(scene: &crate::scene::Scene, wire: &mut crate::scene::WireModel) {
|
||||
let Some(handle) = crate::scene::Scene::handle_from_wire_name(&wire.name) else {
|
||||
return;
|
||||
@@ -419,6 +425,14 @@ fn plot_owner_aci(scene: &crate::scene::Scene, wire: &mut crate::scene::WireMode
|
||||
let Some(entity) = scene.document.get_entity(handle) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Expanded block geometry already carries the effective ACI calculated by
|
||||
// BlockCache. `wire.name` identifies the host INSERT for picking, not the
|
||||
// block child whose color inheritance produced `wire.aci`.
|
||||
if matches!(entity, codec::EntityType::Insert(_)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let common = entity.common();
|
||||
wire.aci = match &common.color {
|
||||
codec::types::Color::Index(index) => *index,
|
||||
@@ -435,6 +449,77 @@ fn plot_owner_aci(scene: &crate::scene::Scene, wire: &mut crate::scene::WireMode
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod plot_owner_aci_tests {
|
||||
use super::plot_owner_aci;
|
||||
|
||||
#[test]
|
||||
fn expanded_block_wire_keeps_its_resolved_effective_aci() {
|
||||
let mut scene = crate::scene::Scene::new();
|
||||
|
||||
// Real-world regression case:
|
||||
// the INSERT itself has explicit true-color white, while its block
|
||||
// children are layer 0 + ByLayer. BlockCache resolves those children
|
||||
// through the INSERT's layer and stores that effective ACI on the wire.
|
||||
let mut insert = codec::entities::Insert::new(
|
||||
"*U264",
|
||||
codec::types::Vector3::ZERO,
|
||||
);
|
||||
insert.common.color = codec::types::Color::Rgb {
|
||||
r: 255,
|
||||
g: 255,
|
||||
b: 255,
|
||||
};
|
||||
|
||||
let insert_handle = scene
|
||||
.document
|
||||
.add_entity(codec::EntityType::Insert(insert))
|
||||
.expect("insert must be added");
|
||||
|
||||
let mut wire = crate::scene::WireModel::default();
|
||||
wire.name = insert_handle.value().to_string();
|
||||
|
||||
// Pretend BlockCache resolved the layer-0 child to indexed color 5.
|
||||
// The plot preparation step must preserve it instead of replacing it
|
||||
// with ACI 0 from the host INSERT's true color.
|
||||
wire.aci = 5;
|
||||
|
||||
plot_owner_aci(&scene, &mut wire);
|
||||
|
||||
assert_eq!(
|
||||
wire.aci, 5,
|
||||
"expanded block geometry must keep the effective ACI already resolved by BlockCache"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_entity_still_refreshes_aci_from_its_owner() {
|
||||
let mut scene = crate::scene::Scene::new();
|
||||
|
||||
let mut line = codec::entities::Line::from_points(
|
||||
codec::types::Vector3::ZERO,
|
||||
codec::types::Vector3::new(1.0, 0.0, 0.0),
|
||||
);
|
||||
line.common.color = codec::types::Color::Index(1);
|
||||
|
||||
let line_handle = scene
|
||||
.document
|
||||
.add_entity(codec::EntityType::Line(line))
|
||||
.expect("line must be added");
|
||||
|
||||
let mut wire = crate::scene::WireModel::default();
|
||||
wire.name = line_handle.value().to_string();
|
||||
wire.aci = 5;
|
||||
|
||||
plot_owner_aci(&scene, &mut wire);
|
||||
|
||||
assert_eq!(
|
||||
wire.aci, 1,
|
||||
"ordinary entity plot wires must still resolve ACI from their owner"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn plot_scene_content(
|
||||
scene: &crate::scene::Scene,
|
||||
paper_space_last: bool,
|
||||
@@ -1710,6 +1795,22 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
/// Drop Plot-dialog state that belongs to the previously active drawing.
|
||||
///
|
||||
/// Plot settings such as paper size and CTB are document/layout data.
|
||||
/// A newly opened DWG may reuse the same tab index and layout name as the
|
||||
/// startup drawing, so `(tab, layout)` alone is not enough to identify the
|
||||
/// context. Keeping that cache makes the first Plot dialog show the startup
|
||||
/// A4/no-CTB state; the second opening then appears correct only because the
|
||||
/// first one refreshed the runtime state.
|
||||
fn reset_plot_session_for_opened_document(&mut self) {
|
||||
self.plot_prev = None;
|
||||
self.plot_prev_context = None;
|
||||
self.plot_reconcile_printer_media = None;
|
||||
self.plot_setup_template = None;
|
||||
self.plot_window = None;
|
||||
}
|
||||
|
||||
pub(super) fn on_file_opened(
|
||||
&mut self,
|
||||
name: String,
|
||||
@@ -1981,6 +2082,12 @@ impl OpenCADStudio {
|
||||
self.tabs[i].scene.load_current_layout_state();
|
||||
self.tabs[i].refresh_active_ucs();
|
||||
}
|
||||
|
||||
// The tab may have been reused for the newly opened file. Any Plot
|
||||
// dialog cache belongs to the previous/startup drawing and must not
|
||||
// win over this DWG's embedded layout settings on the first Ctrl+P.
|
||||
self.reset_plot_session_for_opened_document();
|
||||
|
||||
// Object isolation is session-only. A newly opened drawing must
|
||||
// not inherit the previous tab's filter, and persisted entity
|
||||
// visibility remains independent (not an isolation session).
|
||||
@@ -3794,22 +3901,27 @@ impl OpenCADStudio {
|
||||
let previous_style = self.active_plot_style.clone();
|
||||
let previous_window = self.plot_window;
|
||||
|
||||
// Once Print All has an explicit override, reopening Options must keep
|
||||
// those settings instead of replacing them with the active layout's
|
||||
// page setup.
|
||||
let keep_print_all_override = self.print_all_settings_override;
|
||||
|
||||
// Refresh runtime data such as printers, paper sizes, plot styles and
|
||||
// named page setups.
|
||||
// Refresh runtime-only data such as printers, available media,
|
||||
// CTB names, scales and page-setup rows.
|
||||
//
|
||||
// `on_plot_dialog_open()` also seeds the editor from the active
|
||||
// layout. That is correct for normal PLOT, but Print All Options is a
|
||||
// shared batch override editor: its persisted user choices must win.
|
||||
let task = self.on_plot_dialog_open();
|
||||
|
||||
if keep_print_all_override {
|
||||
// Restore only the user-editable plot settings. Runtime lists populated
|
||||
// above remain intact.
|
||||
self.plot_dialog.copy_settings_from(&previous);
|
||||
self.active_plot_style = previous_style.clone();
|
||||
self.plot_window = previous_window;
|
||||
}
|
||||
// Restore only user-editable values. `copy_settings_from` deliberately
|
||||
// leaves the freshly refreshed runtime lists intact.
|
||||
//
|
||||
// Do this even on the first Print All Options opening after restarting
|
||||
// OCS. Previously this happened only when `print_all_settings_override`
|
||||
// was already true, so the first opening always fell back to the
|
||||
// active layout/default A4 and lost the persisted CTB selection.
|
||||
self.plot_dialog.copy_settings_from(&previous);
|
||||
|
||||
// Preserve the parsed CTB when it matches the restored selection.
|
||||
// If it is absent/stale, dialog_plot_style() can resolve it by name.
|
||||
self.active_plot_style = previous_style.clone();
|
||||
self.plot_window = previous_window;
|
||||
|
||||
self.print_all_options_prev = Some(previous);
|
||||
self.print_all_plot_style_prev = Some(previous_style);
|
||||
@@ -4342,7 +4454,15 @@ impl OpenCADStudio {
|
||||
/// layout's plot settings and the printers found on the system.
|
||||
pub(super) fn on_plot_dialog_open(&mut self) -> Task<Message> {
|
||||
use crate::io::paper_catalog::Orientation;
|
||||
|
||||
let cur = self.tabs[self.active_tab].scene.current_layout.clone();
|
||||
let same_context = self
|
||||
.plot_prev_context
|
||||
.as_ref()
|
||||
.is_some_and(|(tab, layout)| *tab == self.active_tab && layout == &cur);
|
||||
|
||||
let previous = self.plot_dialog.clone();
|
||||
|
||||
let scales: Vec<(String, f64)> = self.tabs[self.active_tab]
|
||||
.scene
|
||||
.scale_list()
|
||||
@@ -4427,31 +4547,72 @@ impl OpenCADStudio {
|
||||
self.command_line
|
||||
.push_warning(crate::tf!("Could not list printers: {error}").as_ref());
|
||||
}
|
||||
self.plot_prev = Some(previous);
|
||||
let cur = self.tabs[self.active_tab].scene.current_layout.clone();
|
||||
self.plot_prev = Some(previous.clone());
|
||||
let layout_entry = format!("*{cur}*");
|
||||
// The auto-applied page setup must not reset the output device: a
|
||||
// drawing rarely stores one, so honouring it would snap the dialog
|
||||
// back to the system default on every open. The user's last choice
|
||||
// (persisted across sessions with the rest of the dialog) survives
|
||||
// unless the setup names an explicit device of its own.
|
||||
let kept_printer = self.plot_dialog.printer.clone();
|
||||
let kept_to_file = self.plot_dialog.to_file;
|
||||
if self.tabs[self.active_tab]
|
||||
.scene
|
||||
.plot_settings_for(&cur)
|
||||
.is_some()
|
||||
{
|
||||
self.select_page_setup(&layout_entry);
|
||||
|
||||
if same_context {
|
||||
// #1654: reopening Plot for the same drawing/layout must keep the
|
||||
// values the user just edited. The layout page setup is the initial
|
||||
// seed, not something that should overwrite paper, scale, CTB,
|
||||
// orientation, area and output choices on every Ctrl+P.
|
||||
//
|
||||
// `copy_settings_from` deliberately leaves refreshed runtime lists
|
||||
// (printers, media, scales, page setups, etc.) untouched.
|
||||
self.plot_dialog.copy_settings_from(&previous);
|
||||
self.plot_dialog.selected_setup = previous.selected_setup.clone();
|
||||
|
||||
// A custom ratio such as 1:3 may not exist in the drawing's named
|
||||
// scale list. select_page_setup() normally adds it while loading a
|
||||
// PlotSettings, but same-context reopening intentionally skips that
|
||||
// reload. Keep the restored custom scale available to the picker so
|
||||
// the validation below does not collapse it to 1:1.
|
||||
if !self.plot_dialog.fit_to_paper
|
||||
&& !self
|
||||
.plot_dialog
|
||||
.scales
|
||||
.iter()
|
||||
.any(|(name, _)| name == &self.plot_dialog.scale)
|
||||
{
|
||||
let factor = plot_dialog_scale_factor(&self.plot_dialog);
|
||||
|
||||
if factor.is_finite() && factor > 0.0 {
|
||||
let name = self.plot_dialog.scale.clone();
|
||||
self.plot_dialog.scales.push((name, factor));
|
||||
self.plot_dialog.scales.sort_by(|a, b| {
|
||||
a.1.partial_cmp(&b.1)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.select_page_setup(crate::ui::window::plot::SETUP_PREV);
|
||||
}
|
||||
let setup_named_a_device =
|
||||
self.plot_dialog.printer.is_some() || self.plot_dialog.to_file;
|
||||
if !setup_named_a_device {
|
||||
self.plot_dialog.printer = kept_printer;
|
||||
self.plot_dialog.to_file = kept_to_file;
|
||||
// First open for this tab/layout: seed the editor from its actual
|
||||
// page setup. An explicit setup selection later still uses
|
||||
// `select_page_setup` and therefore intentionally replaces fields.
|
||||
//
|
||||
// Keep the chosen output device when a drawing page setup does not
|
||||
// name one of its own.
|
||||
let kept_printer = self.plot_dialog.printer.clone();
|
||||
let kept_to_file = self.plot_dialog.to_file;
|
||||
|
||||
if self.tabs[self.active_tab]
|
||||
.scene
|
||||
.plot_settings_for(&cur)
|
||||
.is_some()
|
||||
{
|
||||
self.select_page_setup(&layout_entry);
|
||||
} else {
|
||||
self.select_page_setup(crate::ui::window::plot::SETUP_PREV);
|
||||
}
|
||||
|
||||
let setup_named_a_device =
|
||||
self.plot_dialog.printer.is_some() || self.plot_dialog.to_file;
|
||||
if !setup_named_a_device {
|
||||
self.plot_dialog.printer = kept_printer;
|
||||
self.plot_dialog.to_file = kept_to_file;
|
||||
}
|
||||
}
|
||||
|
||||
self.plot_prev_context = Some((self.active_tab, cur.clone()));
|
||||
// A persisted plot window (saved with the dialog settings) restores
|
||||
// across sessions; a freshly loaded one wins so the two stay in sync.
|
||||
if self.plot_window.is_none() {
|
||||
@@ -4481,29 +4642,57 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
self.active_modal = Some(crate::app::ModalKind::Plot);
|
||||
self.request_printer_media()
|
||||
// Loading printer capabilities is informational when Plot merely
|
||||
// opens. It must not replace the sheet stored in the drawing.
|
||||
self.request_printer_media(false)
|
||||
}
|
||||
|
||||
/// Keep the dialog's view of the selected printer's media current: use the
|
||||
/// session cache when the printer was asked before, otherwise ask it in
|
||||
/// the background and clear the stale answer meanwhile. PDF output and
|
||||
/// the default printer list the catalogue instead.
|
||||
fn request_printer_media(&mut self) -> Task<Message> {
|
||||
let d = &mut self.plot_dialog;
|
||||
let Some(printer) = (!d.to_file).then(|| d.printer.clone()).flatten() else {
|
||||
d.printer_media = None;
|
||||
fn request_printer_media(&mut self, reconcile: bool) -> Task<Message> {
|
||||
let Some(printer) = (!self.plot_dialog.to_file)
|
||||
.then(|| self.plot_dialog.printer.clone())
|
||||
.flatten()
|
||||
else {
|
||||
self.plot_dialog.printer_media = None;
|
||||
self.plot_reconcile_printer_media = None;
|
||||
return Task::none();
|
||||
};
|
||||
if let Some(caps) = crate::io::plot_device::cached_printer_capabilities(&printer) {
|
||||
d.printer_media = Some(caps);
|
||||
|
||||
// PAPERUPDATE is relevant after the user explicitly changes the
|
||||
// device/page setup. Merely reopening Plot must leave the drawing's
|
||||
// stored paper untouched.
|
||||
self.plot_reconcile_printer_media =
|
||||
reconcile.then(|| printer.clone());
|
||||
|
||||
if let Some(caps) =
|
||||
crate::io::plot_device::cached_printer_capabilities(&printer)
|
||||
{
|
||||
self.plot_dialog.printer_media = Some(caps);
|
||||
|
||||
if reconcile {
|
||||
self.reconcile_sheet_with_printer(&printer);
|
||||
}
|
||||
|
||||
return Task::none();
|
||||
}
|
||||
d.printer_media = None;
|
||||
|
||||
self.plot_dialog.printer_media = None;
|
||||
let name = printer.clone();
|
||||
|
||||
background_task(
|
||||
crate::t!("Printer capabilities"),
|
||||
move || crate::io::plot_device::printer_capabilities(&printer),
|
||||
move |caps| Message::PlotDlg(crate::ui::window::plot::PlotDlgMsg::PrinterMedia(name, caps)),
|
||||
move |caps| {
|
||||
Message::PlotDlg(
|
||||
crate::ui::window::plot::PlotDlgMsg::PrinterMedia(
|
||||
name,
|
||||
caps,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4668,6 +4857,9 @@ impl OpenCADStudio {
|
||||
};
|
||||
match msg {
|
||||
M::Close => {
|
||||
// Keep the user's last Plot choices available for the next
|
||||
// invocation even when the dialog was simply closed.
|
||||
self.save_config();
|
||||
self.close_active_modal();
|
||||
Task::none()
|
||||
}
|
||||
@@ -4685,14 +4877,35 @@ impl OpenCADStudio {
|
||||
self.plot_dialog.printer = Some(s);
|
||||
}
|
||||
self.save_config();
|
||||
self.request_printer_media()
|
||||
// This is an explicit device change, so PAPERUPDATE may
|
||||
// reconcile an unsupported sheet when its capabilities arrive.
|
||||
self.request_printer_media(true)
|
||||
}
|
||||
M::PrinterMedia(printer, caps) => {
|
||||
let d = &mut self.plot_dialog;
|
||||
if !d.to_file && d.printer.as_deref() == Some(printer.as_str()) {
|
||||
d.printer_media = caps;
|
||||
self.reconcile_sheet_with_printer(&printer);
|
||||
let still_current =
|
||||
!self.plot_dialog.to_file
|
||||
&& self.plot_dialog.printer.as_deref()
|
||||
== Some(printer.as_str());
|
||||
|
||||
if still_current {
|
||||
self.plot_dialog.printer_media = caps;
|
||||
|
||||
let reconcile = self
|
||||
.plot_reconcile_printer_media
|
||||
.as_deref()
|
||||
.is_some_and(|pending| {
|
||||
pending.eq_ignore_ascii_case(&printer)
|
||||
});
|
||||
|
||||
if reconcile {
|
||||
// Keep the explicit-device context alive while this
|
||||
// printer remains selected. Capability updates for the
|
||||
// same user-selected device must continue to obey
|
||||
// PAPERUPDATE.
|
||||
self.reconcile_sheet_with_printer(&printer);
|
||||
}
|
||||
}
|
||||
|
||||
Task::none()
|
||||
}
|
||||
M::CustomPaper(msg) => {
|
||||
@@ -4952,13 +5165,48 @@ impl OpenCADStudio {
|
||||
Task::done(Message::Command("PLOTWINDOW".into()))
|
||||
}
|
||||
M::SelectSetup(name) => {
|
||||
// Print All Options edits the page setups represented by the
|
||||
// layout rows. With "Save changes to layout" enabled, preserve
|
||||
// the setup we are leaving before another one is loaded.
|
||||
if self.print_all_options
|
||||
&& self.plot_dialog.save_to_layout
|
||||
&& self.plot_dialog.selected_setup != name
|
||||
{
|
||||
let previous = self.plot_dialog.selected_setup.clone();
|
||||
|
||||
if is_layout_entry(&previous) {
|
||||
let layout = layout_entry_name(&previous).to_string();
|
||||
self.apply_dialog_to_named_layout(&layout);
|
||||
} else if previous
|
||||
== crate::ui::window::plot::SETUP_PREV
|
||||
{
|
||||
// `<previous>` is a transient snapshot, not an actual
|
||||
// page setup. `plot_prev_context` records which layout
|
||||
// that snapshot came from.
|
||||
//
|
||||
// If the user returns to that same layout, commit the
|
||||
// restored snapshot to it before reloading the row.
|
||||
if let Some((tab, layout)) =
|
||||
self.plot_prev_context.clone()
|
||||
{
|
||||
let target = format!("*{layout}*");
|
||||
|
||||
if tab == self.active_tab && name == target {
|
||||
self.apply_dialog_to_named_layout(&layout);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.select_page_setup(&name);
|
||||
|
||||
if self.print_all_options {
|
||||
self.plot_dialog.paper_space = true;
|
||||
self.plot_dialog.area = "Layout".into();
|
||||
}
|
||||
|
||||
// The setup may name a different printer.
|
||||
self.request_printer_media()
|
||||
self.request_printer_media(true)
|
||||
}
|
||||
M::SetCurrent => {
|
||||
self.apply_dialog_to_layout();
|
||||
@@ -5385,6 +5633,38 @@ impl OpenCADStudio {
|
||||
self.apply_plot_page_settings();
|
||||
}
|
||||
|
||||
/// Persist the current Plot editor into a specific layout.
|
||||
///
|
||||
/// This is needed by Print All Options because selecting `*Layout1*`,
|
||||
/// `*Layout2*`, etc. does not switch `scene.current_layout`.
|
||||
fn apply_dialog_to_named_layout(&mut self, layout_name: &str) {
|
||||
use codec::objects::{PlotPaperUnits, PlotSettings};
|
||||
|
||||
let i = self.active_tab;
|
||||
|
||||
let base = self.tabs[i]
|
||||
.scene
|
||||
.plot_settings_for(layout_name)
|
||||
.unwrap_or_else(|| {
|
||||
let mut settings = PlotSettings::new("");
|
||||
settings.paper_units = PlotPaperUnits::Millimeters;
|
||||
settings
|
||||
});
|
||||
|
||||
let ps = self.plot_settings_from_dialog(base);
|
||||
|
||||
if self.tabs[i]
|
||||
.scene
|
||||
.set_layout_plot_settings(layout_name, &ps)
|
||||
{
|
||||
self.tabs[i].dirty = true;
|
||||
|
||||
if self.tabs[i].scene.current_layout == layout_name {
|
||||
self.tabs[i].scene.bump_geometry_no_blocks();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Open a preview PDF, export a PDF, or send the job to the chosen printer.
|
||||
fn on_plot_dlg_commit(&mut self, preview: bool) -> Task<Message> {
|
||||
let d = self.plot_dialog.clone();
|
||||
@@ -5534,10 +5814,21 @@ impl OpenCADStudio {
|
||||
if d.style_name.is_empty() || d.style_missing || !d.apply_plot_styles {
|
||||
return None;
|
||||
}
|
||||
self.active_plot_style
|
||||
|
||||
// Fast path: the selected table is already active.
|
||||
if let Some(table) = self
|
||||
.active_plot_style
|
||||
.as_ref()
|
||||
.filter(|table| table.name.eq_ignore_ascii_case(&d.style_name))
|
||||
.cloned()
|
||||
{
|
||||
return Some(table.clone());
|
||||
}
|
||||
|
||||
// The dialog selection is persisted separately from the parsed CTB
|
||||
// object. After reopening a drawing the name may be correct while the
|
||||
// in-memory table is still the previous/default one. Plotting must not
|
||||
// silently ignore the selected CTB in that state.
|
||||
crate::io::plot_style::PlotStyleTable::load_named(&d.style_name).ok()
|
||||
}
|
||||
|
||||
pub(in crate::app) fn window_plot_job(&self) -> Option<PdfPageInput> {
|
||||
@@ -6677,6 +6968,322 @@ mod plot_device_persistence_tests {
|
||||
assert!(!app.plot_dialog.to_file);
|
||||
}
|
||||
|
||||
/// Reopening Plot in the same tab/layout keeps the user's most recent
|
||||
/// paper, scale and CTB choices instead of reloading the layout page setup
|
||||
/// over them. Regression test for #1654.
|
||||
#[test]
|
||||
fn reopening_same_plot_context_keeps_recent_settings() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
|
||||
// First open seeds from the drawing/layout as usual.
|
||||
let _ = app.on_plot_dialog_open();
|
||||
|
||||
// User edits the high-frequency Plot settings.
|
||||
app.plot_dialog.paper = "ISO_A3_(297.00_x_420.00_MM)".into();
|
||||
app.plot_dialog.paper_width_mm = 297.0;
|
||||
app.plot_dialog.paper_height_mm = 420.0;
|
||||
app.plot_dialog.scale = "1:100".into();
|
||||
app.plot_dialog.fit_to_paper = false;
|
||||
app.plot_dialog.style_name = "office.ctb".into();
|
||||
app.plot_dialog.apply_plot_styles = true;
|
||||
|
||||
// Reopening Ctrl+P in the same layout must not reapply the layout's
|
||||
// old page setup and erase those choices.
|
||||
let _ = app.on_plot_dialog_open();
|
||||
|
||||
assert_eq!(
|
||||
app.plot_dialog.paper,
|
||||
"ISO_A3_(297.00_x_420.00_MM)"
|
||||
);
|
||||
assert_eq!(app.plot_dialog.scale, "1:100");
|
||||
assert!(!app.plot_dialog.fit_to_paper);
|
||||
assert_eq!(app.plot_dialog.style_name, "office.ctb");
|
||||
assert!(app.plot_dialog.apply_plot_styles);
|
||||
}
|
||||
|
||||
/// Opening a different drawing in a reused tab must invalidate the Plot
|
||||
/// session cache. The first Plot dialog must read paper and CTB from the
|
||||
/// newly opened document instead of showing the startup drawing's values.
|
||||
#[test]
|
||||
fn newly_opened_document_forces_first_plot_dialog_to_read_layout() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let i = app.active_tab;
|
||||
|
||||
app.tabs[i].scene.current_layout = "Layout1".into();
|
||||
|
||||
let mut ps = app.tabs[i]
|
||||
.scene
|
||||
.plot_settings_for("Layout1")
|
||||
.expect("Layout1 must exist");
|
||||
|
||||
ps.paper_size = "ISO_A2_(420.00_x_594.00_MM)".into();
|
||||
ps.paper_width = 420.0;
|
||||
ps.paper_height = 594.0;
|
||||
ps.current_style_sheet =
|
||||
crate::io::plot_style::MONOCHROME_PLOT_STYLE.into();
|
||||
ps.flags.plot_plot_styles = true;
|
||||
|
||||
assert!(
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.set_layout_plot_settings("Layout1", &ps)
|
||||
);
|
||||
|
||||
// Reproduce stale state inherited from the startup/previous drawing.
|
||||
app.plot_dialog.paper =
|
||||
"ISO_A4_(210.00_x_297.00_MM)".into();
|
||||
app.plot_dialog.style_name.clear();
|
||||
app.plot_dialog.apply_plot_styles = false;
|
||||
|
||||
app.plot_prev = Some(app.plot_dialog.clone());
|
||||
app.plot_prev_context =
|
||||
Some((i, "Layout1".into()));
|
||||
|
||||
// Installing another drawing must make that state unusable.
|
||||
app.reset_plot_session_for_opened_document();
|
||||
|
||||
assert!(app.plot_prev.is_none());
|
||||
assert!(app.plot_prev_context.is_none());
|
||||
|
||||
let _ = app.on_plot_dialog_open();
|
||||
|
||||
assert_eq!(
|
||||
app.plot_dialog.paper,
|
||||
"ISO_A2_(420.00_x_594.00_MM)",
|
||||
"the first Plot opening must use the new drawing's paper"
|
||||
);
|
||||
|
||||
assert!(
|
||||
app.plot_dialog
|
||||
.style_name
|
||||
.eq_ignore_ascii_case(
|
||||
crate::io::plot_style::MONOCHROME_PLOT_STYLE
|
||||
),
|
||||
"the first Plot opening must use the new drawing's CTB"
|
||||
);
|
||||
|
||||
assert!(
|
||||
app.plot_dialog.apply_plot_styles,
|
||||
"the stored CTB must already be enabled on the first Plot opening"
|
||||
);
|
||||
}
|
||||
|
||||
/// Print All Options is a shared batch-settings editor. On the first
|
||||
/// opening after restarting OCS it must keep the persisted user choices
|
||||
/// instead of replacing them with the active layout's page setup.
|
||||
#[test]
|
||||
fn print_all_options_keeps_persisted_plot_preferences_on_first_open() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
|
||||
// Simulate preferences restored from the previous application session.
|
||||
app.plot_dialog.paper =
|
||||
"ISO_A2_(420.00_x_594.00_MM)".into();
|
||||
app.plot_dialog.paper_width_mm = 420.0;
|
||||
app.plot_dialog.paper_height_mm = 594.0;
|
||||
app.plot_dialog.scale = "1:1000".into();
|
||||
app.plot_dialog.style_name =
|
||||
crate::io::plot_style::MONOCHROME_PLOT_STYLE.into();
|
||||
app.plot_dialog.apply_plot_styles = true;
|
||||
|
||||
// Fresh process: no explicit Print All override exists yet.
|
||||
app.print_all_settings_override = false;
|
||||
|
||||
let _ = app.on_print_all_options();
|
||||
|
||||
assert_eq!(
|
||||
app.plot_dialog.paper,
|
||||
"ISO_A2_(420.00_x_594.00_MM)",
|
||||
"first Print All Options opening must keep the persisted paper"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
app.plot_dialog.scale,
|
||||
"1:1000",
|
||||
"first Print All Options opening must keep the persisted scale"
|
||||
);
|
||||
|
||||
assert!(
|
||||
app.plot_dialog
|
||||
.style_name
|
||||
.eq_ignore_ascii_case(
|
||||
crate::io::plot_style::MONOCHROME_PLOT_STYLE
|
||||
),
|
||||
"first Print All Options opening must keep the persisted CTB"
|
||||
);
|
||||
|
||||
assert!(
|
||||
app.plot_dialog.apply_plot_styles,
|
||||
"the persisted CTB must remain enabled"
|
||||
);
|
||||
|
||||
assert!(
|
||||
app.print_all_options,
|
||||
"the dialog must remain in Print All Options mode"
|
||||
);
|
||||
}
|
||||
|
||||
/// `<previous>` belongs to the layout that produced the snapshot.
|
||||
/// Returning to that layout with "Save changes to layout" enabled must
|
||||
/// preserve the restored paper/CTB instead of reloading the old embedded
|
||||
/// page setup over it.
|
||||
#[test]
|
||||
fn previous_snapshot_is_saved_back_to_its_origin_layout() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let i = app.active_tab;
|
||||
|
||||
app.tabs[i].scene.current_layout = "Layout1".into();
|
||||
|
||||
// Layout1 still contains an old page setup.
|
||||
let mut stored = app.tabs[i]
|
||||
.scene
|
||||
.plot_settings_for("Layout1")
|
||||
.expect("Layout1 must exist");
|
||||
|
||||
stored.paper_size = "ISO_A4_(210.00_x_297.00_MM)".into();
|
||||
stored.paper_width = 210.0;
|
||||
stored.paper_height = 297.0;
|
||||
stored.current_style_sheet.clear();
|
||||
stored.flags.plot_plot_styles = false;
|
||||
|
||||
assert!(
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.set_layout_plot_settings("Layout1", &stored)
|
||||
);
|
||||
|
||||
// `<previous>` is the desired state that originally belonged to
|
||||
// Layout1.
|
||||
app.print_all_options = true;
|
||||
app.plot_prev_context = Some((i, "Layout1".into()));
|
||||
app.plot_dialog.selected_setup =
|
||||
crate::ui::window::plot::SETUP_PREV.into();
|
||||
|
||||
app.plot_dialog.paper =
|
||||
"ISO_A2_(420.00_x_594.00_MM)".into();
|
||||
app.plot_dialog.paper_width_mm = 420.0;
|
||||
app.plot_dialog.paper_height_mm = 594.0;
|
||||
app.plot_dialog.style_name =
|
||||
crate::io::plot_style::MONOCHROME_PLOT_STYLE.into();
|
||||
app.plot_dialog.apply_plot_styles = true;
|
||||
app.plot_dialog.save_to_layout = true;
|
||||
|
||||
let _ = app.on_plot_dlg(
|
||||
crate::ui::window::plot::PlotDlgMsg::SelectSetup(
|
||||
"*Layout1*".into(),
|
||||
),
|
||||
);
|
||||
|
||||
let saved = app.tabs[i]
|
||||
.scene
|
||||
.plot_settings_for("Layout1")
|
||||
.expect("Layout1 must still exist");
|
||||
|
||||
assert_eq!(
|
||||
saved.paper_size,
|
||||
"ISO_A2_(420.00_x_594.00_MM)"
|
||||
);
|
||||
|
||||
assert!(
|
||||
saved.current_style_sheet.eq_ignore_ascii_case(
|
||||
crate::io::plot_style::MONOCHROME_PLOT_STYLE
|
||||
)
|
||||
);
|
||||
|
||||
assert!(saved.flags.plot_plot_styles);
|
||||
|
||||
assert_eq!(
|
||||
app.plot_dialog.paper,
|
||||
"ISO_A2_(420.00_x_594.00_MM)"
|
||||
);
|
||||
|
||||
assert!(
|
||||
app.plot_dialog
|
||||
.style_name
|
||||
.eq_ignore_ascii_case(
|
||||
crate::io::plot_style::MONOCHROME_PLOT_STYLE
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// A CTB selected in the dialog must be usable even when the parsed
|
||||
/// active table was not restored with it. This is the state seen after
|
||||
/// reopening a drawing: the dropdown can name the correct CTB while the
|
||||
/// first preview otherwise plots using raw layer colours.
|
||||
#[test]
|
||||
fn selected_ctb_is_resolved_when_active_table_is_stale() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
|
||||
app.plot_dialog.style_name =
|
||||
crate::io::plot_style::MONOCHROME_PLOT_STYLE.into();
|
||||
app.plot_dialog.apply_plot_styles = true;
|
||||
app.plot_dialog.style_missing = false;
|
||||
|
||||
// Reproduce a fresh-session mismatch: the visible selection exists,
|
||||
// but no matching parsed table is active yet.
|
||||
app.active_plot_style = None;
|
||||
|
||||
let table = app
|
||||
.dialog_plot_style(&app.plot_dialog)
|
||||
.expect("selected CTB must be resolved by name");
|
||||
|
||||
assert!(
|
||||
table
|
||||
.name
|
||||
.eq_ignore_ascii_case(
|
||||
crate::io::plot_style::MONOCHROME_PLOT_STYLE
|
||||
),
|
||||
"the table used for plotting must match the visible CTB selection"
|
||||
);
|
||||
}
|
||||
|
||||
/// Printer capability discovery on merely opening Plot must never mutate
|
||||
/// the paper stored in the drawing. PAPERUPDATE is applied only after an
|
||||
/// explicit device/page-setup choice.
|
||||
#[test]
|
||||
fn opening_plot_does_not_replace_sheet_when_printer_media_arrives() {
|
||||
use crate::io::paper_catalog::{resolve, Margins};
|
||||
use crate::io::plot_device::{
|
||||
PrinterCapabilities,
|
||||
PrinterMedia,
|
||||
};
|
||||
|
||||
let a2 =
|
||||
resolve("ISO_A2_(420.00_x_594.00_MM)").expect("A2");
|
||||
let a4 =
|
||||
resolve("ISO_A4_(210.00_x_297.00_MM)").expect("A4");
|
||||
|
||||
let caps = std::sync::Arc::new(PrinterCapabilities {
|
||||
media: vec![PrinterMedia {
|
||||
paper: a4.clone(),
|
||||
margins: Margins::uniform(3.0),
|
||||
borderless: false,
|
||||
}],
|
||||
default_paper: Some(a4),
|
||||
});
|
||||
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.plot_dialog.to_file = false;
|
||||
app.plot_dialog.printer =
|
||||
Some("OCS Test Printer".into());
|
||||
app.plot_dialog.paper = a2.canonical.to_string();
|
||||
app.plot_dialog.paper_update = 1;
|
||||
|
||||
// No explicit printer change is pending: this models the asynchronous
|
||||
// capability reply caused only by opening the Plot dialog.
|
||||
app.plot_reconcile_printer_media = None;
|
||||
|
||||
let _ = app.on_plot_dlg(crate::ui::window::plot::PlotDlgMsg::PrinterMedia(
|
||||
"OCS Test Printer".into(),
|
||||
Some(caps),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
app.plot_dialog.paper,
|
||||
a2.canonical,
|
||||
"opening Plot must preserve the drawing's stored sheet"
|
||||
);
|
||||
}
|
||||
|
||||
/// A layout whose plot settings name an explicit device wins over the
|
||||
/// saved preference: selecting its setup is an explicit device choice.
|
||||
#[test]
|
||||
|
||||
@@ -1933,6 +1933,12 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
||||
"M density" => Some(("properties", "m-density")),
|
||||
"M vertex count" => Some(("properties", "m-vertex-count")),
|
||||
"MASSPROP: no entities selected. Select entities first." => Some(("inquiry", "massprop-no-entities-selected-select-entities")),
|
||||
"MASSPROP: {} has no closed body to measure." => Some(("inquiry", "massprop-has-no-closed-body-to-measure")),
|
||||
"{} Volume={:.4} Centroid=({:.4},{:.4},{:.4})" => Some(("inquiry", "volume-4-centroid-4-4-4")),
|
||||
"Moments of inertia X={:.4} Y={:.4} Z={:.4}" => Some(("inquiry", "moments-of-inertia-x-4-y-4-z-4")),
|
||||
"Products of inertia XY={:.4} YZ={:.4} ZX={:.4}" => Some(("inquiry", "products-of-inertia-xy-4-yz-4-zx-4")),
|
||||
"Radii of gyration X={:.4} Y={:.4} Z={:.4}" => Some(("inquiry", "radii-of-gyration-x-4-y-4-z-4")),
|
||||
"Principal moments about centroid I={:.4} J={:.4} K={:.4}" => Some(("inquiry", "principal-moments-about-centroid-i-4-j-4-k-4")),
|
||||
"MATCHPROP Select destination objects (Enter to finish):" => Some(("properties", "matchprop-select-destination-objects-enter-to")),
|
||||
"MATCHPROP Select source object:" => Some(("properties", "matchprop-select-source-object")),
|
||||
"MEASURE Select object to measure:" => Some(("inquiry", "measure-select-object-to-measure")),
|
||||
|
||||
@@ -769,6 +769,9 @@ enum FilletEntity {
|
||||
handle: Handle,
|
||||
seg_idx: usize,
|
||||
},
|
||||
/// A circle, ellipse or spline: filleted through the kernel's general
|
||||
/// construction, and trimmed only when it has ends to trim.
|
||||
Curve(EntityType),
|
||||
}
|
||||
|
||||
impl FilletEntity {
|
||||
@@ -776,6 +779,9 @@ impl FilletEntity {
|
||||
match e {
|
||||
EntityType::Line(l) => Some(Self::Line(l.clone())),
|
||||
EntityType::Arc(a) => Some(Self::Arc(a.clone())),
|
||||
EntityType::Circle(_) | EntityType::Ellipse(_) | EntityType::Spline(_) => {
|
||||
crate::entities::curve::entity_curve_xy(e).map(|_| Self::Curve(e.clone()))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -794,6 +800,7 @@ impl FilletEntity {
|
||||
Self::Line(l) => EntityType::Line(l.clone()),
|
||||
Self::Arc(a) => EntityType::Arc(a.clone()),
|
||||
Self::LwPoly { poly, .. } => EntityType::LwPolyline(poly.clone()),
|
||||
Self::Curve(entity) => entity.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -802,6 +809,20 @@ impl FilletEntity {
|
||||
Self::Line(l) => l.start.z,
|
||||
Self::Arc(a) => a.center.z,
|
||||
Self::LwPoly { poly, .. } => poly.elevation,
|
||||
Self::Curve(EntityType::Circle(c)) => c.center.z,
|
||||
Self::Curve(EntityType::Ellipse(e)) => e.center.z,
|
||||
Self::Curve(EntityType::Spline(s)) => s.control_points.first().map_or(0.0, |p| p.z),
|
||||
Self::Curve(_) => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// The shape as a kernel curve in plan.
|
||||
fn kernel_curve(&self) -> Option<KernelCurve> {
|
||||
match self {
|
||||
Self::Line(l) => Some(line_curve(l)),
|
||||
Self::Arc(a) => Some(arc_curve(a)),
|
||||
Self::LwPoly { poly, seg_idx, .. } => Some(line_curve(&lwpoly_seg_as_line(poly, *seg_idx))),
|
||||
Self::Curve(entity) => crate::entities::curve::entity_curve_xy(entity),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -825,6 +846,9 @@ fn compute_fillet_entities(
|
||||
if picks_arc_segment(e1) || picks_arc_segment(e2) {
|
||||
return None;
|
||||
}
|
||||
if matches!(e1, FilletEntity::Curve(_)) || matches!(e2, FilletEntity::Curve(_)) {
|
||||
return fillet_curves(e1, click1, e2, click2, radius, z);
|
||||
}
|
||||
|
||||
match (e1, e2) {
|
||||
// ── Line × Line ───────────────────────────────────────────────────
|
||||
@@ -1011,6 +1035,8 @@ fn compute_fillet_entities(
|
||||
None
|
||||
}
|
||||
}
|
||||
// Answered by `fillet_curves` above.
|
||||
(FilletEntity::Curve(_), _) | (_, FilletEntity::Curve(_)) => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1268,6 +1294,116 @@ fn fillet_arc_arc(
|
||||
None
|
||||
}
|
||||
|
||||
/// FILLET where one side is a circle, ellipse or spline. The kernel finds the
|
||||
/// tangent arcs; each side is then cut back to its tangent point, keeping the
|
||||
/// part that was picked. A closed curve has no end to cut back and is left
|
||||
/// whole.
|
||||
fn fillet_curves(
|
||||
e1: &FilletEntity,
|
||||
click1: [f64; 2],
|
||||
e2: &FilletEntity,
|
||||
click2: [f64; 2],
|
||||
radius: f64,
|
||||
z: f64,
|
||||
) -> Option<(EntityType, EntityType, Option<EntityType>)> {
|
||||
if radius < 1e-9 {
|
||||
return None;
|
||||
}
|
||||
let (a, b) = (e1.kernel_curve()?, e2.kernel_curve()?);
|
||||
let common = match e1 {
|
||||
FilletEntity::Line(l) => l.common.clone(),
|
||||
FilletEntity::Arc(a) => a.common.clone(),
|
||||
FilletEntity::LwPoly { poly, .. } => poly.common.clone(),
|
||||
FilletEntity::Curve(entity) => entity.common().clone(),
|
||||
};
|
||||
for fillet in ranked_fillets(&a, click1, &b, click2, radius) {
|
||||
let (Some(first), Some(second)) = (
|
||||
trim_to_tangent(e1, &a, fillet.tangent1, click1),
|
||||
trim_to_tangent(e2, &b, fillet.tangent2, click2),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
return Some((
|
||||
first,
|
||||
second,
|
||||
Some(EntityType::Arc(fillet_entity(&fillet, radius, z, &common))),
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// One side of a fillet cut back to where the arc touches it, the picked part
|
||||
/// kept. `None` when the touch is off a bounded curve's drawn part — only a
|
||||
/// line may be extended to meet the arc.
|
||||
fn trim_to_tangent(
|
||||
entity: &FilletEntity,
|
||||
curve: &KernelCurve,
|
||||
touch: [f64; 2],
|
||||
click: [f64; 2],
|
||||
) -> Option<EntityType> {
|
||||
match entity {
|
||||
FilletEntity::Line(l) => trim_line_to_point(l, touch, click).map(EntityType::Line),
|
||||
FilletEntity::LwPoly { poly, seg_idx, .. } => {
|
||||
let line = lwpoly_seg_as_line(poly, *seg_idx);
|
||||
let trimmed = trim_line_to_point(&line, touch, click)?;
|
||||
Some(EntityType::LwPolyline(rebuild_poly_from_trimmed_line(poly, *seg_idx, &line, &trimmed)))
|
||||
}
|
||||
FilletEntity::Arc(arc) => {
|
||||
let (centre, _, start, end, _) = arc_geom(arc);
|
||||
let at = arc_angle_at(centre, touch);
|
||||
let clamped = clamp_angle_to_arc(at, start, end);
|
||||
if (norm_angle(at) - norm_angle(clamped)).abs() > 0.01 {
|
||||
return None;
|
||||
}
|
||||
let picked = clamp_angle_to_arc(arc_angle_at(centre, click), start, end);
|
||||
Some(EntityType::Arc(
|
||||
if (clamped - start).rem_euclid(TAU) <= (picked - start).rem_euclid(TAU) {
|
||||
trim_arc(arc, clamped, end)
|
||||
} else {
|
||||
trim_arc(arc, start, clamped)
|
||||
},
|
||||
))
|
||||
}
|
||||
FilletEntity::Curve(source) => {
|
||||
let at = curve.parameter_at(touch);
|
||||
let on = curve.point_at(at);
|
||||
let scale = 1e-6 * (1.0 + touch[0].abs().max(touch[1].abs()));
|
||||
if (on[0] - touch[0]).hypot(on[1] - touch[1]) > scale.max(1e-6 * curve.length()) {
|
||||
return None;
|
||||
}
|
||||
if curve.is_closed() {
|
||||
return Some(source.clone());
|
||||
}
|
||||
let keep_end = curve.parameter_at(click) >= at;
|
||||
match source {
|
||||
// Its plan image runs the entity's own parameter only when
|
||||
// it faces up; a mirrored one is left alone.
|
||||
EntityType::Ellipse(ellipse) if ellipse.normal.z > 0.0 => {
|
||||
let mut trimmed = ellipse.clone();
|
||||
trimmed.common.handle = Handle::NULL;
|
||||
let sweep = {
|
||||
let raw = ellipse.end_parameter - ellipse.start_parameter;
|
||||
if raw <= 0.0 { raw + TAU } else { raw }
|
||||
};
|
||||
let cut = ellipse.start_parameter + at * sweep;
|
||||
if keep_end {
|
||||
trimmed.start_parameter = cut;
|
||||
} else {
|
||||
trimmed.end_parameter = cut;
|
||||
}
|
||||
Some(EntityType::Ellipse(trimmed))
|
||||
}
|
||||
EntityType::Spline(spline) => {
|
||||
let (low, high) = super::spline_ops::spline_range(spline)?;
|
||||
let (head, tail) = super::spline_ops::spline_cut(spline, low + at * (high - low))?;
|
||||
Some(EntityType::Spline(if keep_end { tail } else { head }))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trim a line endpoint nearest to the intersection point, keeping the click side.
|
||||
fn trim_line_to_point(line: &LineEnt, isect: [f64; 2], click: [f64; 2]) -> Option<LineEnt> {
|
||||
let mut l = line.clone();
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
// offset at once on the cursor's side; a negative one, on the other side.
|
||||
|
||||
use crate::entities::curve::{entity_curve, lwpolyline_world_xy};
|
||||
use crate::modules::draw::modify::spline_ops::spline_sample_xy;
|
||||
use codec::entities::LwVertex;
|
||||
use codec::entities::{
|
||||
Arc as ArcEnt, Circle as CircleEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline,
|
||||
@@ -24,7 +23,6 @@ use codec::entities::{
|
||||
use codec::{EntityType, Handle};
|
||||
// Polyline offsetting, and the angle normalisation that goes with it, come
|
||||
// from the kernel; only the entity conversion stays here.
|
||||
use kernel::geom2d::nurbs::clamped_uniform_knots;
|
||||
use kernel::geom2d::{
|
||||
offset_polyline, Polyline as KernelPolyline, PolylineVertex as KernelVertex,
|
||||
};
|
||||
@@ -205,111 +203,77 @@ fn offset_lwpolyline(p: &LwPolyline, dist: f64, side_pt: Vec3) -> Option<EntityT
|
||||
offset_lwpolylines(p, dist, side_pt).into_iter().next()
|
||||
}
|
||||
|
||||
// ── Ellipse offset ─────────────────────────────────────────────────────────
|
||||
// ── Ellipse and spline offset ──────────────────────────────────────────────
|
||||
//
|
||||
// A true offset of an ellipse is a Lamé curve, not an ellipse. As an
|
||||
// acceptable CAD approximation we scale both semi-axes uniformly and keep
|
||||
// the same orientation, center and parameter range. The sign of the offset
|
||||
// is determined by whether side_pt is inside or outside the ellipse.
|
||||
// Neither has a parallel of its own kind — an ellipse's is not an ellipse —
|
||||
// so both come back as a spline fitted through true offset points, within a
|
||||
// millionth of the curve's size, by the kernel. The work happens in the
|
||||
// curve's own plane, so a tilted or extruded one offsets in place.
|
||||
|
||||
fn offset_ellipse(e: &EllipseEnt, dist: f64, side_pt: Vec3) -> Option<EntityType> {
|
||||
let a = (e.major_axis.x.powi(2) + e.major_axis.y.powi(2)).sqrt();
|
||||
if a < 1e-9 {
|
||||
return None;
|
||||
}
|
||||
let b = a * e.minor_axis_ratio;
|
||||
let nx = e.major_axis.x / a;
|
||||
let ny = e.major_axis.y / a;
|
||||
// Project side_pt onto ellipse local frame and test inside/outside.
|
||||
let rx = side_pt.x as f64 - e.center.x;
|
||||
let ry = side_pt.y as f64 - e.center.y;
|
||||
let xl = rx * nx + ry * ny;
|
||||
let yl = -rx * ny + ry * nx;
|
||||
let inside = (xl / a).powi(2) + (yl / b).powi(2) < 1.0;
|
||||
let sign = if inside { -1.0 } else { 1.0 };
|
||||
|
||||
let new_a = a + sign * dist;
|
||||
let new_b = b + sign * dist;
|
||||
if new_a <= 1e-9 || new_b <= 1e-9 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut new_e = e.clone();
|
||||
new_e.common.handle = Handle::NULL;
|
||||
// Scale the major_axis vector proportionally.
|
||||
let scale = new_a / a;
|
||||
new_e.major_axis.x *= scale;
|
||||
new_e.major_axis.y *= scale;
|
||||
new_e.major_axis.z *= scale;
|
||||
new_e.minor_axis_ratio = new_b / new_a;
|
||||
Some(EntityType::Ellipse(new_e))
|
||||
let source = EntityType::Ellipse(e.clone());
|
||||
let mut template = SplineEnt::new();
|
||||
template.common = e.common.clone();
|
||||
offset_curved(&source, template, dist, side_pt)
|
||||
}
|
||||
|
||||
// ── Spline offset ──────────────────────────────────────────────────────────
|
||||
//
|
||||
// Strategy: sample the spline into N points, offset each sample point by
|
||||
// `dist` along the local perpendicular (based on the finite-difference
|
||||
// tangent), then fit a new spline through the offset points.
|
||||
|
||||
fn offset_spline(spl: &SplineEnt, dist: f64, side_pt: Vec3) -> Option<EntityType> {
|
||||
let (ts_knot, pts) = spline_sample_xy(spl, 64);
|
||||
let n = pts.len();
|
||||
if n < 2 {
|
||||
offset_curved(&EntityType::Spline(spl.clone()), spl.clone(), dist, side_pt)
|
||||
}
|
||||
|
||||
fn offset_curved(
|
||||
source: &EntityType,
|
||||
mut template: SplineEnt,
|
||||
dist: f64,
|
||||
side_pt: Vec3,
|
||||
) -> Option<EntityType> {
|
||||
let planar = entity_curve(source)?;
|
||||
let side = planar
|
||||
.plane
|
||||
.project([side_pt.x as f64, side_pt.y as f64, side_pt.z as f64])?;
|
||||
// Left of the curve's direction of travel is the kernel's positive side.
|
||||
let curve = &planar.curve;
|
||||
let at = curve.parameter_at(side);
|
||||
let step = 1e-6;
|
||||
let (before, after) = if curve.is_closed() {
|
||||
(curve.point_at((at - step).rem_euclid(1.0)), curve.point_at((at + step).rem_euclid(1.0)))
|
||||
} else {
|
||||
(curve.point_at((at - step).max(0.0)), curve.point_at((at + step).min(1.0)))
|
||||
};
|
||||
let foot = curve.point_at(at);
|
||||
let along = [after[0] - before[0], after[1] - before[1]];
|
||||
let toward = [side[0] - foot[0], side[1] - foot[1]];
|
||||
let left = along[0] * toward[1] - along[1] * toward[0] > 0.0;
|
||||
let signed = if left { dist.abs() } else { -dist.abs() };
|
||||
let tolerance = (curve.length() * 1e-6).max(1e-9);
|
||||
let kernel::geom2d::Curve::Nurbs(parallel) =
|
||||
kernel::geom2d::offset_curve(curve, signed, tolerance)?
|
||||
else {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Determine offset sign from the first non-degenerate tangent.
|
||||
let sign: f64 = (0..n - 1).find_map(|i| {
|
||||
let dx = pts[i + 1][0] - pts[i][0];
|
||||
let dy = pts[i + 1][1] - pts[i][1];
|
||||
let len = (dx * dx + dy * dy).sqrt();
|
||||
if len < 1e-12 {
|
||||
return None;
|
||||
}
|
||||
let vx = side_pt.x as f64 - pts[i][0];
|
||||
let vy = side_pt.y as f64 - pts[i][1];
|
||||
let cross = dx * vy - dy * vx;
|
||||
Some(if cross >= 0.0 { 1.0 } else { -1.0 })
|
||||
})?;
|
||||
|
||||
// Offset each sample point along the local normal.
|
||||
let offset_pts: Vec<codec::types::Vector3> = pts
|
||||
};
|
||||
template.common.handle = Handle::NULL;
|
||||
template.degree = parallel.degree() as i32;
|
||||
template.knots = parallel.knots().to_vec();
|
||||
template.control_points = parallel
|
||||
.control_points()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, p)| {
|
||||
// Tangent via central / forward / backward difference.
|
||||
let (dx, dy) = if i == 0 {
|
||||
let d = [pts[1][0] - pts[0][0], pts[1][1] - pts[0][1]];
|
||||
(d[0], d[1])
|
||||
} else if i == n - 1 {
|
||||
let d = [pts[n - 1][0] - pts[n - 2][0], pts[n - 1][1] - pts[n - 2][1]];
|
||||
(d[0], d[1])
|
||||
} else {
|
||||
(
|
||||
(pts[i + 1][0] - pts[i - 1][0]) * 0.5,
|
||||
(pts[i + 1][1] - pts[i - 1][1]) * 0.5,
|
||||
)
|
||||
};
|
||||
let len = (dx * dx + dy * dy).sqrt().max(1e-12);
|
||||
let nx = -dy / len; // left perpendicular
|
||||
let ny = dx / len;
|
||||
let z = spl.control_points.first().map(|v| v.z).unwrap_or(0.0);
|
||||
codec::types::Vector3::new(p[0] + sign * nx * dist, p[1] + sign * ny * dist, z)
|
||||
.map(|uv| {
|
||||
let [x, y, z] = planar.plane.point_at(*uv);
|
||||
codec::types::Vector3::new(x, y, z)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let _ = ts_knot;
|
||||
// Build a new spline from the offset control points (treat sample pts as fit pts → ctrl pts).
|
||||
let degree = spl.degree.max(1) as usize;
|
||||
let new_ctrl: Vec<codec::types::Vector3> = offset_pts;
|
||||
let n_ctrl = new_ctrl.len();
|
||||
let mut new_spl = spl.clone();
|
||||
new_spl.common.handle = Handle::NULL;
|
||||
new_spl.control_points = new_ctrl;
|
||||
new_spl.knots = clamped_uniform_knots(degree, n_ctrl);
|
||||
new_spl.fit_points.clear();
|
||||
new_spl.weights.clear();
|
||||
Some(EntityType::Spline(new_spl))
|
||||
template.weights = if parallel.is_rational() { parallel.weights().to_vec() } else { Vec::new() };
|
||||
template.flags.rational = parallel.is_rational();
|
||||
template.flags.periodic = false;
|
||||
template.flags.closed = curve.is_closed();
|
||||
if let Some([x, y, z]) = planar.plane.normal() {
|
||||
template.normal = codec::types::Vector3::new(x, y, z);
|
||||
}
|
||||
// A parallel is no longer the curve through the source's fit points.
|
||||
template.fit_points.clear();
|
||||
template.begin_tangent = codec::types::Vector3::new(0.0, 0.0, 0.0);
|
||||
template.end_tangent = codec::types::Vector3::new(0.0, 0.0, 0.0);
|
||||
Some(EntityType::Spline(template))
|
||||
}
|
||||
|
||||
// ── Dispatch ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -306,7 +306,12 @@ pub fn extract_circle_instances(
|
||||
|
||||
/// Allocation-free check whether this wire consists entirely of analytical circle/arc instances.
|
||||
pub fn can_extract_circle_instances(wire: &crate::scene::WireModel) -> bool {
|
||||
if wire.tangent_geoms.is_empty()
|
||||
// Patterned circular curves stay on the regular wire pipeline. Its dash
|
||||
// path is shared with LINE/POLYLINE and remains consistent across file
|
||||
// open/save cycles. The analytical circle path is reserved for continuous
|
||||
// curves until its linetype phase/LOD behaviour is fully equivalent.
|
||||
if wire.pattern_length > 0.0
|
||||
|| wire.tangent_geoms.is_empty()
|
||||
|| !wire.fill_tris.is_empty()
|
||||
|| wire.fill_is_3d
|
||||
|| !wire.text_verts.is_empty()
|
||||
@@ -344,7 +349,8 @@ pub fn extract_circle_instances_into(
|
||||
draw_depth: f32,
|
||||
out: &mut Vec<CircleInstance>,
|
||||
) -> bool {
|
||||
if wire.tangent_geoms.is_empty()
|
||||
if wire.pattern_length > 0.0
|
||||
|| wire.tangent_geoms.is_empty()
|
||||
|| !wire.fill_tris.is_empty()
|
||||
|| wire.fill_is_3d
|
||||
|| !wire.text_verts.is_empty()
|
||||
|
||||
@@ -1774,6 +1774,58 @@ mod tests {
|
||||
assert_eq!(partitioned.regular.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patterned_circle_and_arc_stay_in_regular_wire_pipeline() {
|
||||
use crate::scene::model::wire_model::TangentGeom;
|
||||
|
||||
let mut circle = WireModel::default();
|
||||
circle.points = vec![
|
||||
[10.0, 0.0, 0.0],
|
||||
[0.0, 10.0, 0.0],
|
||||
[-10.0, 0.0, 0.0],
|
||||
];
|
||||
circle.tangent_geoms.push(TangentGeom::PlanarCircle {
|
||||
center: [0.0, 0.0, 0.0],
|
||||
axis_x: [1.0, 0.0, 0.0],
|
||||
axis_y: [0.0, 1.0, 0.0],
|
||||
radius: 10.0,
|
||||
});
|
||||
circle.pattern_length = 1.0;
|
||||
circle.pattern = [0.5, -0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
|
||||
|
||||
let mut arc = WireModel::default();
|
||||
arc.points = vec![
|
||||
[10.0, 0.0, 0.0],
|
||||
[7.0, 7.0, 0.0],
|
||||
[0.0, 10.0, 0.0],
|
||||
];
|
||||
arc.tangent_geoms.push(TangentGeom::Arc {
|
||||
center: [0.0, 0.0, 0.0],
|
||||
axis_x: [1.0, 0.0, 0.0],
|
||||
axis_y: [0.0, 1.0, 0.0],
|
||||
radius: 10.0,
|
||||
start_angle: 0.0,
|
||||
end_angle: std::f64::consts::FRAC_PI_2,
|
||||
});
|
||||
arc.pattern_length = 1.0;
|
||||
arc.pattern = [0.5, -0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
|
||||
|
||||
let wires = vec![circle, arc];
|
||||
let depth_map = rustc_hash::FxHashMap::default();
|
||||
let partitioned = partition_wires(&wires, &depth_map);
|
||||
|
||||
assert_eq!(
|
||||
partitioned.circle_instances.len(),
|
||||
0,
|
||||
"patterned circular curves must not use the analytical circle path"
|
||||
);
|
||||
assert_eq!(
|
||||
partitioned.regular.len(),
|
||||
2,
|
||||
"patterned circle and arc must fall back to the regular dashed-wire path"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partition_wires_partitions_thick_and_tapered_arcs() {
|
||||
use crate::scene::model::wire_model::TangentGeom;
|
||||
|
||||
Reference in new issue
Block a user