diff --git a/.cargo/config.toml b/.cargo/config.toml index 88c43407a..de96c3e4a 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -18,3 +18,13 @@ rustflags = [ # WebAssembly stack while developing with `trunk serve`. '-Clink-arg=-zstack-size=4194304', ] + +[target.x86_64-pc-windows-gnu] +rustflags = [ + "-Clink-arg=-Wl,--stack,16777216", +] + +[target.x86_64-pc-windows-msvc] +rustflags = [ + "-Clink-arg=/STACK:16777216", +] diff --git a/Cargo.lock b/Cargo.lock index 002d00d86..f20ea980e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,7 +79,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.5.4" -source = "git+https://git@github.com/HakanSeven12/cadcodec.git?rev=ffc58a9ee64b2ee818d7cf1e4d8b27a26becbf01#ffc58a9ee64b2ee818d7cf1e4d8b27a26becbf01" +source = "git+https://git@github.com/HakanSeven12/cadcodec.git?rev=4128cc0eddea5cc288f050bf86f8f26df7dbc16c#4128cc0eddea5cc288f050bf86f8f26df7dbc16c" dependencies = [ "ahash 0.8.12", "anyhow", @@ -885,7 +885,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=e0f43546929b6af0e436e710bdfc932161d483a3#e0f43546929b6af0e436e710bdfc932161d483a3" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=33ec14bf212c61b4ee2677c06450b2a3da1c6d95#33ec14bf212c61b4ee2677c06450b2a3da1c6d95" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index 9932c9371..33b4ad92d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,8 +31,8 @@ glam = { version = "0.33", features = ["bytemuck"] } rfd = "0.17" clap = { version = "4", features = ["derive", "string"] } env_logger = "0.11" -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "ffc58a9ee64b2ee818d7cf1e4d8b27a26becbf01", features = ["serde"] } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "e0f43546929b6af0e436e710bdfc932161d483a3", features = ["acis", "offset"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "4128cc0eddea5cc288f050bf86f8f26df7dbc16c", features = ["serde"] } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "33ec14bf212c61b4ee2677c06450b2a3da1c6d95", features = ["acis", "offset"] } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } @@ -74,8 +74,7 @@ iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aa iced_widget = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } [patch."https://github.com/HakanSeven12/cadcodec.git"] -acadrust = { git = "https://git@github.com/HakanSeven12/cadcodec.git", rev = "ffc58a9ee64b2ee818d7cf1e4d8b27a26becbf01" } - +acadrust = { git = "https://git@github.com/HakanSeven12/cadcodec.git", rev = "4128cc0eddea5cc288f050bf86f8f26df7dbc16c" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] getrandom = "0.3" diff --git a/crates/ocs_doc_api/Cargo.toml b/crates/ocs_doc_api/Cargo.toml index a7d8b4119..c1f4d89cc 100644 --- a/crates/ocs_doc_api/Cargo.toml +++ b/crates/ocs_doc_api/Cargo.toml @@ -14,8 +14,8 @@ bincode = "1.3" thiserror = "1" # Host adapters convert ObjectId to acadrust handles; kernel handles stay local. -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "ffc58a9ee64b2ee818d7cf1e4d8b27a26becbf01", features = ["serde"], optional = true } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "e0f43546929b6af0e436e710bdfc932161d483a3", features = ["acis", "offset"], optional = true } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "4128cc0eddea5cc288f050bf86f8f26df7dbc16c", features = ["serde"], optional = true } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "33ec14bf212c61b4ee2677c06450b2a3da1c6d95", features = ["acis", "offset"], optional = true } ocs_plugin_api = { path = "../ocs_plugin_api", optional = true, features = ["host"] } [build-dependencies] diff --git a/crates/ocs_plugin_api/Cargo.toml b/crates/ocs_plugin_api/Cargo.toml index ac88fe336..4df55bf53 100644 --- a/crates/ocs_plugin_api/Cargo.toml +++ b/crates/ocs_plugin_api/Cargo.toml @@ -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 = "c91a1c1de89a0fbc006dc360ddb0251ac8c207d1", optional = true, features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "44cde65", optional = true, features = ["serde"] } # Runtime IPC and serialization (host feature only). interprocess = { version = "2", optional = true } @@ -38,7 +38,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 = "c91a1c1de89a0fbc006dc360ddb0251ac8c207d1", features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "44cde65", features = ["serde"] } [dev-dependencies] serde_json = "1" diff --git a/crates/ocs_web_worker/Cargo.toml b/crates/ocs_web_worker/Cargo.toml index a700c08c3..80b884411 100644 --- a/crates/ocs_web_worker/Cargo.toml +++ b/crates/ocs_web_worker/Cargo.toml @@ -8,7 +8,7 @@ publish = false crate-type = ["cdylib"] [dependencies] -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "c91a1c1de89a0fbc006dc360ddb0251ac8c207d1", features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "44cde65", features = ["serde"] } bincode = "1.3" serde = { version = "1", features = ["derive"] } console_error_panic_hook = "0.1" diff --git a/crates/plugin-template-api2/Cargo.toml b/crates/plugin-template-api2/Cargo.toml index 89cea10e6..7b7b0537c 100644 --- a/crates/plugin-template-api2/Cargo.toml +++ b/crates/plugin-template-api2/Cargo.toml @@ -10,4 +10,4 @@ crate-type = ["cdylib"] [dependencies] ocs_plugin_api = { path = "../ocs_plugin_api", features = ["host"] } -acadrust = { git = "https://github.com/ramox81/cadcodec.git", rev = "6dcda1a", features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "44cde65", features = ["serde"] } diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index d66f73249..0a39768ce 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -777,7 +777,20 @@ common = .slice-could-not-determine-the-solid-s-extent = SLICE: تعذر تحديد نطاق المجسم. .slice-cut-at-ax-value-kept-the-half = SLICE: تم القطع عند __ocs_fmt_0__=__ocs_fmt_1__، وأُبقي النصف __ocs_fmt_2__. .slice-select-exactly-one-solid-created-this-se = SLICE: حدد مجسمًا واحدًا بالضبط أُنشئ في هذه الجلسة. - .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: لا يقطع المستوى المجسم على الجانب المحتفظ به. + .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: لا يقطع المستوى المجسم على الجانب المحتفظ به. + .both = Both + .slice-select-at-least-one-target = SLICE: select at least one solid or surface. + .slice-invalid-plane = SLICE: the cutting plane is invalid. + .slice-side-point-on-plane = SLICE: choose a point away from the cutting plane. + .slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed. + .slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed. + .slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed. + .slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed. + .slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed. + .slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed. + .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. + .slice-retained-update-failed = SLICE: a retained result could not be updated. + .slice-created-results = SLICE: created __ocs_fmt_0__ result object(s). .splinefit-fit-a-spline-through-n-points = SPLINEFIT: تم تمرير منحنى عبر __ocs_fmt_0__ نقطة. .splinefit-need-at-least-3-points = SPLINEFIT: يلزم 3 نقاط على الأقل. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: حدد خطًا متعددًا لتمرير منحنى عبره. @@ -2207,7 +2220,27 @@ draw = .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: - .slice-offset-along-z = SLICE الإزاحة على Z: + .slice-offset-along-z = SLICE الإزاحة على Z: + .slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter: + .slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>: + .slice-first-plane-point = SLICE Specify first point on slicing plane: + .slice-second-plane-point = SLICE Specify second point on slicing plane: + .slice-third-plane-point = SLICE Specify third point on slicing plane: + .slice-point-on-plane = SLICE Specify a point on the slicing plane: + .slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis: + .slice-point-on-view-plane = SLICE Specify a point on the current view plane: + .slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane: + .slice-select-planar-object = SLICE Select a planar curve object: + .slice-select-planar-surface = SLICE Select a planar surface: + .slice-select-cutting-surface = SLICE Select a cutting surface: + .slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] : + .slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] : + .slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface: + .slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface: + .slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object: + .slice-planar-object = planar Object + .slice-zaxis = Zaxis + .slice-three-points = 3points .solid-specify-first-point = SOLID حدد النقطة الأولى: .solid-specify-fourth-point = SOLID حدد النقطة الرابعة: .solid-specify-second-point = SOLID حدد النقطة الثانية: @@ -3197,7 +3230,18 @@ model = .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .elliptical = إهليلجي - .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]: + .filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]: + .filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]: + .filletedge-enter-option-accept-next = Enter an option [Accept/Next] : + .filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]: + .filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>: + .filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>: + .filletedge-enter-expression = Enter expression: + .filletedge-stale-edge = A selected edge no longer belongs to the solid. + .filletedge-history-create-failed = The solid history could not be created. + .filletedge-history-append-failed = The fillet history could not be recorded. .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 78562a4d1..6e7a791ec 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -764,7 +764,20 @@ common = .slice-could-not-determine-the-solid-s-extent = SLICE: обхватът на тялото не можа да бъде определен. .slice-cut-at-ax-value-kept-the-half = SLICE: разрязано при __ocs_fmt_0__=__ocs_fmt_1__, запазена е __ocs_fmt_2__ половина. .slice-select-exactly-one-solid-created-this-se = SLICE: избери точно едно тяло, създадено в тази сесия. - .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: равнината не пресича тялото откъм запазената страна. + .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: равнината не пресича тялото откъм запазената страна. + .both = Both + .slice-select-at-least-one-target = SLICE: select at least one solid or surface. + .slice-invalid-plane = SLICE: the cutting plane is invalid. + .slice-side-point-on-plane = SLICE: choose a point away from the cutting plane. + .slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed. + .slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed. + .slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed. + .slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed. + .slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed. + .slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed. + .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. + .slice-retained-update-failed = SLICE: a retained result could not be updated. + .slice-created-results = SLICE: created __ocs_fmt_0__ result object(s). .splinefit-fit-a-spline-through-n-points = SPLINEFIT: сплайн прекаран през __ocs_fmt_0__ точки. .splinefit-need-at-least-3-points = SPLINEFIT: нужни са поне 3 точки. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: избери полилиния, през която да се прекара сплайн. @@ -2194,7 +2207,27 @@ draw = .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: - .slice-offset-along-z = SLICE отместване по Z: + .slice-offset-along-z = SLICE отместване по Z: + .slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter: + .slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>: + .slice-first-plane-point = SLICE Specify first point on slicing plane: + .slice-second-plane-point = SLICE Specify second point on slicing plane: + .slice-third-plane-point = SLICE Specify third point on slicing plane: + .slice-point-on-plane = SLICE Specify a point on the slicing plane: + .slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis: + .slice-point-on-view-plane = SLICE Specify a point on the current view plane: + .slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane: + .slice-select-planar-object = SLICE Select a planar curve object: + .slice-select-planar-surface = SLICE Select a planar surface: + .slice-select-cutting-surface = SLICE Select a cutting surface: + .slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] : + .slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] : + .slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface: + .slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface: + .slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object: + .slice-planar-object = planar Object + .slice-zaxis = Zaxis + .slice-three-points = 3points .solid-specify-first-point = SOLID Посочи първа точка: .solid-specify-fourth-point = SOLID Посочи четвърта точка: .solid-specify-second-point = SOLID Посочи втора точка: @@ -3184,7 +3217,18 @@ model = .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .elliptical = Елиптичен - .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]: + .filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]: + .filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]: + .filletedge-enter-option-accept-next = Enter an option [Accept/Next] : + .filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]: + .filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>: + .filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>: + .filletedge-enter-expression = Enter expression: + .filletedge-stale-edge = A selected edge no longer belongs to the solid. + .filletedge-history-create-failed = The solid history could not be created. + .filletedge-history-append-failed = The fillet history could not be recorded. .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index 3dbc60ece..fe39582e9 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -764,7 +764,20 @@ common = .slice-could-not-determine-the-solid-s-extent = SLICE: Nelze určit rozsah tělesa. .slice-cut-at-ax-value-kept-the-half = SLICE: řez na __ocs_fmt_0__=__ocs_fmt_1__, zachování poloviny __ocs_fmt_2__. .slice-select-exactly-one-solid-created-this-se = SLICE: vyberte přesně jedno těleso vytvořené v této relaci. - .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: rovina neprotíná těleso na držené straně. + .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: rovina neprotíná těleso na držené straně. + .both = Both + .slice-select-at-least-one-target = SLICE: select at least one solid or surface. + .slice-invalid-plane = SLICE: the cutting plane is invalid. + .slice-side-point-on-plane = SLICE: choose a point away from the cutting plane. + .slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed. + .slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed. + .slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed. + .slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed. + .slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed. + .slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed. + .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. + .slice-retained-update-failed = SLICE: a retained result could not be updated. + .slice-created-results = SLICE: created __ocs_fmt_0__ result object(s). .splinefit-fit-a-spline-through-n-points = SPLINEFIT: proložení spline přes body __ocs_fmt_0__. .splinefit-need-at-least-3-points =SPLINEFIT: potřebujete alespoň 3 body. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: vyberte křivku pro proložení spline. @@ -2194,7 +2207,27 @@ draw = .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: - .slice-offset-along-z = Odsazení SLICE podél Z: + .slice-offset-along-z = Odsazení SLICE podél Z: + .slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter: + .slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>: + .slice-first-plane-point = SLICE Specify first point on slicing plane: + .slice-second-plane-point = SLICE Specify second point on slicing plane: + .slice-third-plane-point = SLICE Specify third point on slicing plane: + .slice-point-on-plane = SLICE Specify a point on the slicing plane: + .slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis: + .slice-point-on-view-plane = SLICE Specify a point on the current view plane: + .slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane: + .slice-select-planar-object = SLICE Select a planar curve object: + .slice-select-planar-surface = SLICE Select a planar surface: + .slice-select-cutting-surface = SLICE Select a cutting surface: + .slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] : + .slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] : + .slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface: + .slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface: + .slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object: + .slice-planar-object = planar Object + .slice-zaxis = Zaxis + .slice-three-points = 3points .solid-specify-first-point = SOLID Zadejte první bod: .solid-specify-fourth-point = SOLID Zadejte čtvrtý bod: .solid-specify-second-point = SOLID Zadejte druhý bod: @@ -3182,7 +3215,18 @@ model = .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .elliptical = Eliptický - .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]: + .filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]: + .filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]: + .filletedge-enter-option-accept-next = Enter an option [Accept/Next] : + .filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]: + .filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>: + .filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>: + .filletedge-enter-expression = Enter expression: + .filletedge-stale-edge = A selected edge no longer belongs to the solid. + .filletedge-history-create-failed = The solid history could not be created. + .filletedge-history-append-failed = The fillet history could not be recorded. .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 32a5a2c40..e3f676f6a 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -765,7 +765,20 @@ common = .slice-could-not-determine-the-solid-s-extent = SLICE: konnte die Ausdehnung des Volumenkörper nicht bestimmen. .slice-cut-at-ax-value-kept-the-half = SLICE: geschnitten bei __ocs_fmt_0__=__ocs_fmt_1__, die Hälfte von __ocs_fmt_2__ beibehalten. .slice-select-exactly-one-solid-created-this-se = SLICE: Wählen Sie genau einen erstellten Volumenkörper. - .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: Die Ebene kreuzt den Volumenkörper auf der gehaltenen Seite nicht. + .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: Die Ebene kreuzt den Volumenkörper auf der gehaltenen Seite nicht. + .both = Both + .slice-select-at-least-one-target = SLICE: select at least one solid or surface. + .slice-invalid-plane = SLICE: the cutting plane is invalid. + .slice-side-point-on-plane = SLICE: choose a point away from the cutting plane. + .slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed. + .slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed. + .slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed. + .slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed. + .slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed. + .slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed. + .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. + .slice-retained-update-failed = SLICE: a retained result could not be updated. + .slice-created-results = SLICE: created __ocs_fmt_0__ result object(s). .splinefit-fit-a-spline-through-n-points = SPLINEFIT: Passen Sie einen Spline durch __ocs_fmt_0__ Punkte. .splinefit-need-at-least-3-points = SPLINEFIT: mindestens 3 Punkte werden benötigt. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: Wählen Sie eine Polylinie, um eine Spline einzupassen. @@ -2195,7 +2208,27 @@ draw = .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: - .slice-offset-along-z = SLICE Versetzt entlang Z: + .slice-offset-along-z = SLICE Versetzt entlang Z: + .slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter: + .slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>: + .slice-first-plane-point = SLICE Specify first point on slicing plane: + .slice-second-plane-point = SLICE Specify second point on slicing plane: + .slice-third-plane-point = SLICE Specify third point on slicing plane: + .slice-point-on-plane = SLICE Specify a point on the slicing plane: + .slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis: + .slice-point-on-view-plane = SLICE Specify a point on the current view plane: + .slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane: + .slice-select-planar-object = SLICE Select a planar curve object: + .slice-select-planar-surface = SLICE Select a planar surface: + .slice-select-cutting-surface = SLICE Select a cutting surface: + .slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] : + .slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] : + .slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface: + .slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface: + .slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object: + .slice-planar-object = planar Object + .slice-zaxis = Zaxis + .slice-three-points = 3points .solid-specify-first-point = SOLID Geben Sie den ersten Punkt an: .solid-specify-fourth-point = SOLID Vierter Punkt angeben: .solid-specify-second-point = SOLID Zweiter Punkt angeben: @@ -3183,7 +3216,18 @@ model = .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .elliptical = Elliptisch - .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]: + .filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]: + .filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]: + .filletedge-enter-option-accept-next = Enter an option [Accept/Next] : + .filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]: + .filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>: + .filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>: + .filletedge-enter-expression = Enter expression: + .filletedge-stale-edge = A selected edge no longer belongs to the solid. + .filletedge-history-create-failed = The solid history could not be created. + .filletedge-history-append-failed = The fillet history could not be recorded. .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 1298bc4fa..5dba873a3 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -766,7 +766,20 @@ common = .slice-could-not-determine-the-solid-s-extent = SLICE: δεν ήταν δυνατός ο προσδιορισμός της έκτασης του στερεού. .slice-cut-at-ax-value-kept-the-half = SLICE: τομή στο __ocs_fmt_0__=__ocs_fmt_1__, διατηρήθηκε το __ocs_fmt_2__ ήμισυ. .slice-select-exactly-one-solid-created-this-se = SLICE: επιλέξτε ακριβώς ένα στερεό που δημιουργήθηκε σε αυτή τη συνεδρία. - .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: το επίπεδο δεν τέμνει το στερεό στην πλευρά που διατηρείται. + .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: το επίπεδο δεν τέμνει το στερεό στην πλευρά που διατηρείται. + .both = Both + .slice-select-at-least-one-target = SLICE: select at least one solid or surface. + .slice-invalid-plane = SLICE: the cutting plane is invalid. + .slice-side-point-on-plane = SLICE: choose a point away from the cutting plane. + .slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed. + .slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed. + .slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed. + .slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed. + .slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed. + .slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed. + .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. + .slice-retained-update-failed = SLICE: a retained result could not be updated. + .slice-created-results = SLICE: created __ocs_fmt_0__ result object(s). .splinefit-fit-a-spline-through-n-points = SPLINEFIT: προσαρμογή spline μέσα από __ocs_fmt_0__ σημεία. .splinefit-need-at-least-3-points = SPLINEFIT: απαιτούνται τουλάχιστον 3 σημεία. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: επιλέξτε μια πολυγραμμή για προσαρμογή spline. @@ -2194,7 +2207,27 @@ draw = .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: - .slice-offset-along-z = SLICE μετατόπιση κατά Z: + .slice-offset-along-z = SLICE μετατόπιση κατά Z: + .slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter: + .slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>: + .slice-first-plane-point = SLICE Specify first point on slicing plane: + .slice-second-plane-point = SLICE Specify second point on slicing plane: + .slice-third-plane-point = SLICE Specify third point on slicing plane: + .slice-point-on-plane = SLICE Specify a point on the slicing plane: + .slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis: + .slice-point-on-view-plane = SLICE Specify a point on the current view plane: + .slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane: + .slice-select-planar-object = SLICE Select a planar curve object: + .slice-select-planar-surface = SLICE Select a planar surface: + .slice-select-cutting-surface = SLICE Select a cutting surface: + .slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] : + .slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] : + .slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface: + .slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface: + .slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object: + .slice-planar-object = planar Object + .slice-zaxis = Zaxis + .slice-three-points = 3points .solid-specify-first-point = SOLID Ορίστε πρώτο σημείο: .solid-specify-fourth-point = SOLID Ορίστε τέταρτο σημείο: .solid-specify-second-point = SOLID Ορίστε δεύτερο σημείο: @@ -3185,7 +3218,18 @@ model = .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .elliptical = Ελλειπτικό - .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]: + .filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]: + .filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]: + .filletedge-enter-option-accept-next = Enter an option [Accept/Next] : + .filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]: + .filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>: + .filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>: + .filletedge-enter-expression = Enter expression: + .filletedge-stale-edge = A selected edge no longer belongs to the solid. + .filletedge-history-create-failed = The solid history could not be created. + .filletedge-history-append-failed = The fillet history could not be recorded. .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 6c97312fe..1536065db 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -767,6 +767,19 @@ common = .slice-cut-at-ax-value-kept-the-half = SLICE: cut at __ocs_fmt_0__=__ocs_fmt_1__, kept the __ocs_fmt_2__ half. .slice-select-exactly-one-solid-created-this-se = SLICE: select exactly one solid created this session. .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: the plane does not cross the solid on the kept side. + .both = Both + .slice-select-at-least-one-target = SLICE: select at least one solid or surface. + .slice-invalid-plane = SLICE: the cutting plane is invalid. + .slice-side-point-on-plane = SLICE: choose a point away from the cutting plane. + .slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed. + .slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed. + .slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed. + .slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed. + .slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed. + .slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed. + .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. + .slice-retained-update-failed = SLICE: a retained result could not be updated. + .slice-created-results = SLICE: created __ocs_fmt_0__ result object(s). .splinefit-fit-a-spline-through-n-points = SPLINEFIT: fit a spline through __ocs_fmt_0__ points. .splinefit-need-at-least-3-points = SPLINEFIT: need at least 3 points. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: select a polyline to fit a spline through. @@ -2196,6 +2209,26 @@ draw = .slice-offset-along-x = SLICE offset along X: .slice-offset-along-y = SLICE offset along Y: .slice-offset-along-z = SLICE offset along Z: + .slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter: + .slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>: + .slice-first-plane-point = SLICE Specify first point on slicing plane: + .slice-second-plane-point = SLICE Specify second point on slicing plane: + .slice-third-plane-point = SLICE Specify third point on slicing plane: + .slice-point-on-plane = SLICE Specify a point on the slicing plane: + .slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis: + .slice-point-on-view-plane = SLICE Specify a point on the current view plane: + .slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane: + .slice-select-planar-object = SLICE Select a planar curve object: + .slice-select-planar-surface = SLICE Select a planar surface: + .slice-select-cutting-surface = SLICE Select a cutting surface: + .slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] : + .slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] : + .slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface: + .slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface: + .slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object: + .slice-planar-object = planar Object + .slice-zaxis = Zaxis + .slice-three-points = 3points .solid-specify-first-point = SOLID Specify first point: .solid-specify-fourth-point = SOLID Specify fourth point: .solid-specify-second-point = SOLID Specify second point: @@ -3187,6 +3220,17 @@ model = .cylinder-specify-axis-endpoint = CYLINDER Specify axis endpoint: .elliptical = Elliptical .select-a-solid-edge = Select a solid edge: + .filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]: + .filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]: + .filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]: + .filletedge-enter-option-accept-next = Enter an option [Accept/Next] : + .filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]: + .filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>: + .filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>: + .filletedge-enter-expression = Enter expression: + .filletedge-stale-edge = A selected edge no longer belongs to the solid. + .filletedge-history-create-failed = The solid history could not be created. + .filletedge-history-append-failed = The fillet history could not be recorded. .specify-fillet-radius = Specify fillet radius <__ocs_fmt_0__>: .specify-chamfer-distance = Specify chamfer distance <__ocs_fmt_0__>: .polysolid-height-width-justification = POLYSOLID Height = __ocs_fmt_0__, Width = __ocs_fmt_1__, Justification = __ocs_fmt_2__ diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 1c0f75e74..097d983ba 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -765,7 +765,20 @@ common = .slice-could-not-determine-the-solid-s-extent = SLICE: no se pudo determinar la extensión del sólido. .slice-cut-at-ax-value-kept-the-half = SLICE: corte en __ocs_fmt_0__=__ocs_fmt_1__, se conservó la mitad __ocs_fmt_2__. .slice-select-exactly-one-solid-created-this-se = SLICE: seleccione exactamente un sólido creado en esta sesión. - .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: el plano no atraviesa el sólido en el lado conservado. + .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: el plano no atraviesa el sólido en el lado conservado. + .both = Both + .slice-select-at-least-one-target = SLICE: select at least one solid or surface. + .slice-invalid-plane = SLICE: the cutting plane is invalid. + .slice-side-point-on-plane = SLICE: choose a point away from the cutting plane. + .slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed. + .slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed. + .slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed. + .slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed. + .slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed. + .slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed. + .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. + .slice-retained-update-failed = SLICE: a retained result could not be updated. + .slice-created-results = SLICE: created __ocs_fmt_0__ result object(s). .splinefit-fit-a-spline-through-n-points = SPLINEFIT: se ajustó una spline por __ocs_fmt_0__ puntos. .splinefit-need-at-least-3-points = SPLINEFIT: se necesitan al menos 3 puntos. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: seleccione una polilínea por la que ajustar una spline. @@ -2195,7 +2208,27 @@ draw = .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: - .slice-offset-along-z = SLICE desfase a lo largo de Z: + .slice-offset-along-z = SLICE desfase a lo largo de Z: + .slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter: + .slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>: + .slice-first-plane-point = SLICE Specify first point on slicing plane: + .slice-second-plane-point = SLICE Specify second point on slicing plane: + .slice-third-plane-point = SLICE Specify third point on slicing plane: + .slice-point-on-plane = SLICE Specify a point on the slicing plane: + .slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis: + .slice-point-on-view-plane = SLICE Specify a point on the current view plane: + .slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane: + .slice-select-planar-object = SLICE Select a planar curve object: + .slice-select-planar-surface = SLICE Select a planar surface: + .slice-select-cutting-surface = SLICE Select a cutting surface: + .slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] : + .slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] : + .slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface: + .slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface: + .slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object: + .slice-planar-object = planar Object + .slice-zaxis = Zaxis + .slice-three-points = 3points .solid-specify-first-point = SOLID Precise el primer punto: .solid-specify-fourth-point = SOLID Precise el cuarto punto: .solid-specify-second-point = SOLID Precise el segundo punto: @@ -3185,7 +3218,18 @@ model = .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .elliptical = Elíptico - .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]: + .filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]: + .filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]: + .filletedge-enter-option-accept-next = Enter an option [Accept/Next] : + .filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]: + .filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>: + .filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>: + .filletedge-enter-expression = Enter expression: + .filletedge-stale-edge = A selected edge no longer belongs to the solid. + .filletedge-history-create-failed = The solid history could not be created. + .filletedge-history-append-failed = The fillet history could not be recorded. .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index e54f22873..a3c76dc17 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -764,7 +764,20 @@ common = .slice-could-not-determine-the-solid-s-extent = SLICE: kiinteän aineen laajuutta ei voitu määrittää. .slice-cut-at-ax-value-kept-the-half = SLICE: leikattu kohdasta __ocs_fmt_0__=__ocs_fmt_1__, __ocs_fmt_2__ puolet säilytetty. .slice-select-exactly-one-solid-created-this-se = SLICE: valitse täsmälleen yksi tämän istunnon luotu solid. - .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: kone ei ylitä kiinteää pintaa säilytetyllä puolella. + .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: kone ei ylitä kiinteää pintaa säilytetyllä puolella. + .both = Both + .slice-select-at-least-one-target = SLICE: select at least one solid or surface. + .slice-invalid-plane = SLICE: the cutting plane is invalid. + .slice-side-point-on-plane = SLICE: choose a point away from the cutting plane. + .slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed. + .slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed. + .slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed. + .slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed. + .slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed. + .slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed. + .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. + .slice-retained-update-failed = SLICE: a retained result could not be updated. + .slice-created-results = SLICE: created __ocs_fmt_0__ result object(s). .splinefit-fit-a-spline-through-n-points = SPLINEFIT: sovita spline __ocs_fmt_0__-pisteiden läpi. .splinefit-need-at-least-3-points =SPLINEFIT: tarvitset vähintään 3 pistettä. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: valitse polyline sovittaaksesi splinin läpi. @@ -2194,7 +2207,27 @@ draw = .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: - .slice-offset-along-z = SLICE offset pitkin Z: + .slice-offset-along-z = SLICE offset pitkin Z: + .slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter: + .slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>: + .slice-first-plane-point = SLICE Specify first point on slicing plane: + .slice-second-plane-point = SLICE Specify second point on slicing plane: + .slice-third-plane-point = SLICE Specify third point on slicing plane: + .slice-point-on-plane = SLICE Specify a point on the slicing plane: + .slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis: + .slice-point-on-view-plane = SLICE Specify a point on the current view plane: + .slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane: + .slice-select-planar-object = SLICE Select a planar curve object: + .slice-select-planar-surface = SLICE Select a planar surface: + .slice-select-cutting-surface = SLICE Select a cutting surface: + .slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] : + .slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] : + .slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface: + .slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface: + .slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object: + .slice-planar-object = planar Object + .slice-zaxis = Zaxis + .slice-three-points = 3points .solid-specify-first-point = SOLID Määritä ensimmäinen piste: .solid-specify-fourth-point = SOLID Määritä neljäs piste: .solid-specify-second-point = SOLID Määritä toinen piste: @@ -3184,7 +3217,18 @@ model = .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .elliptical = Elliptinen - .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]: + .filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]: + .filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]: + .filletedge-enter-option-accept-next = Enter an option [Accept/Next] : + .filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]: + .filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>: + .filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>: + .filletedge-enter-expression = Enter expression: + .filletedge-stale-edge = A selected edge no longer belongs to the solid. + .filletedge-history-create-failed = The solid history could not be created. + .filletedge-history-append-failed = The fillet history could not be recorded. .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 1666ae44b..3e0fa73e0 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -765,7 +765,20 @@ common = .slice-could-not-determine-the-solid-s-extent = SLICE : ne pouvait déterminer l'étendue du solide. .slice-cut-at-ax-value-kept-the-half = SLICE: coupé à __ocs_fmt_0__=__ocs_fmt_1__, maintenu la moitié __ocs_fmt_2__. .slice-select-exactly-one-solid-created-this-se = SLICE : sélectionnez exactement un solide créé cette session. - .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: le plan ne traverse pas le solide du côté retenu. + .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: le plan ne traverse pas le solide du côté retenu. + .both = Both + .slice-select-at-least-one-target = SLICE: select at least one solid or surface. + .slice-invalid-plane = SLICE: the cutting plane is invalid. + .slice-side-point-on-plane = SLICE: choose a point away from the cutting plane. + .slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed. + .slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed. + .slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed. + .slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed. + .slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed. + .slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed. + .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. + .slice-retained-update-failed = SLICE: a retained result could not be updated. + .slice-created-results = SLICE: created __ocs_fmt_0__ result object(s). .splinefit-fit-a-spline-through-n-points = SPLINEFIT: adaptez une élingue à des points __ocs_fmt_0__. .splinefit-need-at-least-3-points = SPLINEFIT: nécessite au moins 3 points. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT : sélectionnez une polyligne pour l'adapter à une spline. @@ -2195,7 +2208,27 @@ draw = .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: - .slice-offset-along-z = SLICE décalage le long de Z: + .slice-offset-along-z = SLICE décalage le long de Z: + .slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter: + .slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>: + .slice-first-plane-point = SLICE Specify first point on slicing plane: + .slice-second-plane-point = SLICE Specify second point on slicing plane: + .slice-third-plane-point = SLICE Specify third point on slicing plane: + .slice-point-on-plane = SLICE Specify a point on the slicing plane: + .slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis: + .slice-point-on-view-plane = SLICE Specify a point on the current view plane: + .slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane: + .slice-select-planar-object = SLICE Select a planar curve object: + .slice-select-planar-surface = SLICE Select a planar surface: + .slice-select-cutting-surface = SLICE Select a cutting surface: + .slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] : + .slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] : + .slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface: + .slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface: + .slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object: + .slice-planar-object = planar Object + .slice-zaxis = Zaxis + .slice-three-points = 3points .solid-specify-first-point = SOLID Préciser le premier point: .solid-specify-fourth-point = SOLID Préciser le quatrième point: .solid-specify-second-point = SOLID Préciser le deuxième point: @@ -3185,7 +3218,18 @@ model = .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .elliptical = Elliptique - .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]: + .filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]: + .filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]: + .filletedge-enter-option-accept-next = Enter an option [Accept/Next] : + .filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]: + .filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>: + .filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>: + .filletedge-enter-expression = Enter expression: + .filletedge-stale-edge = A selected edge no longer belongs to the solid. + .filletedge-history-create-failed = The solid history could not be created. + .filletedge-history-append-failed = The fillet history could not be recorded. .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 1a9d1d2b2..ef77a707c 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -753,7 +753,20 @@ common = .slice-could-not-determine-the-solid-s-extent = SLICE: ठोस की सीमा निर्धारित नहीं कर सकता है। .slice-cut-at-ax-value-kept-the-half = SLICE: __ocs_fmt_0__ = __ocs_fmt_1__ में कटौती, __ocs_fmt_2__ आधा रखा। .slice-select-exactly-one-solid-created-this-se = SLICE: इस सत्र को ठीक से एक ठोस बनाया गया चुनें। - .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: विमान को रखा पक्ष पर ठोस पार नहीं किया जाता है। + .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: विमान को रखा पक्ष पर ठोस पार नहीं किया जाता है। + .both = Both + .slice-select-at-least-one-target = SLICE: select at least one solid or surface. + .slice-invalid-plane = SLICE: the cutting plane is invalid. + .slice-side-point-on-plane = SLICE: choose a point away from the cutting plane. + .slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed. + .slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed. + .slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed. + .slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed. + .slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed. + .slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed. + .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. + .slice-retained-update-failed = SLICE: a retained result could not be updated. + .slice-created-results = SLICE: created __ocs_fmt_0__ result object(s). .splinefit-fit-a-spline-through-n-points = SPLINEFIT: __ocs_fmt_0__ अंक के माध्यम से एक स्पैन फिट। .splinefit-need-at-least-3-points = SPLINEFIT: कम से कम 3 अंक की आवश्यकता है। .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: एक sline के माध्यम से फिट करने के लिए एक पॉलीलाइन का चयन करें। @@ -2183,7 +2196,27 @@ draw = .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 वाई के साथ ऑफसेट: - .slice-offset-along-z = SLICE Z के साथ ऑफसेट: + .slice-offset-along-z = SLICE Z के साथ ऑफसेट: + .slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter: + .slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>: + .slice-first-plane-point = SLICE Specify first point on slicing plane: + .slice-second-plane-point = SLICE Specify second point on slicing plane: + .slice-third-plane-point = SLICE Specify third point on slicing plane: + .slice-point-on-plane = SLICE Specify a point on the slicing plane: + .slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis: + .slice-point-on-view-plane = SLICE Specify a point on the current view plane: + .slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane: + .slice-select-planar-object = SLICE Select a planar curve object: + .slice-select-planar-surface = SLICE Select a planar surface: + .slice-select-cutting-surface = SLICE Select a cutting surface: + .slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] : + .slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] : + .slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface: + .slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface: + .slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object: + .slice-planar-object = planar Object + .slice-zaxis = Zaxis + .slice-three-points = 3points .solid-specify-first-point = SOLID पहली बिंदु निर्दिष्ट करें: .solid-specify-fourth-point = SOLID चौथे बिंदु को निर्दिष्ट करें: .solid-specify-second-point = SOLID दूसरे बिंदु को निर्दिष्ट करें: @@ -3173,7 +3206,18 @@ model = .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .elliptical = दीर्घवृत्तीय - .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]: + .filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]: + .filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]: + .filletedge-enter-option-accept-next = Enter an option [Accept/Next] : + .filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]: + .filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>: + .filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>: + .filletedge-enter-expression = Enter expression: + .filletedge-stale-edge = A selected edge no longer belongs to the solid. + .filletedge-history-create-failed = The solid history could not be created. + .filletedge-history-append-failed = The fillet history could not be recorded. .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index fcbc9dce5..f1d65d13b 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -764,7 +764,20 @@ common = .slice-could-not-determine-the-solid-s-extent = SLICE: nem tudta meghatározni a szilárdtest kiterjedését. .slice-cut-at-ax-value-kept-the-half = SLICE: vágás a __ocs_fmt_0__=__ocs_fmt_1__ pontnál, a __ocs_fmt_2__ felét megtartotta. .slice-select-exactly-one-solid-created-this-se = SLICE: válasszon ki pontosan egy szilárdtestet, amelyet ebben a munkamenetben hozott létre. - .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: a sík nem keresztezi a szilárdtestet a megtartott oldalon. + .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: a sík nem keresztezi a szilárdtestet a megtartott oldalon. + .both = Both + .slice-select-at-least-one-target = SLICE: select at least one solid or surface. + .slice-invalid-plane = SLICE: the cutting plane is invalid. + .slice-side-point-on-plane = SLICE: choose a point away from the cutting plane. + .slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed. + .slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed. + .slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed. + .slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed. + .slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed. + .slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed. + .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. + .slice-retained-update-failed = SLICE: a retained result could not be updated. + .slice-created-results = SLICE: created __ocs_fmt_0__ result object(s). .splinefit-fit-a-spline-through-n-points = SPLINEFIT: illesszen be egy spline-t a __ocs_fmt_0__ pontokon. .splinefit-need-at-least-3-points =SPLINEFIT: legalább 3 pont szükséges. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: válasszon egy vonalláncot a spline átillesztéséhez. @@ -2194,7 +2207,27 @@ draw = .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: - .slice-offset-along-z = SLICE eltolás a Z mentén: + .slice-offset-along-z = SLICE eltolás a Z mentén: + .slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter: + .slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>: + .slice-first-plane-point = SLICE Specify first point on slicing plane: + .slice-second-plane-point = SLICE Specify second point on slicing plane: + .slice-third-plane-point = SLICE Specify third point on slicing plane: + .slice-point-on-plane = SLICE Specify a point on the slicing plane: + .slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis: + .slice-point-on-view-plane = SLICE Specify a point on the current view plane: + .slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane: + .slice-select-planar-object = SLICE Select a planar curve object: + .slice-select-planar-surface = SLICE Select a planar surface: + .slice-select-cutting-surface = SLICE Select a cutting surface: + .slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] : + .slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] : + .slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface: + .slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface: + .slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object: + .slice-planar-object = planar Object + .slice-zaxis = Zaxis + .slice-three-points = 3points .solid-specify-first-point = SOLID Adja meg az első pontot: .solid-specify-fourth-point = SOLID Adja meg a negyedik pontot: .solid-specify-second-point = SOLID Második pont megadása: @@ -3184,7 +3217,18 @@ model = .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .elliptical = Elliptikus - .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]: + .filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]: + .filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]: + .filletedge-enter-option-accept-next = Enter an option [Accept/Next] : + .filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]: + .filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>: + .filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>: + .filletedge-enter-expression = Enter expression: + .filletedge-stale-edge = A selected edge no longer belongs to the solid. + .filletedge-history-create-failed = The solid history could not be created. + .filletedge-history-append-failed = The fillet history could not be recorded. .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index fb5be76da..581b0a6ec 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -764,7 +764,20 @@ common = .slice-could-not-determine-the-solid-s-extent = SLICE: impossibile determinare l'estensione del solido. .slice-cut-at-ax-value-kept-the-half = SLICE: tagliato a __ocs_fmt_0__=__ocs_fmt_1__, ha mantenuto la metà __ocs_fmt_2__. .slice-select-exactly-one-solid-created-this-se = SLICE: seleziona esattamente un solido creato in questa sessione. - .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: il piano non attraversa il solido dal lato mantenuto. + .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: il piano non attraversa il solido dal lato mantenuto. + .both = Both + .slice-select-at-least-one-target = SLICE: select at least one solid or surface. + .slice-invalid-plane = SLICE: the cutting plane is invalid. + .slice-side-point-on-plane = SLICE: choose a point away from the cutting plane. + .slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed. + .slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed. + .slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed. + .slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed. + .slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed. + .slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed. + .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. + .slice-retained-update-failed = SLICE: a retained result could not be updated. + .slice-created-results = SLICE: created __ocs_fmt_0__ result object(s). .splinefit-fit-a-spline-through-n-points = SPLINEFIT: adatta una spline attraverso i punti __ocs_fmt_0__. .splinefit-need-at-least-3-points =SPLINEFIT: servono almeno 3 punti. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: seleziona una polilinea attraverso la quale inserire una spline. @@ -2194,7 +2207,27 @@ draw = .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: - .slice-offset-along-z = Scostamento SLICE lungo Z: + .slice-offset-along-z = Scostamento SLICE lungo Z: + .slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter: + .slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>: + .slice-first-plane-point = SLICE Specify first point on slicing plane: + .slice-second-plane-point = SLICE Specify second point on slicing plane: + .slice-third-plane-point = SLICE Specify third point on slicing plane: + .slice-point-on-plane = SLICE Specify a point on the slicing plane: + .slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis: + .slice-point-on-view-plane = SLICE Specify a point on the current view plane: + .slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane: + .slice-select-planar-object = SLICE Select a planar curve object: + .slice-select-planar-surface = SLICE Select a planar surface: + .slice-select-cutting-surface = SLICE Select a cutting surface: + .slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] : + .slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] : + .slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface: + .slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface: + .slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object: + .slice-planar-object = planar Object + .slice-zaxis = Zaxis + .slice-three-points = 3points .solid-specify-first-point = SOLID Specificare il primo punto: .solid-specify-fourth-point = SOLID Specificare il quarto punto: .solid-specify-second-point = SOLID Specificare il secondo punto: @@ -3184,7 +3217,18 @@ model = .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .elliptical = Ellittico - .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]: + .filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]: + .filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]: + .filletedge-enter-option-accept-next = Enter an option [Accept/Next] : + .filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]: + .filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>: + .filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>: + .filletedge-enter-expression = Enter expression: + .filletedge-stale-edge = A selected edge no longer belongs to the solid. + .filletedge-history-create-failed = The solid history could not be created. + .filletedge-history-append-failed = The fillet history could not be recorded. .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index ef8e5b9db..f43975364 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -755,7 +755,20 @@ common = .slice-could-not-determine-the-solid-s-extent = SLICE: ソリッドの範囲を特定できませんでした。 .slice-cut-at-ax-value-kept-the-half = SLICE: __ocs_fmt_0__=__ocs_fmt_1__ で切断し、__ocs_fmt_2__ 側を保持しました。 .slice-select-exactly-one-solid-created-this-se = SLICE: このセッションで作成したソリッドを 1 つ選択してください。 - .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: 保持側で平面がソリッドと交差しません。 + .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: 保持側で平面がソリッドと交差しません。 + .both = Both + .slice-select-at-least-one-target = SLICE: select at least one solid or surface. + .slice-invalid-plane = SLICE: the cutting plane is invalid. + .slice-side-point-on-plane = SLICE: choose a point away from the cutting plane. + .slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed. + .slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed. + .slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed. + .slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed. + .slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed. + .slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed. + .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. + .slice-retained-update-failed = SLICE: a retained result could not be updated. + .slice-created-results = SLICE: created __ocs_fmt_0__ result object(s). .splinefit-fit-a-spline-through-n-points = SPLINEFIT: __ocs_fmt_0__ 点を通るスプラインを作成しました。 .splinefit-need-at-least-3-points = SPLINEFIT: 3 点以上必要です。 .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: スプラインを通すポリラインを選択してください。 @@ -2185,7 +2198,27 @@ draw = .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 方向オフセット: - .slice-offset-along-z = SLICE Z 方向オフセット: + .slice-offset-along-z = SLICE Z 方向オフセット: + .slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter: + .slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>: + .slice-first-plane-point = SLICE Specify first point on slicing plane: + .slice-second-plane-point = SLICE Specify second point on slicing plane: + .slice-third-plane-point = SLICE Specify third point on slicing plane: + .slice-point-on-plane = SLICE Specify a point on the slicing plane: + .slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis: + .slice-point-on-view-plane = SLICE Specify a point on the current view plane: + .slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane: + .slice-select-planar-object = SLICE Select a planar curve object: + .slice-select-planar-surface = SLICE Select a planar surface: + .slice-select-cutting-surface = SLICE Select a cutting surface: + .slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] : + .slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] : + .slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface: + .slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface: + .slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object: + .slice-planar-object = planar Object + .slice-zaxis = Zaxis + .slice-three-points = 3points .solid-specify-first-point = SOLID 1 点目を指定: .solid-specify-fourth-point = SOLID 4 点目を指定: .solid-specify-second-point = SOLID 2 点目を指定: @@ -3173,7 +3206,18 @@ model = .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .elliptical = 楕円 - .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]: + .filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]: + .filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]: + .filletedge-enter-option-accept-next = Enter an option [Accept/Next] : + .filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]: + .filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>: + .filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>: + .filletedge-enter-expression = Enter expression: + .filletedge-stale-edge = A selected edge no longer belongs to the solid. + .filletedge-history-create-failed = The solid history could not be created. + .filletedge-history-append-failed = The fillet history could not be recorded. .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index f2924828c..f3f85d334 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -764,7 +764,20 @@ common = .slice-could-not-determine-the-solid-s-extent = SLICE: 솔리드의 범위를 확인할 수 없습니다. .slice-cut-at-ax-value-kept-the-half = SLICE: __ocs_fmt_0__=__ocs_fmt_1__에서 절단하고 __ocs_fmt_2__ 절반을 유지합니다. .slice-select-exactly-one-solid-created-this-se = SLICE: 이번 세션에서 생성된 정확히 하나의 솔리드를 선택합니다. - .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: 평면이 유지된 측면의 솔리드를 교차하지 않습니다. + .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: 평면이 유지된 측면의 솔리드를 교차하지 않습니다. + .both = Both + .slice-select-at-least-one-target = SLICE: select at least one solid or surface. + .slice-invalid-plane = SLICE: the cutting plane is invalid. + .slice-side-point-on-plane = SLICE: choose a point away from the cutting plane. + .slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed. + .slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed. + .slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed. + .slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed. + .slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed. + .slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed. + .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. + .slice-retained-update-failed = SLICE: a retained result could not be updated. + .slice-created-results = SLICE: created __ocs_fmt_0__ result object(s). .splinefit-fit-a-spline-through-n-points = SPLINEFIT: __ocs_fmt_0__ 점을 통해 스플라인을 맞춥니다. .splinefit-need-at-least-3-points =SPLINEFIT: 최소 3개의 포인트가 필요합니다. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: 스플라인에 맞게 폴리선을 선택합니다. @@ -2194,7 +2207,27 @@ draw = .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 오프셋: - .slice-offset-along-z = Z를 따라 SLICE 오프셋: + .slice-offset-along-z = Z를 따라 SLICE 오프셋: + .slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter: + .slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>: + .slice-first-plane-point = SLICE Specify first point on slicing plane: + .slice-second-plane-point = SLICE Specify second point on slicing plane: + .slice-third-plane-point = SLICE Specify third point on slicing plane: + .slice-point-on-plane = SLICE Specify a point on the slicing plane: + .slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis: + .slice-point-on-view-plane = SLICE Specify a point on the current view plane: + .slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane: + .slice-select-planar-object = SLICE Select a planar curve object: + .slice-select-planar-surface = SLICE Select a planar surface: + .slice-select-cutting-surface = SLICE Select a cutting surface: + .slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] : + .slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] : + .slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface: + .slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface: + .slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object: + .slice-planar-object = planar Object + .slice-zaxis = Zaxis + .slice-three-points = 3points .solid-specify-first-point = SOLID 첫 번째 점 지정: .solid-specify-fourth-point = SOLID 네 번째 점 지정: .solid-specify-second-point = SOLID 두 번째 점 지정: @@ -3184,7 +3217,18 @@ model = .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .elliptical = 타원형 - .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]: + .filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]: + .filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]: + .filletedge-enter-option-accept-next = Enter an option [Accept/Next] : + .filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]: + .filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>: + .filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>: + .filletedge-enter-expression = Enter expression: + .filletedge-stale-edge = A selected edge no longer belongs to the solid. + .filletedge-history-create-failed = The solid history could not be created. + .filletedge-history-append-failed = The fillet history could not be recorded. .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 93a868eef..ce817e236 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -763,7 +763,20 @@ common = .slice-could-not-determine-the-solid-s-extent = SLICE: kon de omvang van de vaste stof niet bepalen. .slice-cut-at-ax-value-kept-the-half = SLICE: knippen met __ocs_fmt_0__=__ocs_fmt_1__, de helft __ocs_fmt_2__ behouden. .slice-select-exactly-one-solid-created-this-se = SLICE: selecteer precies één vaste die deze sessie aangemaakt heeft. - .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: het vlak kruist de vaste stof niet aan de kant van de bewaarde zijde. + .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: het vlak kruist de vaste stof niet aan de kant van de bewaarde zijde. + .both = Both + .slice-select-at-least-one-target = SLICE: select at least one solid or surface. + .slice-invalid-plane = SLICE: the cutting plane is invalid. + .slice-side-point-on-plane = SLICE: choose a point away from the cutting plane. + .slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed. + .slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed. + .slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed. + .slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed. + .slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed. + .slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed. + .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. + .slice-retained-update-failed = SLICE: a retained result could not be updated. + .slice-created-results = SLICE: created __ocs_fmt_0__ result object(s). .splinefit-fit-a-spline-through-n-points = SPLINEFIT: pas een spline door __ocs_fmt_0__ punten. .splinefit-need-at-least-3-points = SPLINEFIT: minimaal 3 punten nodig. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: selecteer een polylijn om een spline door te passen. @@ -2195,7 +2208,27 @@ draw = .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: - .slice-offset-along-z = SLICE offset langs Z: + .slice-offset-along-z = SLICE offset langs Z: + .slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter: + .slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>: + .slice-first-plane-point = SLICE Specify first point on slicing plane: + .slice-second-plane-point = SLICE Specify second point on slicing plane: + .slice-third-plane-point = SLICE Specify third point on slicing plane: + .slice-point-on-plane = SLICE Specify a point on the slicing plane: + .slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis: + .slice-point-on-view-plane = SLICE Specify a point on the current view plane: + .slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane: + .slice-select-planar-object = SLICE Select a planar curve object: + .slice-select-planar-surface = SLICE Select a planar surface: + .slice-select-cutting-surface = SLICE Select a cutting surface: + .slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] : + .slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] : + .slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface: + .slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface: + .slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object: + .slice-planar-object = planar Object + .slice-zaxis = Zaxis + .slice-three-points = 3points .solid-specify-first-point = SOLID Geef het eerste punt op: .solid-specify-fourth-point = SOLID Geef het vierde punt op: .solid-specify-second-point = SOLID Tweede punt specificeren: @@ -2773,9 +2806,7 @@ annotate = .markup = Markering .multileader = Multiverwijslijn .multileader-draw-order = Tekenvolgorde multiverwijslijn: - .multiline-text = - Multiline - Tekst + .multiline-text = Multilijntekst .ordinate = Ordinaat .out = Uit .qdim-select-geometry-to-dimension-enter-when-d = QDIM Selecteer de te bematen geometrie (Enter om te voltooien): @@ -3142,7 +3173,7 @@ model = .revolve-entity = Rotatieobject .revolve-flags = Vlaggen omdraaien .spherical = Sferisch - .subtract = Aftrekken + .subtract = Verschil .surface-construction = Oppervlaktebouw .sweep-path = Vegen / Pad .sweep-entity = Sweepobject @@ -3154,7 +3185,7 @@ model = .u-vector-1 = U-vector 1 .u-vector-2 = U-vector 2 .uid = UID - .union = Unie + .union = Samenvoegen .v-isolines = V-isolijnen .v-vector-1 = Vector 1 .v-vector-2 = Vector 2 @@ -3185,7 +3216,18 @@ model = .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .elliptical = Elliptisch - .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]: + .filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]: + .filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]: + .filletedge-enter-option-accept-next = Enter an option [Accept/Next] : + .filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]: + .filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>: + .filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>: + .filletedge-enter-expression = Enter expression: + .filletedge-stale-edge = A selected edge no longer belongs to the solid. + .filletedge-history-create-failed = The solid history could not be created. + .filletedge-history-append-failed = The fillet history could not be recorded. .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ @@ -3880,7 +3922,7 @@ properties = .monochrome = Monochroom .more-colors = Meer kleuren... .multileader = Multiverwijslijn - .multiline = Multiline + .multiline = Multilijntekst .multileader-style = Multiverwijslijnstijl .multiple-lines = Meerdere regels .n-closed = N gesloten diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 28cb72201..370f2eca9 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -764,7 +764,20 @@ common = .slice-could-not-determine-the-solid-s-extent = SLICE: nie można określić zasięgu bryły. .slice-cut-at-ax-value-kept-the-half = SLICE: cięcie w __ocs_fmt_0__=__ocs_fmt_1__, zachowanie połowy __ocs_fmt_2__. .slice-select-exactly-one-solid-created-this-se = SLICE: wybierz dokładnie jedną bryłę utworzoną w tej sesji. - .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: płaszczyzna nie przecina bryły po zachowanej stronie. + .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: płaszczyzna nie przecina bryły po zachowanej stronie. + .both = Both + .slice-select-at-least-one-target = SLICE: select at least one solid or surface. + .slice-invalid-plane = SLICE: the cutting plane is invalid. + .slice-side-point-on-plane = SLICE: choose a point away from the cutting plane. + .slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed. + .slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed. + .slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed. + .slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed. + .slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed. + .slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed. + .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. + .slice-retained-update-failed = SLICE: a retained result could not be updated. + .slice-created-results = SLICE: created __ocs_fmt_0__ result object(s). .splinefit-fit-a-spline-through-n-points = SPLINEFIT: dopasuj splajn przez punkty __ocs_fmt_0__. .splinefit-need-at-least-3-points =SPLINEFIT: potrzebne są co najmniej 3 punkty. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: wybierz polilinię, przez którą ma zostać dopasowany splajn. @@ -2194,7 +2207,27 @@ draw = .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: - .slice-offset-along-z = Przesunięcie SLICE wzdłuż Z: + .slice-offset-along-z = Przesunięcie SLICE wzdłuż Z: + .slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter: + .slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>: + .slice-first-plane-point = SLICE Specify first point on slicing plane: + .slice-second-plane-point = SLICE Specify second point on slicing plane: + .slice-third-plane-point = SLICE Specify third point on slicing plane: + .slice-point-on-plane = SLICE Specify a point on the slicing plane: + .slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis: + .slice-point-on-view-plane = SLICE Specify a point on the current view plane: + .slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane: + .slice-select-planar-object = SLICE Select a planar curve object: + .slice-select-planar-surface = SLICE Select a planar surface: + .slice-select-cutting-surface = SLICE Select a cutting surface: + .slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] : + .slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] : + .slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface: + .slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface: + .slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object: + .slice-planar-object = planar Object + .slice-zaxis = Zaxis + .slice-three-points = 3points .solid-specify-first-point = SOLID Określ pierwszy punkt: .solid-specify-fourth-point = SOLID Określ czwarty punkt: .solid-specify-second-point = SOLID Określ drugi punkt: @@ -3184,7 +3217,18 @@ model = .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .elliptical = Eliptyczny - .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]: + .filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]: + .filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]: + .filletedge-enter-option-accept-next = Enter an option [Accept/Next] : + .filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]: + .filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>: + .filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>: + .filletedge-enter-expression = Enter expression: + .filletedge-stale-edge = A selected edge no longer belongs to the solid. + .filletedge-history-create-failed = The solid history could not be created. + .filletedge-history-append-failed = The fillet history could not be recorded. .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 64f5c9416..2e938bf5e 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -765,7 +765,20 @@ common = .slice-could-not-determine-the-solid-s-extent = SLICE: não foi possível determinar a extensão do sólido. .slice-cut-at-ax-value-kept-the-half = SLICE: corte em __ocs_fmt_0__=__ocs_fmt_1__, metade __ocs_fmt_2__ mantida. .slice-select-exactly-one-solid-created-this-se = SLICE: selecione exatamente um sólido criado nesta sessão. - .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: o plano não atravessa o sólido no lado mantido. + .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: o plano não atravessa o sólido no lado mantido. + .both = Both + .slice-select-at-least-one-target = SLICE: select at least one solid or surface. + .slice-invalid-plane = SLICE: the cutting plane is invalid. + .slice-side-point-on-plane = SLICE: choose a point away from the cutting plane. + .slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed. + .slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed. + .slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed. + .slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed. + .slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed. + .slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed. + .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. + .slice-retained-update-failed = SLICE: a retained result could not be updated. + .slice-created-results = SLICE: created __ocs_fmt_0__ result object(s). .splinefit-fit-a-spline-through-n-points = SPLINEFIT: spline ajustada através de __ocs_fmt_0__ pontos. .splinefit-need-at-least-3-points = SPLINEFIT: são necessários pelo menos 3 pontos. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: selecione uma polilinha através da qual ajustar uma spline. @@ -2195,7 +2208,27 @@ draw = .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: - .slice-offset-along-z = SLICE deslocamento ao longo de Z: + .slice-offset-along-z = SLICE deslocamento ao longo de Z: + .slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter: + .slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>: + .slice-first-plane-point = SLICE Specify first point on slicing plane: + .slice-second-plane-point = SLICE Specify second point on slicing plane: + .slice-third-plane-point = SLICE Specify third point on slicing plane: + .slice-point-on-plane = SLICE Specify a point on the slicing plane: + .slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis: + .slice-point-on-view-plane = SLICE Specify a point on the current view plane: + .slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane: + .slice-select-planar-object = SLICE Select a planar curve object: + .slice-select-planar-surface = SLICE Select a planar surface: + .slice-select-cutting-surface = SLICE Select a cutting surface: + .slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] : + .slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] : + .slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface: + .slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface: + .slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object: + .slice-planar-object = planar Object + .slice-zaxis = Zaxis + .slice-three-points = 3points .solid-specify-first-point = SOLID Especifique o primeiro ponto: .solid-specify-fourth-point = SOLID Especifique o quarto ponto: .solid-specify-second-point = SOLID Especifique o segundo ponto: @@ -3185,7 +3218,18 @@ model = .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .elliptical = Elíptico - .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]: + .filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]: + .filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]: + .filletedge-enter-option-accept-next = Enter an option [Accept/Next] : + .filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]: + .filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>: + .filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>: + .filletedge-enter-expression = Enter expression: + .filletedge-stale-edge = A selected edge no longer belongs to the solid. + .filletedge-history-create-failed = The solid history could not be created. + .filletedge-history-append-failed = The fillet history could not be recorded. .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index f0b563824..f7615b132 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -768,7 +768,20 @@ common = .slice-could-not-determine-the-solid-s-extent = SLICE: не может определить степень твердого тела. .slice-cut-at-ax-value-kept-the-half = SLICE: вырезать на __ocs_fmt_0__=__ocs_fmt_1__, сохранить __ocs_fmt_2__ половину. .slice-select-exactly-one-solid-created-this-se = SLICE: выберите один твердотельный, созданный на этом сеансе. - .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: плоскость не пересекает твердую часть на удерживаемой стороне. + .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: плоскость не пересекает твердую часть на удерживаемой стороне. + .both = Both + .slice-select-at-least-one-target = SLICE: select at least one solid or surface. + .slice-invalid-plane = SLICE: the cutting plane is invalid. + .slice-side-point-on-plane = SLICE: choose a point away from the cutting plane. + .slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed. + .slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed. + .slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed. + .slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed. + .slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed. + .slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed. + .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. + .slice-retained-update-failed = SLICE: a retained result could not be updated. + .slice-created-results = SLICE: created __ocs_fmt_0__ result object(s). .splinefit-fit-a-spline-through-n-points = SPLINEFIT: проведите сплин через точки __ocs_fmt_0__. .splinefit-need-at-least-3-points = SPLINEFIT: необходимо не менее 3 баллов. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: выберите полилин, чтобы соответствовать сплину. @@ -2198,7 +2211,27 @@ draw = .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: - .slice-offset-along-z = SLICE Смещение вдоль Z: + .slice-offset-along-z = SLICE Смещение вдоль Z: + .slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter: + .slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>: + .slice-first-plane-point = SLICE Specify first point on slicing plane: + .slice-second-plane-point = SLICE Specify second point on slicing plane: + .slice-third-plane-point = SLICE Specify third point on slicing plane: + .slice-point-on-plane = SLICE Specify a point on the slicing plane: + .slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis: + .slice-point-on-view-plane = SLICE Specify a point on the current view plane: + .slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane: + .slice-select-planar-object = SLICE Select a planar curve object: + .slice-select-planar-surface = SLICE Select a planar surface: + .slice-select-cutting-surface = SLICE Select a cutting surface: + .slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] : + .slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] : + .slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface: + .slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface: + .slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object: + .slice-planar-object = planar Object + .slice-zaxis = Zaxis + .slice-three-points = 3points .solid-specify-first-point = SOLID Укажите первую точку: .solid-specify-fourth-point = SOLID Укажите четвертую точку: .solid-specify-second-point = SOLID Укажите вторую точку: @@ -3186,7 +3219,18 @@ model = .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .elliptical = Эллиптический - .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]: + .filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]: + .filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]: + .filletedge-enter-option-accept-next = Enter an option [Accept/Next] : + .filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]: + .filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>: + .filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>: + .filletedge-enter-expression = Enter expression: + .filletedge-stale-edge = A selected edge no longer belongs to the solid. + .filletedge-history-create-failed = The solid history could not be created. + .filletedge-history-append-failed = The fillet history could not be recorded. .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 69705fee8..7e042e70d 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -754,6 +754,19 @@ common = .slice-cut-at-ax-value-kept-the-half = SLICE: __ocs_fmt_0__=__ocs_fmt_1__ konumunda kesildi, __ocs_fmt_2__ yarısı korundu. .slice-select-exactly-one-solid-created-this-se = SLICE: Bu oturumda oluşturulan tam bir katı seçin. .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: Düzlem, korunan tarafta katıyı kesmiyor. + .both = Her ikisi + .slice-select-at-least-one-target = SLICE: En az bir katı veya yüzey seçin. + .slice-invalid-plane = SLICE: Kesme düzlemi geçersiz. + .slice-side-point-on-plane = SLICE: Kesme düzleminden uzakta bir nokta seçin. + .slice-plane-misses-target = SLICE: Kesme düzlemi seçilen her nesneyi kesmiyor; hiçbir nesne değiştirilmedi. + .slice-unsupported-intersection = SLICE: Seçilen geometri desteklenmeyen bir yüzey kesişimi içeriyor; hiçbir nesne değiştirilmedi. + .slice-coincident-plane = SLICE: Kesme düzlemi kaynak geometriyle çakışıyor; hiçbir nesne değiştirilmedi. + .slice-topology-refused = SLICE: Seçilen topoloji güvenle bölünemedi; hiçbir nesne değiştirilmedi. + .slice-result-encode-failed = SLICE: Bir sonuç kodlanamadı; hiçbir nesne değiştirilmedi. + .slice-result-display-failed = SLICE: Bir sonuç eksiksiz görüntülenemedi; hiçbir nesne değiştirilmedi. + .slice-result-commit-failed = SLICE: Bir sonuç işlenemedi; tüm kaynak nesneler korundu. + .slice-retained-update-failed = SLICE: Korunan sonuç güncellenemedi. + .slice-created-results = SLICE: __ocs_fmt_0__ sonuç nesnesi oluşturuldu. .splinefit-fit-a-spline-through-n-points = SPLINEFIT: __ocs_fmt_0__ noktadan geçen bir spline uyduruldu. .splinefit-need-at-least-3-points = SPLINEFIT: En az 3 nokta gerekir. .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT: İçinden spline geçirilecek bir çoklu çizgi seçin. @@ -2177,6 +2190,26 @@ draw = .slice-offset-along-x = SLICE X boyunca ofset: .slice-offset-along-y = SLICE Y boyunca ofset: .slice-offset-along-z = SLICE Z boyunca ofset: + .slice-select-targets = SLICE Dilimlenecek katıları veya yüzeyleri seçin, ardından Enter'a basın: + .slice-first-point-or-method = SLICE Kesme düzlemindeki ilk noktayı belirtin veya [düzlemsel Nesne/Yüzey/Zekseni/Görünüm/XY/YZ/ZX/3nokta] <3nokta>: + .slice-first-plane-point = SLICE Kesme düzlemindeki ilk noktayı belirtin: + .slice-second-plane-point = SLICE Kesme düzlemindeki ikinci noktayı belirtin: + .slice-third-plane-point = SLICE Kesme düzlemindeki üçüncü noktayı belirtin: + .slice-point-on-plane = SLICE Kesme düzleminde bir nokta belirtin: + .slice-point-on-normal-axis = SLICE Düzlemin normal ekseninde bir nokta belirtin: + .slice-point-on-view-plane = SLICE Geçerli görünüm düzleminde bir nokta belirtin: + .slice-point-on-ucs-plane = SLICE Geçerli UCS düzleminde bir nokta belirtin: + .slice-select-planar-object = SLICE Düzlemsel bir eğri nesnesi seçin: + .slice-select-planar-surface = SLICE Düzlemsel bir yüzey seçin: + .slice-select-cutting-surface = SLICE Bir kesme yüzeyi seçin: + .slice-select-side-or-both = SLICE İstenen tarafta bir nokta belirtin veya [Her ikisi] : + .slice-select-object-or-both = SLICE Korunacak dilimlenmiş nesneyi seçin veya [Her ikisi] : + .slice-surface-not-planar = SLICE Seçilen yüzey düzlemsel değil. Düzlemsel bir yüzey seçin: + .slice-surface-not-supported = SLICE Seçilen yüzey desteklenen tek bir kesme yüzeyi tanımlamıyor. Başka bir yüzey seçin: + .slice-object-not-planar = SLICE Seçilen nesne bir düzlem tanımlamıyor. Düzlemsel bir eğri nesnesi seçin: + .slice-planar-object = düzlemsel Nesne + .slice-zaxis = Zekseni + .slice-three-points = 3nokta .solid-specify-first-point = SOLID İlk noktayı belirtin: .solid-specify-fourth-point = SOLID Dördüncü noktayı belirtin: .solid-specify-second-point = SOLID İkinci noktayı belirtin: @@ -3146,6 +3179,17 @@ model = .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .elliptical = Eliptik .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .filletedge-select-edge-or-chain-loop-radius = Bir kenar seçin veya [Zincir/Döngü/Yarıçap]: + .filletedge-select-edge-chain-or-edge-radius = Bir kenar zinciri seçin veya [Kenar/Yarıçap]: + .filletedge-select-loop-edge-or-edge-chain-radius = Döngünün kenarını seçin veya [Kenar/Zincir/Yarıçap]: + .filletedge-enter-option-accept-next = Bir seçenek girin [Kabul/Sonraki] : + .filletedge-accept-or-radius = Fillet'i kabul etmek için Enter'a basın veya [Yarıçap]: + .filletedge-enter-radius-or-expression = Fillet yarıçapını girin veya [İfade] <__ocs_fmt_0__>: + .filletedge-specify-radius-or-expression = Yarıçapı belirtin veya [İfade] <__ocs_fmt_0__>: + .filletedge-enter-expression = İfadeyi girin: + .filletedge-stale-edge = Seçilen kenar artık katıya ait değil. + .filletedge-history-create-failed = Katı geçmişi oluşturulamadı. + .filletedge-history-append-failed = Fillet geçmişi kaydedilemedi. .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index caf2c55c2..324259609 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -752,7 +752,20 @@ common = .slice-could-not-determine-the-solid-s-extent = SLICE:无法确定固体的大小。 .slice-cut-at-ax-value-kept-the-half = SLICE: 切到__ocs_fmt_0__=__ocs_fmt_1__, 保留__ocs_fmt_2__的一半。 .slice-select-exactly-one-solid-created-this-se = SLICE:选择一个创建此会话的正体 。 - .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: 飞机不会越过所保存的一侧的固体。 + .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: 飞机不会越过所保存的一侧的固体。 + .both = Both + .slice-select-at-least-one-target = SLICE: select at least one solid or surface. + .slice-invalid-plane = SLICE: the cutting plane is invalid. + .slice-side-point-on-plane = SLICE: choose a point away from the cutting plane. + .slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed. + .slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed. + .slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed. + .slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed. + .slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed. + .slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed. + .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. + .slice-retained-update-failed = SLICE: a retained result could not be updated. + .slice-created-results = SLICE: created __ocs_fmt_0__ result object(s). .splinefit-fit-a-spline-through-n-points = SPLINEFIT: 匹配一条通过__ocs_fmt_0__点的相片。 .splinefit-need-at-least-3-points = SPLINEFIT:至少需要3个点。 .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT:选择一条多线以适应通过的分线。 @@ -2182,7 +2195,27 @@ draw = .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 相冲: - .slice-offset-along-z = SLICE 与 Z 相冲: + .slice-offset-along-z = SLICE 与 Z 相冲: + .slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter: + .slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>: + .slice-first-plane-point = SLICE Specify first point on slicing plane: + .slice-second-plane-point = SLICE Specify second point on slicing plane: + .slice-third-plane-point = SLICE Specify third point on slicing plane: + .slice-point-on-plane = SLICE Specify a point on the slicing plane: + .slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis: + .slice-point-on-view-plane = SLICE Specify a point on the current view plane: + .slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane: + .slice-select-planar-object = SLICE Select a planar curve object: + .slice-select-planar-surface = SLICE Select a planar surface: + .slice-select-cutting-surface = SLICE Select a cutting surface: + .slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] : + .slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] : + .slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface: + .slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface: + .slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object: + .slice-planar-object = planar Object + .slice-zaxis = Zaxis + .slice-three-points = 3points .solid-specify-first-point = SOLID 指定第一点: .solid-specify-fourth-point = SOLID 指定第四点: .solid-specify-second-point = SOLID 指定第二点: @@ -3168,7 +3201,18 @@ model = .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .elliptical = 椭圆 - .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]: + .filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]: + .filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]: + .filletedge-enter-option-accept-next = Enter an option [Accept/Next] : + .filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]: + .filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>: + .filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>: + .filletedge-enter-expression = Enter expression: + .filletedge-stale-edge = A selected edge no longer belongs to the solid. + .filletedge-history-create-failed = The solid history could not be created. + .filletedge-history-append-failed = The fillet history could not be recorded. .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index ecefead67..340ca3c9f 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -764,7 +764,20 @@ common = .slice-could-not-determine-the-solid-s-extent = SLICE:無法確定固體的大小。 .slice-cut-at-ax-value-kept-the-half = SLICE: 切到__ocs_fmt_0__=__ocs_fmt_1__, 保留__ocs_fmt_2__的一半。 .slice-select-exactly-one-solid-created-this-se = SLICE:選擇一個建立此會話的正體 。 - .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: 平面不會越過所儲存的一側的固體。 + .slice-the-plane-does-not-cross-the-solid-on-th = SLICE: 平面不會越過所儲存的一側的固體。 + .both = Both + .slice-select-at-least-one-target = SLICE: select at least one solid or surface. + .slice-invalid-plane = SLICE: the cutting plane is invalid. + .slice-side-point-on-plane = SLICE: choose a point away from the cutting plane. + .slice-plane-misses-target = SLICE: the cutting plane does not cross every selected object; no object was changed. + .slice-unsupported-intersection = SLICE: the selected geometry has an unsupported surface intersection; no object was changed. + .slice-coincident-plane = SLICE: the cutting plane is coincident with source geometry; no object was changed. + .slice-topology-refused = SLICE: the selected topology could not be split safely; no object was changed. + .slice-result-encode-failed = SLICE: a result could not be encoded; no object was changed. + .slice-result-display-failed = SLICE: a result could not be displayed completely; no object was changed. + .slice-result-commit-failed = SLICE: a result could not be committed; all source objects were retained. + .slice-retained-update-failed = SLICE: a retained result could not be updated. + .slice-created-results = SLICE: created __ocs_fmt_0__ result object(s). .splinefit-fit-a-spline-through-n-points = SPLINEFIT: 匹配一條通過__ocs_fmt_0__點的相片。 .splinefit-need-at-least-3-points = SPLINEFIT:至少需要3個點。 .splinefit-select-a-polyline-to-fit-a-spline-th = SPLINEFIT:選擇一條多線以適應通過的分線。 @@ -2194,7 +2207,27 @@ draw = .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 相衝: - .slice-offset-along-z = SLICE 與 Z 相衝: + .slice-offset-along-z = SLICE 與 Z 相衝: + .slice-select-targets = SLICE Select solids or surfaces to slice, then press Enter: + .slice-first-point-or-method = SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>: + .slice-first-plane-point = SLICE Specify first point on slicing plane: + .slice-second-plane-point = SLICE Specify second point on slicing plane: + .slice-third-plane-point = SLICE Specify third point on slicing plane: + .slice-point-on-plane = SLICE Specify a point on the slicing plane: + .slice-point-on-normal-axis = SLICE Specify a point on the plane's normal axis: + .slice-point-on-view-plane = SLICE Specify a point on the current view plane: + .slice-point-on-ucs-plane = SLICE Specify a point on the current UCS plane: + .slice-select-planar-object = SLICE Select a planar curve object: + .slice-select-planar-surface = SLICE Select a planar surface: + .slice-select-cutting-surface = SLICE Select a cutting surface: + .slice-select-side-or-both = SLICE Specify a point on the desired side or [Both] : + .slice-select-object-or-both = SLICE Select a sliced object to keep or [Both] : + .slice-surface-not-planar = SLICE The selected surface is not planar. Select a planar surface: + .slice-surface-not-supported = SLICE The selected surface does not define one supported cutting sheet. Select another surface: + .slice-object-not-planar = SLICE The selected object does not define a plane. Select a planar curve object: + .slice-planar-object = planar Object + .slice-zaxis = Zaxis + .slice-three-points = 3points .solid-specify-first-point = SOLID 指定第一點: .solid-specify-fourth-point = SOLID 指定第四點: .solid-specify-second-point = SOLID 指定第二點: @@ -3180,7 +3213,18 @@ model = .cylinder-specify-second-point-for-height = CYLINDER { common.specify }: { common.height } · { model.second-point }: .cylinder-specify-axis-endpoint = CYLINDER { common.specify }: { draw.axis-endpoint }: .elliptical = 橢圓 - .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .select-a-solid-edge = { draw.select }: { properties.solid } · { common.edge }: + .filletedge-select-edge-or-chain-loop-radius = Select an edge or [Chain/Loop/Radius]: + .filletedge-select-edge-chain-or-edge-radius = Select an edge chain or [Edge/Radius]: + .filletedge-select-loop-edge-or-edge-chain-radius = Select edge of loop or [Edge/Chain/Radius]: + .filletedge-enter-option-accept-next = Enter an option [Accept/Next] : + .filletedge-accept-or-radius = Press Enter to accept the fillet or [Radius]: + .filletedge-enter-radius-or-expression = Enter fillet radius or [Expression] <__ocs_fmt_0__>: + .filletedge-specify-radius-or-expression = Specify Radius or [Expression] <__ocs_fmt_0__>: + .filletedge-enter-expression = Enter expression: + .filletedge-stale-edge = A selected edge no longer belongs to the solid. + .filletedge-history-create-failed = The solid history could not be created. + .filletedge-history-append-failed = The fillet history could not be recorded. .specify-fillet-radius = { common.specify }: { common.fillet } · { common.radius } <__ocs_fmt_0__>: .specify-chamfer-distance = { common.specify }: { common.chamfer } · { common.distance } <__ocs_fmt_0__>: .polysolid-height-width-justification = POLYSOLID { common.height } = __ocs_fmt_0__, { common.width } = __ocs_fmt_1__, { ui.justification } = __ocs_fmt_2__ diff --git a/plugins/registry.json b/plugins/registry.json index 724602992..22fe274f9 100644 --- a/plugins/registry.json +++ b/plugins/registry.json @@ -7,7 +7,7 @@ { "repo": "mf4633/opencad-storm-sewer-plugin", "name": "Storm Sewer", - "description": "Gravity storm-drain network design and analysis (Rational + Manning + HGL, LandXML import)." + "description": "Gravity storm-drain network design and analysis (Rational + Manning + HGL, LandXML import). Free to draw and analyze; Pro ($29/yr) adds HTML reports, pipe sizing, multi-RP." }, { "repo": "mf4633/opencad-hydrocomplete-plugin", diff --git a/src/app/automation.rs b/src/app/automation.rs index e0b11eb66..6d7b4b05e 100644 --- a/src/app/automation.rs +++ b/src/app/automation.rs @@ -1265,4 +1265,63 @@ mod tests { let _ = std::fs::remove_file(sidecar); let _ = std::fs::remove_file(&path); } + + #[test] + fn test_pline_line_then_arc() { + use crate::app::Message; + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + { + app.tabs[0].scene.selection.borrow_mut().vp_size = (1920.0, 1080.0); + app.tabs[0].scene.sync_tiles_from_panes(1920.0, 1080.0); + } + + // Start PLINE + let _ = app.update(Message::CommandInput("PLINE".to_string())); + let _ = app.update(Message::CommandSubmit); + + // Click first point (100, 100) + let _ = app.update(Message::ViewportMove(iced::Point::new(100.0, 100.0))); + let _ = app.update(Message::ViewportLeftPress); + let _ = app.update(Message::ViewportLeftRelease); + + // Move to (200, 100) and click second point (draw line) + let _ = app.update(Message::ViewportMove(iced::Point::new(200.0, 100.0))); + let _ = app.update(Message::ViewportLeftPress); + let _ = app.update(Message::ViewportLeftRelease); + + let wid = app.main_window.unwrap_or_else(iced::window::Id::unique); + + // Switch to arc: Option 1 - CommandOptionPick("A") + let _ = app.update(Message::CommandOptionPick("A".to_string())); + let _ = app.view(wid); + println!("ENTITIES: {}", app.tabs[0].scene.document.entities().count()); + println!("CMD: {:?}", app.tabs[0].active_cmd.as_ref().map(|c| c.name())); + + // Move mouse! + let _ = app.update(Message::ViewportMove(iced::Point::new(200.0, 100.0))); + let _ = app.view(wid); + let _ = app.update(Message::ViewportMove(iced::Point::new(201.0, 100.0))); + let _ = app.view(wid); + let _ = app.update(Message::ViewportMove(iced::Point::new(200.0, 150.0))); + let _ = app.view(wid); + let _ = app.update(Message::ViewportMove(iced::Point::new(150.0, 150.0))); + let _ = app.view(wid); + + // Click arc point + let _ = app.update(Message::ViewportLeftPress); + let _ = app.update(Message::ViewportLeftRelease); + + // Move mouse again + let _ = app.update(Message::ViewportMove(iced::Point::new(150.0, 160.0))); + + // Switch to line + let _ = app.update(Message::CommandOptionPick("L".to_string())); + + // Move mouse again + let _ = app.update(Message::ViewportMove(iced::Point::new(100.0, 150.0))); + + // Finish + let _ = app.update(Message::CommandOptionPick(String::new())); + } } diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index b40bc3693..a5cadff8e 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -733,7 +733,10 @@ impl OpenCADStudio { &result, CmdResult::Relaunch(..) | CmdResult::Dispatch(..) + | CmdResult::SolidEdgeBlend { .. } | CmdResult::SolidSubtract { .. } + | CmdResult::SliceEntities { .. } + | CmdResult::SliceSurfaceEntities { .. } ); let task = self.apply_cmd_result_inner(result); let i = self.active_tab; @@ -4111,11 +4114,20 @@ impl OpenCADStudio { CmdResult::SolidEdgeBlend { handle, - pick, + edges, + base_face, value, + other_value, fillet, } => { - let task = self.solid_edge_blend(handle, pick, value, fillet); + let task = self.solid_edge_blend( + handle, + &edges, + base_face, + value, + other_value, + fillet, + ); self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.tabs[i].scene.clear_preview_wire(); @@ -4149,6 +4161,32 @@ impl OpenCADStudio { return task; } + CmdResult::SliceEntities { + targets, + plane, + keep_point, + } => { + let task = self.solid_slice(&targets, plane, keep_point); + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.tabs[i].scene.clear_preview_wire(); + self.restore_pre_cmd_tangent(); + return task; + } + + CmdResult::SliceSurfaceEntities { + targets, + cutter, + keep_point, + } => { + let task = self.solid_slice_surface(&targets, *cutter, keep_point); + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.tabs[i].scene.clear_preview_wire(); + self.restore_pre_cmd_tangent(); + return task; + } + CmdResult::HatcheditApply { handle, name, diff --git a/src/app/commands/dim.rs b/src/app/commands/dim.rs index b5c2a0eaf..8adf3b441 100644 --- a/src/app/commands/dim.rs +++ b/src/app/commands/dim.rs @@ -1,6 +1,6 @@ use super::*; -fn selected_solid(app: &OpenCADStudio, tab: usize) -> Option { +fn selected_edge_body(app: &OpenCADStudio, tab: usize) -> Option { let scene = &app.tabs.get(tab)?.scene; let selected = scene.selected_handles_in_order(); let [handle] = selected.as_slice() else { @@ -9,7 +9,7 @@ fn selected_solid(app: &OpenCADStudio, tab: usize) -> Option { if matches!( scene.document.get_entity(*handle), - Some(acadrust::EntityType::Solid3D(_)) + Some(acadrust::EntityType::Solid3D(_) | acadrust::EntityType::Surface(_)) ) { Some(*handle) } else { @@ -17,6 +17,36 @@ fn selected_solid(app: &OpenCADStudio, tab: usize) -> Option { } } +fn solid_edge_sources( + app: &mut OpenCADStudio, + tab: usize, +) -> Vec<(acadrust::Handle, cadkernel::brep::Body)> { + let handles = app.tabs[tab] + .scene + .document + .entities() + .filter_map(|entity| { + matches!( + entity, + acadrust::EntityType::Solid3D(_) | acadrust::EntityType::Surface(_) + ) + .then_some(entity.common().handle) + }) + .collect::>(); + app.tabs[tab].scene.restore_solid_models(&handles); + handles + .into_iter() + .filter_map(|handle| { + app.tabs[tab] + .scene + .solid_models + .get(&handle) + .cloned() + .map(|body| (handle, body)) + }) + .collect() +} + impl OpenCADStudio { pub(super) fn dispatch_dim(&mut self, cmd: &str, i: usize) -> Option> { match cmd { @@ -960,16 +990,30 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } - "SOLIDFILLET" => { + "FILLETEDGE" | "SOLIDFILLET" => { use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand}; - let command = SolidEdgeCommand::new(EdgeOperation::Fillet, selected_solid(self, i)); + let target = selected_edge_body(self, i); + let bodies = solid_edge_sources(self, i); + let command = SolidEdgeCommand::new( + EdgeOperation::Fillet, + target, + bodies, + crate::scene::WireModel::SELECTED, + ); self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); } - "FILLET" if selected_solid(self, i).is_some() => { + "FILLET" if selected_edge_body(self, i).is_some() => { use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand}; - let command = SolidEdgeCommand::new(EdgeOperation::Fillet, selected_solid(self, i)); + let target = selected_edge_body(self, i); + let bodies = solid_edge_sources(self, i); + let command = SolidEdgeCommand::new( + EdgeOperation::Fillet, + target, + bodies, + crate::scene::WireModel::SELECTED, + ); self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); } @@ -1107,16 +1151,52 @@ impl OpenCADStudio { } } - "SOLIDCHAMFER" => { - use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand}; - let command = SolidEdgeCommand::new(EdgeOperation::Chamfer, selected_solid(self, i)); + "CHAMFEREDGE" | "SOLIDCHAMFER" => { + use crate::modules::model::edge_cmd::SolidEdgeCommand; + let target = selected_edge_body(self, i); + let header = &self.tabs[i].scene.document.header; + let distances = (header.chamfer_distance_a, header.chamfer_distance_b); + let bodies = solid_edge_sources(self, i); + let command = SolidEdgeCommand::new_chamfer( + target, + bodies, + crate::scene::WireModel::SELECTED, + distances, + ); + let distances = command.chamfer_distances(); + self.command_line.push_output( + crate::tf!( + "Distance1 = {:.4}, Distance2 = {:.4}", + distances.0, + distances.1 + ) + .as_ref(), + ); self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); } - "CHAMFER" if selected_solid(self, i).is_some() => { - use crate::modules::model::edge_cmd::{EdgeOperation, SolidEdgeCommand}; - let command = SolidEdgeCommand::new(EdgeOperation::Chamfer, selected_solid(self, i)); + "CHAMFER" if selected_edge_body(self, i).is_some() => { + use crate::modules::model::edge_cmd::SolidEdgeCommand; + let target = selected_edge_body(self, i); + let header = &self.tabs[i].scene.document.header; + let distances = (header.chamfer_distance_a, header.chamfer_distance_b); + let bodies = solid_edge_sources(self, i); + let command = SolidEdgeCommand::new_chamfer( + target, + bodies, + crate::scene::WireModel::SELECTED, + distances, + ); + let distances = command.chamfer_distances(); + self.command_line.push_output( + crate::tf!( + "Distance1 = {:.4}, Distance2 = {:.4}", + distances.0, + distances.1 + ) + .as_ref(), + ); self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); } diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index 927e593d6..69dc797b2 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -1307,21 +1307,51 @@ impl OpenCADStudio { } } - // SLICE [X|Y|Z] [TOP|BOTTOM] — cut the selected solid with an - // axis-aligned plane, keeping the lower half by default. "SLICE" | "SL" => { - use crate::command::SelectThenKeywordCommand; - let has_sel = !self.tabs[i].scene.selected_entities().is_empty(); - let c = SelectThenKeywordCommand::new( - "SLICE", - "SLICE cutting-plane axis [X / Y / Z] (add TOP/BOTTOM by typing):", - vec![ - ("X", "X", Some("SLICE offset along X:")), - ("Y", "Y", Some("SLICE offset along Y:")), - ("Z", "Z", Some("SLICE offset along Z:")), - ], - has_sel, - ); + use crate::modules::model::slice_cmd::SliceCommand; + let (targets, centre, radius, view_normal) = { + let scene = &mut self.tabs[i].scene; + let mut targets = scene + .selected_handles_in_order() + .into_iter() + .filter(|handle| !scene.is_layer_locked(*handle)) + .filter(|handle| { + matches!( + scene.document.get_entity(*handle), + Some( + acadrust::EntityType::Solid3D(_) + | acadrust::EntityType::Surface(_) + ) + ) + }) + .collect::>(); + scene.restore_solid_models(&targets); + targets.retain(|handle| scene.solid_models.contains_key(handle)); + let bounds = targets + .iter() + .filter_map(|handle| { + crate::scene::model::solid_model::extent(&scene.solid_models[handle]) + }) + .fold(None::<([f64; 3], [f64; 3])>, |bounds, (low, high)| { + Some(match bounds { + None => (low, high), + Some((mut min, mut max)) => { + for axis in 0..3 { + min[axis] = min[axis].min(low[axis]); + max[axis] = max[axis].max(high[axis]); + } + (min, max) + } + }) + }); + let (centre, radius) = bounds.map_or((glam::DVec3::ZERO, 10.0), |(min, max)| { + let min = glam::DVec3::from_array(min); + let max = glam::DVec3::from_array(max); + ((min + max) * 0.5, (max - min).length().max(2.0) * 0.65) + }); + (targets, centre, radius, scene.active_gaze_dir().as_dvec3()) + }; + let c = SliceCommand::new(targets, view_normal, centre, radius); self.command_line.push_info(&c.prompt()); self.tabs[i].active_cmd = Some(Box::new(c)); } @@ -1340,7 +1370,18 @@ impl OpenCADStudio { let value: Option = parts.get(val_idx).and_then(|s| s.parse().ok()); let keep_low = !parts.iter().any(|s| s == "TOP"); match value { - Some(v) => return Some(self.solid_slice(axis, v, keep_low)), + Some(v) => { + let (origin, x, normal) = match axis { + 0 => ([v, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]), + 1 => ([0.0, v, 0.0], [0.0, 0.0, 1.0], [0.0, 1.0, 0.0]), + _ => ([0.0, 0.0, v], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]), + }; + let plane = cadkernel::space::Plane::orthonormal(origin, x, normal) + .expect("fixed world axes define a plane"); + let side = glam::DVec3::from_array(origin) + + glam::DVec3::from_array(normal) * if keep_low { -1.0 } else { 1.0 }; + return Some(self.slice_selected(plane, Some(side))); + } None => self.command_line.push_info( crate::t!("Usage: SLICE [X|Y|Z] [TOP|BOTTOM] (cuts the selected solid)").as_ref(), ), diff --git a/src/app/commands/fileops.rs b/src/app/commands/fileops.rs index 5b68856ea..e1237595b 100644 --- a/src/app/commands/fileops.rs +++ b/src/app/commands/fileops.rs @@ -335,9 +335,9 @@ impl OpenCADStudio { "PERSP" => return Some(Task::done(Message::SetProjection(false))), "LAYERS" => return Some(Task::done(Message::ToggleLayers)), - // SCRIPT — run a command script: each non-blank, non-comment + // SCRIPT — run a command script: each non-comment // line is fed through the same command path the `--script` startup - // flag uses, so the behaviour matches headless automation exactly. + // flag uses. Blank rows submit Enter to the active command. "SCRIPT" | "SCR" => { use crate::command::ValuePromptCommand; let c = ValuePromptCommand::new("SCRIPT", "SCRIPT path to the .scr file:"); @@ -352,10 +352,8 @@ impl OpenCADStudio { let cmds: Vec> = text .lines() .map(str::trim) - .filter(|l| { - !l.is_empty() && !l.starts_with('#') && !l.starts_with(';') - }) - .map(|l| Task::done(Message::Command(l.to_string()))) + .filter(|l| !l.starts_with('#') && !l.starts_with(';')) + .map(|l| Task::done(Message::ScriptLine(l.to_string()))) .collect(); self.command_line.push_output(crate::tf!( "SCRIPT: running {} command(s) from {p}.", diff --git a/src/app/mod.rs b/src/app/mod.rs index d201787c3..c9dbd3cbf 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2117,6 +2117,10 @@ pub enum Message { CommandInput(String), CommandSubmit, Command(String), + /// Execute one complete line read from a command script. Unlike UI/ribbon + /// dispatch, this accepts an interactive verb and all of its arguments on + /// the same line (`BOX 0,0,0 10,10,0 10`). + ScriptLine(String), /// Append one typed character to the command-line input from the /// global key-press subscription. Used when the text-input widget /// itself isn't focused (focus parked on viewport / button / etc.) @@ -3832,7 +3836,7 @@ impl OpenCADStudio { Task::batch( cfg.script_lines .into_iter() - .map(|line| Task::done(Message::Command(line))), + .map(|line| Task::done(Message::ScriptLine(line))), ) }; s.queue_startup_prompts(); diff --git a/src/app/model_ops.rs b/src/app/model_ops.rs index 183e8fd63..a19f66125 100644 --- a/src/app/model_ops.rs +++ b/src/app/model_ops.rs @@ -7,7 +7,7 @@ use acadrust::{ objects::SolidHistoryOperation, EntityType, Handle, }; -use cadkernel::brep::Body; +use cadkernel::brep::{Body, EdgeKey, FaceKey}; use iced::Task; use std::collections::{HashMap, HashSet}; @@ -76,6 +76,35 @@ struct PreparedSubtract { result: Option<(Handle, EntityType, Body, PreparedSolidDisplay)>, } +struct PreparedSlice { + retained: Handle, + entity: EntityType, + body: Body, + display: PreparedSolidDisplay, + extra: Option<(EntityType, Body)>, +} + +enum ModelSliceTool { + Plane(cadkernel::space::Plane), + Surface(Body), +} + +impl ModelSliceTool { + fn side(&self, point: [f64; 3]) -> Option { + match self { + Self::Plane(plane) => plane.distance_to(point), + Self::Surface(surface) => cadkernel::brep::surface_side(surface, point), + } + } + + fn split(&self, body: &Body) -> Result, cadkernel::brep::Snag> { + match self { + Self::Plane(plane) => cadkernel::brep::slice_by_plane(body, *plane), + Self::Surface(surface) => cadkernel::brep::slice_by_surface(body, surface), + } + } +} + struct IntersectGroup { kind: UnionEntityKind, plane: Option, @@ -244,6 +273,29 @@ fn entity_with_boolean_body(mut source: EntityType, body: &Body) -> Option Option { + let document = crate::scene::convert::acis_export::solid_to_sat(body)?; + let wires = solid_model::edge_wires(body); + match &mut source { + EntityType::Solid3D(entity) => { + entity.wires = wires; + entity.silhouettes.clear(); + entity.set_sat_document(&document); + } + EntityType::Surface(entity) => { + entity.wires = wires; + entity.silhouettes.clear(); + if entity.kind != SurfaceKind::Plane { + entity.kind = SurfaceKind::Generic; + entity.surface_data = SurfaceData::Generic; + } + entity.acis_data = AcisData::from_sat(&document.to_sat_string()); + } + _ => return None, + } + Some(source) +} + enum IntersectBodyOutcome { Area(Body), Touching, @@ -551,44 +603,169 @@ impl super::OpenCADStudio { groups } - fn replace_solid_body(&mut self, handle: Handle, result: Body, label: &str) -> bool { + fn replace_solid_body_with_fillet( + &mut self, + handle: Handle, + source: &Body, + result: Body, + edges: &[EdgeKey], + radius: f64, + ) -> bool { let i = self.active_tab; - let Some(document) = - crate::scene::convert::acis_export::solid_to_sat(&result) - else { - self.command_line - .push_error(crate::t!("The result could not be encoded as ACIS.").as_ref()); - return false; - }; - let Some(EntityType::Solid3D(mut entity)) = - self.tabs[i].scene.document.get_entity(handle).cloned() - else { - return false; - }; - entity.wires = solid_model::edge_wires(&result); - entity.silhouettes.clear(); - entity.history_handle = None; - entity.set_sat_document(&document); - - let Some(display) = self.tabs[i].scene.prepare_solid_model_display(handle, &result) + let Some(display) = self.tabs[i] + .scene + .prepare_solid_model_display(handle, &result) .filter(|display| display.0.complete) else { - self.command_line.push_error(crate::t!("The result could not be displayed completely. The original solid was retained.").as_ref()); + self.command_line.push_error(crate::t!("The result could not be displayed completely. The original edge body was retained.").as_ref()); return false; }; - self.push_undo_snapshot(i, label); - self.tabs[i].scene.delete_solid_history(handle); - if !self.tabs[i] - .scene - .update_entity(EntityType::Solid3D(entity)) + let ordered = source.edge_keys().collect::>(); + let Some(history_edges) = edges + .iter() + .map(|edge| { + ordered + .iter() + .position(|candidate| candidate == edge) + .and_then(|ordinal| i32::try_from(ordinal).ok()) + }) + .collect::>>() + else { + self.command_line + .push_error(crate::t!("A selected edge no longer belongs to the body.").as_ref()); + return false; + }; + + self.push_undo_snapshot(i, "FILLETEDGE"); + if self.tabs[i].scene.document.solid_history_graph(handle).is_none() + && !self.tabs[i] + .scene + .create_solid_history(handle, solid_history::brep_op(source)) { self.command_line - .push_error(crate::t!("The solid could not be updated.").as_ref()); + .push_error(crate::t!("The edge body history could not be created.").as_ref()); return false; } - let history = solid_history::brep_op(&result); - self.tabs[i].scene.create_solid_history(handle, history); - self.tabs[i].scene.register_prepared_solid_model(handle, result, display); + if !self.tabs[i].scene.append_solid_history( + handle, + solid_history::fillet_op(history_edges, radius), + ) { + self.command_line + .push_error(crate::t!("The fillet history could not be recorded.").as_ref()); + return false; + } + let _ = self.tabs[i].scene.apply_solid_history_choice( + handle, + solid_history::PROP_HISTORY, + "Record", + ); + let Some(entity) = self.tabs[i].scene.document.get_entity(handle).cloned() else { + return false; + }; + let Some(entity) = entity_with_history_body(entity, &result) else { + return false; + }; + if !self.tabs[i].scene.update_entity(entity) { + self.command_line + .push_error(crate::t!("The edge body could not be updated.").as_ref()); + return false; + } + self.tabs[i] + .scene + .register_prepared_solid_model(handle, result, display); + self.tabs[i].scene.deselect_all(); + self.tabs[i].scene.select_entity(handle, false); + self.tabs[i].dirty = true; + self.refresh_properties(); + true + } + + fn replace_edge_body_with_chamfer( + &mut self, + handle: Handle, + source: &Body, + result: Body, + edges: &[EdgeKey], + base_face: FaceKey, + base_distance: f64, + other_distance: f64, + ) -> bool { + let i = self.active_tab; + let Some(display) = self.tabs[i] + .scene + .prepare_solid_model_display(handle, &result) + .filter(|display| display.0.complete) + else { + self.command_line.push_error(crate::t!("The result could not be displayed completely. The original edge body was retained.").as_ref()); + return false; + }; + let ordered_edges = source.edge_keys().collect::>(); + let Some(history_edges) = edges + .iter() + .map(|edge| { + ordered_edges + .iter() + .position(|candidate| candidate == edge) + .and_then(|ordinal| i32::try_from(ordinal).ok()) + }) + .collect::>>() + else { + self.command_line + .push_error(crate::t!("A selected edge no longer belongs to the body.").as_ref()); + return false; + }; + let Some(history_face) = source + .face_keys() + .position(|candidate| candidate == base_face) + .and_then(|ordinal| i32::try_from(ordinal).ok()) + else { + self.command_line + .push_error(crate::t!("The selected base face no longer belongs to the body.").as_ref()); + return false; + }; + + self.push_undo_snapshot(i, "CHAMFEREDGE"); + if self.tabs[i].scene.document.solid_history_graph(handle).is_none() + && !self.tabs[i] + .scene + .create_solid_history(handle, solid_history::brep_op(source)) + { + self.command_line + .push_error(crate::t!("The edge body history could not be created.").as_ref()); + return false; + } + if !self.tabs[i].scene.append_solid_history( + handle, + solid_history::chamfer_op( + history_edges, + history_face, + base_distance, + other_distance, + ), + ) { + self.command_line + .push_error(crate::t!("The chamfer history could not be recorded.").as_ref()); + return false; + } + let _ = self.tabs[i].scene.apply_solid_history_choice( + handle, + solid_history::PROP_HISTORY, + "Record", + ); + let Some(entity) = self.tabs[i].scene.document.get_entity(handle).cloned() else { + return false; + }; + let Some(entity) = entity_with_history_body(entity, &result) else { + return false; + }; + if !self.tabs[i].scene.update_entity(entity) { + self.command_line + .push_error(crate::t!("The edge body could not be updated.").as_ref()); + return false; + } + self.tabs[i] + .scene + .register_prepared_solid_model(handle, result, display); self.tabs[i].scene.deselect_all(); self.tabs[i].scene.select_entity(handle, false); self.tabs[i].dirty = true; @@ -599,20 +776,21 @@ impl super::OpenCADStudio { pub(super) fn solid_edge_blend( &mut self, handle: Handle, - pick: glam::DVec3, + edges: &[EdgeKey], + base_face: Option, value: f64, + other_value: f64, fillet: bool, ) -> Task { let i = self.active_tab; if self.reject_locked_edit(i, handle) { return Task::none(); } - if !matches!( - self.tabs[i].scene.document.get_entity(handle), - Some(EntityType::Solid3D(_)) - ) { + if !matches!(self.tabs[i].scene.document.get_entity(handle), Some( + EntityType::Solid3D(_) | EntityType::Surface(_) + )) { self.command_line - .push_error(crate::t!("Select a 3D solid edge.").as_ref()); + .push_error(crate::t!("Select a 3D solid or surface edge.").as_ref()); return Task::none(); } self.tabs[i].scene.restore_solid_models(&[handle]); @@ -621,15 +799,35 @@ impl super::OpenCADStudio { .push_error(crate::t!("The solid geometry could not be restored.").as_ref()); return Task::none(); }; - let Some(edge) = solid_model::nearest_edge(&body, pick.to_array()) else { - self.command_line - .push_error(crate::t!("Select a solid edge.").as_ref()); - return Task::none(); - }; let result = if fillet { - cadkernel::brep::fillet(&body, edge, value) + match cadkernel::brep::fillet_edges(&body, edges, value) { + Ok(result) => Some(result), + Err(error) => { + self.command_line + .push_error(&format!("FILLETEDGE: {error}")); + return Task::none(); + } + } } else { - cadkernel::brep::chamfer(&body, edge, value) + let Some(base_face) = base_face else { + self.command_line + .push_error(crate::t!("CHAMFEREDGE: select a base face edge.").as_ref()); + return Task::none(); + }; + match cadkernel::brep::chamfer_edges( + &body, + edges, + base_face, + value, + other_value, + ) { + Ok(result) => Some(result), + Err(error) => { + self.command_line + .push_error(&format!("CHAMFEREDGE: {error}")); + return Task::none(); + } + } }; let Some(result) = result else { self.command_line.push_error( @@ -638,8 +836,25 @@ impl super::OpenCADStudio { ); return Task::none(); }; - let label = if fillet { "SOLIDFILLET" } else { "SOLIDCHAMFER" }; - if self.replace_solid_body(handle, result, label) { + let label = if fillet { "FILLETEDGE" } else { "CHAMFEREDGE" }; + let updated = if fillet { + self.replace_solid_body_with_fillet(handle, &body, result, edges, value) + } else { + self.replace_edge_body_with_chamfer( + handle, + &body, + result, + edges, + base_face.expect("checked chamfer face"), + value, + other_value, + ) + }; + if updated { + if !fillet { + self.tabs[i].scene.document.header.chamfer_distance_a = value; + self.tabs[i].scene.document.header.chamfer_distance_b = other_value; + } self.command_line .push_output(crate::tf!("{label}: solid updated.").as_ref()); } @@ -740,10 +955,42 @@ impl super::OpenCADStudio { } }; - if self.replace_solid_body(handle, result, "SHELL") { + let Some(source) = self.tabs[i].scene.document.get_entity(handle).cloned() else { + return Task::none(); + }; + let Some(entity) = entity_with_boolean_body(source, &result) else { self.command_line - .push_output(crate::t!("SHELL: solid updated.").as_ref()); + .push_error(crate::t!("The result could not be encoded as ACIS.").as_ref()); + return Task::none(); + }; + let Some(display) = self.tabs[i] + .scene + .prepare_solid_model_display(handle, &result) + .filter(|display| display.0.complete) + else { + self.command_line.push_error(crate::t!("The result could not be displayed completely. The original solid was retained.").as_ref()); + return Task::none(); + }; + + self.push_undo_snapshot(i, "SHELL"); + self.tabs[i].scene.delete_solid_history(handle); + if !self.tabs[i].scene.update_entity(entity) { + self.command_line + .push_error(crate::t!("The solid could not be updated.").as_ref()); + return Task::none(); } + self.tabs[i] + .scene + .create_solid_history(handle, solid_history::brep_op(&result)); + self.tabs[i] + .scene + .register_prepared_solid_model(handle, result, display); + self.tabs[i].scene.deselect_all(); + self.tabs[i].scene.select_entity(handle, false); + self.tabs[i].dirty = true; + self.refresh_properties(); + self.command_line + .push_output(crate::t!("SHELL: solid updated.").as_ref()); Task::none() } @@ -1374,77 +1621,248 @@ impl super::OpenCADStudio { Task::none() } - /// Slice the one selected solid with an axis-aligned plane (axis 0/1/2 = - /// X/Y/Z at `value`), keeping the lower side when `keep_low` is true. The - /// kept half is the intersection of the solid with a half-space box, reusing - /// the same boolean path as the modelling tools. - pub(super) fn solid_slice(&mut self, axis: usize, value: f64, keep_low: bool) -> Task { + /// Split every selected solid or surface by an arbitrary plane. All + /// geometry, persistence records, and displays are prepared before the + /// first source entity is changed. + pub(super) fn solid_slice( + &mut self, + requested: &[Handle], + plane: cadkernel::space::Plane, + keep_point: Option, + ) -> Task { + self.solid_slice_with_tool(requested, ModelSliceTool::Plane(plane), keep_point) + } + + pub(super) fn solid_slice_surface( + &mut self, + requested: &[Handle], + cutter: Body, + keep_point: Option, + ) -> Task { + self.solid_slice_with_tool(requested, ModelSliceTool::Surface(cutter), keep_point) + } + + fn solid_slice_with_tool( + &mut self, + requested: &[Handle], + tool: ModelSliceTool, + keep_point: Option, + ) -> Task { let i = self.active_tab; - let handles = self.selected_solid_handles(); - if handles.len() != 1 { - self.command_line - .push_error(crate::t!("SLICE: select exactly one solid created this session.").as_ref()); + let mut handles = requested + .iter() + .copied() + .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle)) + .filter(|handle| { + matches!( + self.tabs[i].scene.document.get_entity(*handle), + Some(EntityType::Solid3D(_) | EntityType::Surface(_)) + ) + }) + .collect::>(); + handles.sort_unstable_by_key(|handle| handle.value()); + handles.dedup(); + self.tabs[i].scene.restore_solid_models(&handles); + handles.retain(|handle| self.tabs[i].scene.solid_models.contains_key(handle)); + if handles.is_empty() { + self.command_line.push_error( + crate::t!("SLICE: select at least one solid or surface.").as_ref(), + ); return Task::none(); } - let solid = self.tabs[i].scene.solid_models[&handles[0]].clone(); - // Bounding box from the solid's edge wires. - let Some((min, max)) = solid_model::extent(&solid) else { - self.command_line - .push_error(crate::t!("SLICE: could not determine the solid's extent.").as_ref()); - return Task::none(); - }; - // Generous margin so the box fully spans the solid in the free axes. - let m = [ - (max[0] - min[0]).max(1.0), - (max[1] - min[1]).max(1.0), - (max[2] - min[2]).max(1.0), - ]; - let mut lo = [min[0] - m[0], min[1] - m[1], min[2] - m[2]]; - let mut hi = [max[0] + m[0], max[1] + m[1], max[2] + m[2]]; - if keep_low { - hi[axis] = value; + + let keep_positive = if let Some(point) = keep_point { + let tolerance = cadkernel::brep::operation_tolerance( + &handles + .iter() + .map(|handle| &self.tabs[i].scene.solid_models[handle]) + .collect::>(), + ); + let Some(distance) = tool.side(point.to_array()) else { + self.command_line + .push_error(crate::t!("SLICE: the cutting plane is invalid.").as_ref()); + return Task::none(); + }; + if distance.abs() <= tolerance { + self.command_line.push_error( + crate::t!("SLICE: choose a point away from the cutting plane.").as_ref(), + ); + return Task::none(); + } + Some(distance > 0.0) } else { - lo[axis] = value; - } - if hi[axis] <= lo[axis] { - self.command_line - .push_error(crate::t!("SLICE: the plane does not cross the solid on the kept side.").as_ref()); - return Task::none(); - } - let center = [ - (lo[0] + hi[0]) / 2.0, - (lo[1] + hi[1]) / 2.0, - (lo[2] + hi[2]) / 2.0, - ]; - let halfspace = solid_model::box_solid(center, hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]); - let Some(result) = halfspace - .as_ref() - .and_then(|half| solid_model::boolean(Bool::Intersect, &solid, half)) - else { - self.command_line - .push_error(crate::t!("SLICE failed — the plane may not cross the solid.").as_ref()); - return Task::none(); + None }; + + let mut prepared = Vec::with_capacity(handles.len()); + for handle in &handles { + let body = &self.tabs[i].scene.solid_models[handle]; + let result = match tool.split(body) { + Ok(Some(result)) => result, + Ok(None) => { + self.command_line.push_error( + crate::t!("SLICE: the cutting plane does not cross every selected object; no object was changed.") + .as_ref(), + ); + return Task::none(); + } + Err(cadkernel::brep::Snag::NoClosedForm) => { + self.command_line.push_error( + crate::t!("SLICE: the selected geometry has an unsupported surface intersection; no object was changed.") + .as_ref(), + ); + return Task::none(); + } + Err(cadkernel::brep::Snag::Coincident) => { + self.command_line.push_error( + crate::t!("SLICE: the cutting plane is coincident with source geometry; no object was changed.") + .as_ref(), + ); + return Task::none(); + } + Err(cadkernel::brep::Snag::CutRefused) => { + self.command_line.push_error( + crate::t!("SLICE: the selected topology could not be split safely; no object was changed.") + .as_ref(), + ); + return Task::none(); + } + }; + let (body, extra_body) = match keep_positive { + Some(true) => (result.positive, None), + Some(false) => (result.negative, None), + None => (result.negative, Some(result.positive)), + }; + let Some(source) = self.tabs[i].scene.document.get_entity(*handle).cloned() else { + return Task::none(); + }; + let Some(entity) = entity_with_boolean_body(source.clone(), &body) else { + self.command_line.push_error( + crate::t!("SLICE: a result could not be encoded; no object was changed.") + .as_ref(), + ); + return Task::none(); + }; + let Some(display) = self.tabs[i] + .scene + .prepare_solid_model_display(*handle, &body) + .filter(|display| display.0.complete) + else { + self.command_line.push_error( + crate::t!("SLICE: a result could not be displayed completely; no object was changed.") + .as_ref(), + ); + return Task::none(); + }; + let extra = match extra_body { + Some(extra_body) => { + let mut extra_entity = match entity_with_boolean_body(source, &extra_body) { + Some(entity) => entity, + None => { + self.command_line.push_error( + crate::t!("SLICE: a result could not be encoded; no object was changed.") + .as_ref(), + ); + return Task::none(); + } + }; + extra_entity.common_mut().handle = Handle::NULL; + Some((extra_entity, extra_body)) + } + None => None, + }; + prepared.push(PreparedSlice { + retained: *handle, + entity, + body, + display, + extra, + }); + } + self.push_undo_snapshot(i, "SLICE"); - self.tabs[i].scene.erase_entities(&handles); - let mut s3d = Solid3D::new(); - s3d.wires = solid_model::edge_wires(&result); - let history = solid_history::brep_op(&result); - let handle = self.add_solid_model(EntityType::Solid3D(s3d), result, history); + let mut created = Vec::new(); + for item in &prepared { + let Some((entity, body)) = &item.extra else { + continue; + }; + let handle = match entity { + EntityType::Solid3D(_) => self.add_solid_model_preserving_style( + entity.clone(), + body.clone(), + solid_history::brep_op(body), + ), + EntityType::Surface(_) => { + self.add_surface_model_preserving_style(entity.clone(), body.clone()) + } + _ => Handle::NULL, + }; + if handle.is_null() { + self.tabs[i].scene.rollback_new_entities(&created); + self.discard_last_undo_entry(i); + self.command_line.push_error( + crate::t!("SLICE: a result could not be committed; all source objects were retained.") + .as_ref(), + ); + return Task::none(); + } + created.push(handle); + } + + let record_history = self.tabs[i].scene.document.header.record_solid_history; + let mut retained = Vec::with_capacity(prepared.len()); + for item in prepared { + self.tabs[i].scene.delete_solid_history(item.retained); + if !self.tabs[i].scene.update_entity(item.entity) { + self.command_line.push_error( + crate::t!("SLICE: a retained result could not be updated.").as_ref(), + ); + return Task::none(); + } + if record_history + && matches!( + self.tabs[i].scene.document.get_entity(item.retained), + Some(EntityType::Solid3D(_)) + ) + { + self.tabs[i] + .scene + .create_solid_history(item.retained, solid_history::brep_op(&item.body)); + } + self.tabs[i].scene.register_prepared_solid_model( + item.retained, + item.body, + item.display, + ); + retained.push(item.retained); + } + + retained.extend(created); self.tabs[i].scene.deselect_all(); - if !handle.is_null() { - self.tabs[i].scene.select_entity(handle, false); + for handle in &retained { + self.tabs[i].scene.select_entity(*handle, false); } self.tabs[i].dirty = true; self.refresh_properties(); - let ax = ["X", "Y", "Z"][axis]; - self.command_line.push_output(crate::tf!( - "SLICE: cut at {ax}={value}, kept the {} half.", - if keep_low { "lower" } else { "upper" } - ).as_ref()); + self.command_line.push_output( + crate::tf!( + "SLICE: created %{count} result object(s).", + count = retained.len() + ) + .as_ref(), + ); Task::none() } + pub(super) fn slice_selected( + &mut self, + plane: cadkernel::space::Plane, + keep_point: Option, + ) -> Task { + let handles = self.tabs[self.active_tab].scene.selected_handles_in_order(); + self.solid_slice(&handles, plane, keep_point) + } + /// INTERFERE — create a solid from the overlap of the two selected solids, /// leaving the originals in place (a non-destructive boolean intersect). pub(super) fn solid_interfere(&mut self) -> Task { diff --git a/src/app/update/dynamic.rs b/src/app/update/dynamic.rs index feaf6b016..1e1888055 100644 --- a/src/app/update/dynamic.rs +++ b/src/app/update/dynamic.rs @@ -740,6 +740,14 @@ impl OpenCADStudio { .as_mut() .map(|c| c.on_preview_wires(cur)) .unwrap_or_default(); + let preview_hidden = self.tabs[i] + .active_cmd + .as_ref() + .map(|command| command.preview_hidden_handles().to_vec()) + .unwrap_or_default(); + self.tabs[i] + .scene + .set_command_preview_hidden(&preview_hidden); self.tabs[i].scene.set_preview_wires(previews); } } diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index b80f9ee7c..5aff46f09 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -265,6 +265,11 @@ impl OpenCADStudio { self.control_observe_user_message(&msg); let perf_started = crate::perf::enabled().then(Instant::now); let perf_label = perf_message_label(&msg); + // Keep the stable tab id because dispatch may switch or close tabs. + let perf_edit_before = perf_started.map(|started| { + let tab = &self.tabs[self.active_tab]; + (started, tab.id, tab.scene.geometry_epoch) + }); // A modal dialog must capture the keyboard the same way it already // captures the mouse. Otherwise keystrokes from the global key // subscription leak past the modal into the command line and fire as @@ -324,6 +329,17 @@ impl OpenCADStudio { self.otrack_active = None; self.otrack_kind = None; } + if let Some((started, tab_id, before)) = perf_edit_before { + if let Some(tab) = self.tabs.iter().find(|tab| tab.id == tab_id) { + if tab.scene.geometry_epoch != before { + tab.scene.record_nav_perf_caused( + crate::scene::NavPerfOp::Edit, + perf_label, + started, + ); + } + } + } if let Some(started) = perf_started { let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0; if elapsed_ms >= 5.0 { @@ -1953,6 +1969,14 @@ impl OpenCADStudio { self.dispatch_command(&cmd) } + Message::ScriptLine(line) => { + if line.trim().is_empty() { + self.feed_command(crate::command::StepInput::Enter) + } else { + self.run_command_line(&line) + } + } + Message::ToggleLayers => { if self.active_modal == Some(super::ModalKind::Layers) { self.ribbon.deactivate_tool_if("LAYERS"); diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index c39916fe4..12c339a01 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -2468,6 +2468,14 @@ impl OpenCADStudio { previews.push(guide); } } + let preview_hidden = self.tabs[i] + .active_cmd + .as_ref() + .map(|command| command.preview_hidden_handles().to_vec()) + .unwrap_or_default(); + self.tabs[i] + .scene + .set_command_preview_hidden(&preview_hidden); self.tabs[i].scene.set_preview_wires(previews); } else { self.tabs[i].snap_result = None; diff --git a/src/command.rs b/src/command.rs index 40299938e..99d092712 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1558,11 +1558,13 @@ pub enum CmdResult { options: LoftOptions, color: [f32; 4], }, - /// Round or bevel the straight edge nearest `pick` on a solid. + /// Round or bevel one or more resolved B-rep edges on a solid. SolidEdgeBlend { handle: Handle, - pick: DVec3, + edges: Vec, + base_face: Option, value: f64, + other_value: f64, fillet: bool, }, /// Offset a solid and optionally open selected faces. @@ -1576,6 +1578,20 @@ pub enum CmdResult { cutters: Vec, convert_meshes: bool, }, + /// Split every selected solid or surface with one arbitrary plane. + /// `keep_point == None` retains both sides; otherwise it selects the side + /// containing that WCS point. + SliceEntities { + targets: Vec, + plane: cadkernel::space::Plane, + keep_point: Option, + }, + /// Split selected solids or surfaces with one selected analytic sheet. + SliceSurfaceEntities { + targets: Vec, + cutter: Box, + keep_point: Option, + }, /// INSERT landed on a block that has AttributeDefinitions. /// The host should look up the attdefs for `block_name` from the document /// and call `attreq_set_attdefs()` on the command, then loop on text input. diff --git a/src/entities/common.rs b/src/entities/common.rs index 1f837daca..1d1fbee41 100644 --- a/src/entities/common.rs +++ b/src/entities/common.rs @@ -114,7 +114,11 @@ pub fn format_length(value: f64) -> String { let per_foot = 12.0 * scale; let feet = (total / per_foot).trunc(); let rem = (total - feet * per_foot) / scale; - format!("{}{:.0}'-{:.*}\"", sign, feet, prec, rem) + if feet == 0.0 { + format!("{}{:.*}\"", sign, prec, rem) + } else { + format!("{}{:.0}'-{:.*}\"", sign, feet, prec, rem) + } } 4 | 5 => { // Architectural and fractional formats use 1/64-inch resolution. @@ -142,6 +146,9 @@ pub fn format_length(value: f64) -> String { }; let unit_suffix = if ctx.lunits == 4 { "\"" } else { "" }; match feet { + Some(f) if f == 0.0 => { + format!("{}{:.0}{}{}", sign, whole, frac_str, unit_suffix) + } Some(f) => format!("{}{:.0}'-{:.0}{}{}", sign, f, whole, frac_str, unit_suffix), None => format!("{}{:.0}{}", sign, whole, frac_str), } @@ -1102,7 +1109,9 @@ mod length_format_tests { #[test] fn architectural_carries_into_feet() { - assert_eq!(with_units(4, 4, 11.99), "0'-11 63/64\""); + assert_eq!(with_units(4, 4, 11.99), "11 63/64\""); + assert_eq!(with_units(4, 4, 0.5), "0 1/2\""); + assert_eq!(with_units(4, 4, -9.25), "-9 1/4\""); assert_eq!(with_units(4, 4, 11.999), "1'-0\""); assert_eq!(with_units(4, 4, 23.999), "2'-0\""); assert_eq!(with_units(4, 4, 66.5), "5'-6 1/2\""); @@ -1111,7 +1120,9 @@ mod length_format_tests { #[test] fn engineering_carries_into_feet() { - assert_eq!(with_units(3, 1, 11.94), "0'-11.9\""); + assert_eq!(with_units(3, 1, 11.94), "11.9\""); + assert_eq!(with_units(3, 1, 0.5), "0.5\""); + assert_eq!(with_units(3, 1, -9.25), "-9.3\""); assert_eq!(with_units(3, 1, 11.99), "1'-0.0\""); assert_eq!(with_units(3, 1, 23.99), "2'-0.0\""); assert_eq!(with_units(3, 2, 11.999), "1'-0.00\""); diff --git a/src/io/pdf_export.rs b/src/io/pdf_export.rs index 0ff656d13..10b72da48 100644 --- a/src/io/pdf_export.rs +++ b/src/io/pdf_export.rs @@ -20,8 +20,6 @@ use printpdf::{ PdfFontHandle, PdfPage, PdfSaveOptions, Point, Polygon, PolygonRing, Pt, Rgb, TextItem, WindingOrder, }; -#[cfg(not(target_arch = "wasm32"))] -use std::io::Write; use std::path::Path; #[derive(Clone, Debug)] @@ -175,8 +173,7 @@ pub fn export_pdf( plot_style, options, ); - let mut file = std::fs::File::create(path).map_err(|e| e.to_string())?; - file.write_all(&bytes).map_err(|e| e.to_string()) + write_pdf_atomically(path, &bytes) } /// Export several independently sized pages into one PDF file. @@ -190,8 +187,22 @@ pub fn export_pdf_pages( return Err("No pages were selected.".into()); } let bytes = build_pdf_pages(pages, plot_style); - let mut file = std::fs::File::create(path).map_err(|e| e.to_string())?; - file.write_all(&bytes).map_err(|e| e.to_string()) + write_pdf_atomically(path, &bytes) +} + +/// Write a complete PDF beside the destination, then replace it atomically. +#[cfg(not(target_arch = "wasm32"))] +fn write_pdf_atomically(path: &Path, bytes: &[u8]) -> Result<(), String> { + let temp_path = super::save_temp_path(path); + if let Err(error) = std::fs::write(&temp_path, bytes) { + let _ = std::fs::remove_file(&temp_path); + return Err(format!("Failed to write PDF data: {error}")); + } + if let Err(error) = super::replace_save_file(&temp_path, path) { + let _ = std::fs::remove_file(&temp_path); + return Err(format!("Failed to replace PDF file: {error}")); + } + Ok(()) } /// Show a parented PDF save-file dialog and return the chosen path. @@ -1505,6 +1516,24 @@ fn emit_text( mod tests { use super::*; + #[test] + fn atomic_write_replaces_an_existing_pdf() { + let path = std::env::temp_dir().join(format!( + "ocs-pdf-replace-{}-{}.pdf", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::write(&path, b"old").unwrap(); + + write_pdf_atomically(&path, b"new").unwrap(); + + assert_eq!(std::fs::read(&path).unwrap(), b"new"); + std::fs::remove_file(path).unwrap(); + } + #[test] fn clip_and_scale_emit_pdf_bytes() { let w = PlotWire { diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index eda54347e..de350e331 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -2346,11 +2346,44 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "SLICE offset along X:" => Some(("draw", "slice-offset-along-x")), "SLICE offset along Y:" => Some(("draw", "slice-offset-along-y")), "SLICE offset along Z:" => Some(("draw", "slice-offset-along-z")), + "SLICE Select solids or surfaces to slice, then press Enter:" => Some(("draw", "slice-select-targets")), + "SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:" => Some(("draw", "slice-first-point-or-method")), + "SLICE Specify first point on slicing plane:" => Some(("draw", "slice-first-plane-point")), + "SLICE Specify second point on slicing plane:" => Some(("draw", "slice-second-plane-point")), + "SLICE Specify third point on slicing plane:" => Some(("draw", "slice-third-plane-point")), + "SLICE Specify a point on the slicing plane:" => Some(("draw", "slice-point-on-plane")), + "SLICE Specify a point on the plane's normal axis:" => Some(("draw", "slice-point-on-normal-axis")), + "SLICE Specify a point on the current view plane:" => Some(("draw", "slice-point-on-view-plane")), + "SLICE Specify a point on the current UCS plane:" => Some(("draw", "slice-point-on-ucs-plane")), + "SLICE Select a planar curve object:" => Some(("draw", "slice-select-planar-object")), + "SLICE Select a planar surface:" => Some(("draw", "slice-select-planar-surface")), + "SLICE Select a cutting surface:" => Some(("draw", "slice-select-cutting-surface")), + "SLICE Specify a point on the desired side or [Both] :" => Some(("draw", "slice-select-side-or-both")), + "SLICE Select a sliced object to keep or [Both] :" => Some(("draw", "slice-select-object-or-both")), + "SLICE The selected surface is not planar. Select a planar surface:" => Some(("draw", "slice-surface-not-planar")), + "SLICE The selected surface does not define one supported cutting sheet. Select another surface:" => Some(("draw", "slice-surface-not-supported")), + "SLICE The selected object does not define a plane. Select a planar curve object:" => Some(("draw", "slice-object-not-planar")), + "planar Object" => Some(("draw", "slice-planar-object")), + "Zaxis" => Some(("draw", "slice-zaxis")), + "3points" => Some(("draw", "slice-three-points")), + "Both" => Some(("common", "both")), "SLICE failed — the plane may not cross the solid." => Some(("common", "slice-failed-the-plane-may-not-cross-the-solid")), "SLICE: could not determine the solid's extent." => Some(("common", "slice-could-not-determine-the-solid-s-extent")), "SLICE: cut at {ax}={value}, kept the {} half." => Some(("common", "slice-cut-at-ax-value-kept-the-half")), "SLICE: select exactly one solid created this session." => Some(("common", "slice-select-exactly-one-solid-created-this-se")), "SLICE: the plane does not cross the solid on the kept side." => Some(("common", "slice-the-plane-does-not-cross-the-solid-on-th")), + "SLICE: select at least one solid or surface." => Some(("common", "slice-select-at-least-one-target")), + "SLICE: the cutting plane is invalid." => Some(("common", "slice-invalid-plane")), + "SLICE: choose a point away from the cutting plane." => Some(("common", "slice-side-point-on-plane")), + "SLICE: the cutting plane does not cross every selected object; no object was changed." => Some(("common", "slice-plane-misses-target")), + "SLICE: the selected geometry has an unsupported surface intersection; no object was changed." => Some(("common", "slice-unsupported-intersection")), + "SLICE: the cutting plane is coincident with source geometry; no object was changed." => Some(("common", "slice-coincident-plane")), + "SLICE: the selected topology could not be split safely; no object was changed." => Some(("common", "slice-topology-refused")), + "SLICE: a result could not be encoded; no object was changed." => Some(("common", "slice-result-encode-failed")), + "SLICE: a result could not be displayed completely; no object was changed." => Some(("common", "slice-result-display-failed")), + "SLICE: a result could not be committed; all source objects were retained." => Some(("common", "slice-result-commit-failed")), + "SLICE: a retained result could not be updated." => Some(("common", "slice-retained-update-failed")), + "SLICE: created %{count} result object(s)." => Some(("common", "slice-created-results")), "SOLID Specify first point:" => Some(("draw", "solid-specify-first-point")), "SOLID Specify fourth point:" => Some(("draw", "solid-specify-fourth-point")), "SOLID Specify second point:" => Some(("draw", "solid-specify-second-point")), @@ -3871,8 +3904,19 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "CYLINDER Specify axis endpoint:" => Some(("model", "cylinder-specify-axis-endpoint")), "Elliptical" => Some(("model", "elliptical")), "Select a solid edge:" => Some(("model", "select-a-solid-edge")), + "Select an edge or [Chain/Loop/Radius]:" => Some(("model", "filletedge-select-edge-or-chain-loop-radius")), + "Select an edge chain or [Edge/Radius]:" => Some(("model", "filletedge-select-edge-chain-or-edge-radius")), + "Select edge of loop or [Edge/Chain/Radius]:" => Some(("model", "filletedge-select-loop-edge-or-edge-chain-radius")), + "Enter an option [Accept/Next] :" => Some(("model", "filletedge-enter-option-accept-next")), + "Press Enter to accept the fillet or [Radius]:" => Some(("model", "filletedge-accept-or-radius")), + "Enter fillet radius or [Expression] <{:.4}>:" => Some(("model", "filletedge-enter-radius-or-expression")), + "Specify Radius or [Expression] <{:.4}>:" => Some(("model", "filletedge-specify-radius-or-expression")), + "Enter expression:" => Some(("model", "filletedge-enter-expression")), "Specify fillet radius <{:.3}>:" => Some(("model", "specify-fillet-radius")), "Specify chamfer distance <{:.3}>:" => Some(("model", "specify-chamfer-distance")), + "A selected edge no longer belongs to the solid." => Some(("model", "filletedge-stale-edge")), + "The solid history could not be created." => Some(("model", "filletedge-history-create-failed")), + "The fillet history could not be recorded." => Some(("model", "filletedge-history-append-failed")), "POLYSOLID Height = {:.4}, Width = {:.4}, Justification = {}\n{}" => Some(("model", "polysolid-height-width-justification")), "Specify start point or [Object/Height/Width/Justify] :" => Some(("model", "specify-start-point-or-object-height-width-justify-object")), "Specify height <{:.4}>:" => Some(("model", "specify-height")), diff --git a/src/main.rs b/src/main.rs index f197aa6f9..a691981e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -134,7 +134,9 @@ fn main() -> iced::Result { Ok(text) => text .lines() .map(str::trim) - .filter(|l| !l.is_empty() && !l.starts_with('#')) + // Blank script rows are significant: they submit Enter to + // the active command (for example, accepting a preview). + .filter(|l| !l.starts_with('#') && !l.starts_with(';')) .map(str::to_string) .collect(), Err(e) => { diff --git a/src/modules/draw/draw/arc.rs b/src/modules/draw/draw/arc.rs index 80e8f2da1..c1851af62 100644 --- a/src/modules/draw/draw/arc.rs +++ b/src/modules/draw/draw/arc.rs @@ -7,7 +7,7 @@ use cadkernel::geom2d::{self, Curve as KernelCurve}; use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::IconKind; -use crate::scene::model::wire_model::WireModel; +use crate::scene::model::wire_model::{TangentGeom, WireModel}; use glam::DVec3; const TAU: f64 = std::f64::consts::TAU; @@ -71,9 +71,9 @@ fn arc_preview( end_angle: f64, plane: WorkingPlane, ) -> Option { - let center = plane.to_local(center); + let center_local = plane.to_local(center); let arc = geom2d::bounded_arc( - [center.x, center.y], + [center_local.x, center_local.y], radius, start_angle, end_angle, @@ -81,14 +81,23 @@ fn arc_preview( let points = KernelCurve::Arc(arc) .tessellate_angle(TAU / 64.0) .into_iter() - .map(|point| plane.to_world(DVec3::new(point[0], point[1], center.z)).to_array()) + .map(|point| plane.to_world(DVec3::new(point[0], point[1], center_local.z)).to_array()) .collect(); - Some(WireModel::solid_f64( + let mut wire = WireModel::solid_f64( "rubber_band".into(), points, WireModel::CYAN, false, - )) + ); + wire.tangent_geoms.push(TangentGeom::Arc { + center: [center.x, center.y, center.z], + axis_x: plane.x.to_array(), + axis_y: plane.y.to_array(), + radius: arc.radius, + start_angle: arc.start_angle, + end_angle: arc.end_angle, + }); + Some(wire) } fn make_arc( diff --git a/src/modules/draw/draw/circle.rs b/src/modules/draw/draw/circle.rs index 12c684bf5..38dc0731f 100644 --- a/src/modules/draw/draw/circle.rs +++ b/src/modules/draw/draw/circle.rs @@ -11,7 +11,7 @@ use crate::t; use crate::command::{CadCommand, CmdResult, DynField, TangentObject, WorkingPlane}; use crate::modules::draw::defaults; use crate::modules::IconKind; -use crate::scene::model::wire_model::WireModel; +use crate::scene::model::wire_model::{TangentGeom, WireModel}; use glam::DVec3; const TAU: f64 = std::f64::consts::TAU; @@ -56,16 +56,23 @@ pub const ICON: IconKind = ICON_CR; // ── Shared geometry ──────────────────────────────────────────────────────── fn circle_wire(center: DVec3, radius: f64, plane: WorkingPlane) -> WireModel { - let center = plane.to_local(center); + let center_local = plane.to_local(center); let points = KernelCurve::Circle(KernelCircle { - centre: [center.x, center.y], + centre: [center_local.x, center_local.y], radius, }) .tessellate_angle(TAU / 64.0) .into_iter() - .map(|point| plane.to_world(DVec3::new(point[0], point[1], center.z)).to_array()) + .map(|point| plane.to_world(DVec3::new(point[0], point[1], center_local.z)).to_array()) .collect(); - WireModel::solid_f64("rubber_band".into(), points, WireModel::CYAN, false) + let mut wire = WireModel::solid_f64("rubber_band".into(), points, WireModel::CYAN, false); + wire.tangent_geoms.push(TangentGeom::PlanarCircle { + center: [center.x, center.y, center.z], + axis_x: plane.x.to_array(), + axis_y: plane.y.to_array(), + radius, + }); + wire } fn make_circle(center: DVec3, radius: f64, plane: WorkingPlane) -> EntityType { diff --git a/src/modules/draw/draw/ellipse.rs b/src/modules/draw/draw/ellipse.rs index 1b975efde..db8af61c4 100644 --- a/src/modules/draw/draw/ellipse.rs +++ b/src/modules/draw/draw/ellipse.rs @@ -11,7 +11,7 @@ use crate::t; use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::IconKind; -use crate::scene::model::wire_model::WireModel; +use crate::scene::model::wire_model::{TangentGeom, WireModel}; use glam::DVec3; fn parse_num(text: &str) -> Option { @@ -75,7 +75,16 @@ fn ellipse_wire( [p.x, p.y, p.z] }) .collect(); - WireModel::solid_f64("rubber_band".into(), pts, WireModel::CYAN, false) + let mut wire = WireModel::solid_f64("rubber_band".into(), pts, WireModel::CYAN, false); + wire.tangent_geoms.push(TangentGeom::PlanarEllipse { + center: [center.x, center.y, center.z], + major_axis: [major.x, major.y, major.z], + normal: plane.z.to_array(), + minor_axis_ratio: ratio, + start_param: t_start, + end_param: t_e, + }); + wire } /// Convert a world point to the parametric angle on the ellipse. diff --git a/src/modules/draw/draw/polyline.rs b/src/modules/draw/draw/polyline.rs index 61542c690..852c42400 100644 --- a/src/modules/draw/draw/polyline.rs +++ b/src/modules/draw/draw/polyline.rs @@ -17,7 +17,7 @@ use crate::t; use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; -use crate::scene::model::wire_model::WireModel; +use crate::scene::model::wire_model::{TangentGeom, WireModel}; // ── Ribbon definition ────────────────────────────────────────────────────── @@ -443,6 +443,7 @@ impl CadCommand for PlineCommand { let pt = self.plane.to_local(pt_world).as_vec3(); let mut pts: Vec<[f32; 3]> = Vec::new(); + let mut tangent_geom = None; match self.mode { SegMode::Line => { pts.push([last.x, last.y, last.z]); @@ -456,8 +457,44 @@ impl CadCommand for PlineCommand { .map(|t| t.as_dvec2()) .unwrap_or(DVec2::new(1.0, 0.0)); let bulge = compute_bulge(a, tangent, b); - let arc_pts = arc_sample_points(last, bulge, pt, 16); - pts.extend_from_slice(&arc_pts); + if bulge.abs() >= 1e-9 && (b - a).length_squared() >= 1e-12 { + if let Some(arc) = + crate::entities::common::BulgeArc::from_bulge([a.x, a.y], [b.x, b.y], bulge) + { + for s in arc.tessellate_angle(std::f64::consts::TAU / 64.0) { + pts.push([s[0] as f32, s[1] as f32, last.z]); + } + let (sa, ea) = if arc.sweep >= 0.0 { + (arc.start_angle, arc.end_angle) + } else { + (arc.end_angle, arc.start_angle) + }; + let world_center = self + .plane + .to_world(DVec3::new(arc.center[0], arc.center[1], last.z as f64)); + if arc.radius > 0.0 + && arc.radius.is_finite() + && arc.radius <= 1e6 + && sa.is_finite() + && ea.is_finite() + { + tangent_geom = Some(TangentGeom::Arc { + center: world_center.to_array(), + axis_x: self.plane.x.to_array(), + axis_y: self.plane.y.to_array(), + radius: arc.radius, + start_angle: sa, + end_angle: ea, + }); + } + } else { + let arc_pts = arc_sample_points(last, bulge, pt, 64); + pts.extend_from_slice(&arc_pts); + } + } else { + pts.push([last.x, last.y, last.z]); + pts.push([pt.x, pt.y, pt.z]); + } } } @@ -465,12 +502,16 @@ impl CadCommand for PlineCommand { .into_iter() .map(|point| self.plane.to_world(Vec3::from_array(point).as_dvec3()).as_vec3().to_array()) .collect(); - Some(WireModel::solid( + let mut wire = WireModel::solid( "rubber_band".into(), world_points, WireModel::CYAN, false, - )) + ); + if let Some(tg) = tangent_geom { + wire.tangent_geoms.push(tg); + } + Some(wire) } } @@ -548,4 +589,20 @@ mod tests { CmdResult::FinalizeLiveEntity(h) if h == handle )); } + + #[test] + fn test_draw_line_then_arc() { + let mut cmd = PlineCommand::new(); + cmd.on_point(DVec3::new(0.0, 0.0, 0.0)); + let res = cmd.on_point(DVec3::new(10.0, 0.0, 0.0)); + assert!(matches!(res, CmdResult::CommitLiveEntity(_))); + cmd.set_live_handle(Handle::new(1)); + cmd.on_text_input("A"); + let wire = cmd.on_mouse_move(DVec3::new(10.0, 5.0, 0.0)).expect("arc wire"); + assert_eq!(wire.tangent_geoms.len(), 1); + assert!(matches!(wire.tangent_geoms[0], TangentGeom::Arc { .. })); + assert!(wire.points.len() >= 16); + let res2 = cmd.on_point(DVec3::new(10.0, 5.0, 0.0)); + assert!(matches!(res2, CmdResult::UpdateLiveEntity { .. })); + } } diff --git a/src/modules/model/edge_cmd.rs b/src/modules/model/edge_cmd.rs index 65ee2d315..ae7cb1303 100644 --- a/src/modules/model/edge_cmd.rs +++ b/src/modules/model/edge_cmd.rs @@ -1,42 +1,311 @@ +use std::sync::atomic::{AtomicU64, Ordering}; + use acadrust::Handle; +use cadkernel::brep::{Body, EdgeKey, FaceKey}; use glam::DVec3; use crate::command::{ - CadCommand, CmdResult, DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec, + CadCommand, CmdOption, CmdResult, DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec, }; +use crate::scene::model::wire_model::WireModel; -#[derive(Clone, Copy)] +static FILLET_RADIUS_BITS: AtomicU64 = AtomicU64::new(1.0f64.to_bits()); + +fn fillet_radius() -> f64 { + f64::from_bits(FILLET_RADIUS_BITS.load(Ordering::Relaxed)) +} + +fn set_fillet_radius(value: f64) { + FILLET_RADIUS_BITS.store(value.to_bits(), Ordering::Relaxed); +} + +#[derive(Clone, Copy, PartialEq, Eq)] pub enum EdgeOperation { Fillet, Chamfer, } +#[derive(Clone, Copy, PartialEq, Eq)] +enum EdgeSelectionMode { + Edge, + Chain, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum EdgeStep { + Selecting, + PickingLoop, + LoopConfirm, + PreviewConfirm, + Radius, + Expression, + ChamferDistance1, + ChamferDistance2, +} + pub struct SolidEdgeCommand { operation: EdgeOperation, target: Option, - picked: Option<(Handle, DVec3)>, + bodies: Vec<(Handle, Body)>, + handle: Option, + selected_edges: Vec, + base_face: Option, + selection_batches: Vec<(Vec, DVec3)>, + selection_mode: EdgeSelectionMode, + step: EdgeStep, + value_return: EdgeStep, + expression_return: EdgeStep, + loop_candidates: Vec<(FaceKey, Vec)>, + loop_index: usize, + last_pick: Option, default_value: f64, + other_value: f64, + preview_color: [f32; 4], + preview_wires: Vec, + preview_hidden: Vec, } impl SolidEdgeCommand { - pub fn new(operation: EdgeOperation, target: Option) -> Self { + pub fn new( + operation: EdgeOperation, + target: Option, + bodies: Vec<(Handle, Body)>, + preview_color: [f32; 4], + ) -> Self { Self { operation, target, - picked: None, - default_value: 1.0, + bodies, + handle: None, + selected_edges: Vec::new(), + base_face: None, + selection_batches: Vec::new(), + selection_mode: EdgeSelectionMode::Edge, + step: EdgeStep::Selecting, + value_return: EdgeStep::Selecting, + expression_return: EdgeStep::Radius, + loop_candidates: Vec::new(), + loop_index: 0, + last_pick: None, + default_value: if operation == EdgeOperation::Fillet { + fillet_radius() + } else { + 1.0 + }, + other_value: 1.0, + preview_color, + preview_wires: Vec::new(), + preview_hidden: Vec::new(), } } - fn finish(&self, value: f64) -> CmdResult { - let Some((handle, pick)) = self.picked else { + pub fn new_chamfer( + target: Option, + bodies: Vec<(Handle, Body)>, + preview_color: [f32; 4], + distances: (f64, f64), + ) -> Self { + let mut command = Self::new(EdgeOperation::Chamfer, target, bodies, preview_color); + command.default_value = positive_default(distances.0); + command.other_value = positive_default(distances.1); + command + } + + pub fn chamfer_distances(&self) -> (f64, f64) { + (self.default_value, self.other_value) + } + + fn body(&self, handle: Handle) -> Option<&Body> { + self.bodies + .iter() + .find_map(|(candidate, body)| (*candidate == handle).then_some(body)) + } + + fn active_body(&self) -> Option<(Handle, &Body)> { + let handle = self.handle?; + self.body(handle).map(|body| (handle, body)) + } + + fn pick_allowed(&self, handle: Handle) -> bool { + !handle.is_null() + && self.target.is_none_or(|target| target == handle) + && self.handle.is_none_or(|selected| selected == handle) + && self.body(handle).is_some() + } + + fn add_batch(&mut self, edges: Vec, anchor: DVec3) -> bool { + let batch = edges + .into_iter() + .filter(|edge| !self.selected_edges.contains(edge)) + .collect::>(); + if batch.is_empty() { + return false; + } + self.selected_edges.extend(batch.iter().copied()); + self.selection_batches.push((batch, anchor)); + true + } + + fn current_loop(&self) -> Option<&[EdgeKey]> { + self.loop_candidates + .get(self.loop_index) + .map(|(_, edges)| edges.as_slice()) + } + + fn current_loop_face(&self) -> Option { + self.loop_candidates + .get(self.loop_index) + .map(|(face, _)| *face) + } + + fn preview_edges(&self) -> Vec { + let mut edges = self.selected_edges.clone(); + if self.step == EdgeStep::LoopConfirm { + if let Some(loop_edges) = self.current_loop() { + for edge in loop_edges { + if !edges.contains(edge) { + edges.push(*edge); + } + } + } + } + edges + } + + fn preview_active(&self) -> bool { + matches!( + self.step, + EdgeStep::Selecting + | EdgeStep::PickingLoop + | EdgeStep::LoopConfirm + | EdgeStep::PreviewConfirm + | EdgeStep::Radius + | EdgeStep::Expression + | EdgeStep::ChamferDistance1 + | EdgeStep::ChamferDistance2 + ) + } + + fn preview_for_values( + &self, + value: f64, + other_value: f64, + ) -> Option<(Handle, Vec)> { + if !self.preview_active() { + return None; + } + let edges = self.preview_edges(); + if edges.is_empty() { + return None; + } + let (handle, body) = self.active_body()?; + let result = match self.operation { + EdgeOperation::Fillet => cadkernel::brep::fillet_edges(body, &edges, value).ok()?, + EdgeOperation::Chamfer => cadkernel::brep::chamfer_edges( + body, + &edges, + self.base_face.or_else(|| self.current_loop_face())?, + value, + other_value, + ) + .ok()?, + }; + let mut wires = crate::scene::model::solid_model::grip_preview_wires(&result, handle); + for wire in &mut wires { + wire.color = self.preview_color; + wire.name = format!("{}-{}-PREVIEW", handle.value(), self.name()); + } + (!wires.is_empty()).then_some((handle, wires)) + } + + fn rebuild_preview(&mut self) { + self.preview_wires.clear(); + self.preview_hidden.clear(); + if let Some((handle, wires)) = + self.preview_for_values(self.default_value, self.other_value) + { + self.preview_wires = wires; + self.preview_hidden.push(handle); + } + } + + fn begin_radius(&mut self, return_to: EdgeStep) -> CmdResult { + self.value_return = return_to; + self.step = EdgeStep::Radius; + self.rebuild_preview(); + CmdResult::NeedPoint + } + + fn accept_radius(&mut self, value: f64) -> Option { + if value <= 0.0 || !value.is_finite() { + return None; + } + self.default_value = value; + if self.operation == EdgeOperation::Fillet { + set_fillet_radius(value); + } + self.step = self.value_return; + self.rebuild_preview(); + Some(CmdResult::NeedPoint) + } + + fn begin_distances(&mut self, return_to: EdgeStep) -> CmdResult { + self.value_return = return_to; + self.step = EdgeStep::ChamferDistance1; + self.rebuild_preview(); + CmdResult::NeedPoint + } + + fn accept_distance(&mut self, value: f64) -> Option { + if value <= 0.0 || !value.is_finite() { + return None; + } + match self.step { + EdgeStep::ChamferDistance1 => { + self.default_value = value; + self.step = EdgeStep::ChamferDistance2; + } + EdgeStep::ChamferDistance2 => { + self.other_value = value; + self.step = self.value_return; + } + _ => return None, + } + self.rebuild_preview(); + Some(CmdResult::NeedPoint) + } + + fn accept_loop(&mut self) -> CmdResult { + if let (Some(edges), Some(anchor)) = ( + self.current_loop().map(|edges| edges.to_vec()), + self.last_pick, + ) { + if self.operation == EdgeOperation::Chamfer && self.base_face.is_none() { + self.base_face = self.current_loop_face(); + } + self.add_batch(edges, anchor); + } + self.loop_candidates.clear(); + self.loop_index = 0; + self.step = EdgeStep::Selecting; + self.rebuild_preview(); + CmdResult::NeedPoint + } + + fn finish(&self) -> CmdResult { + let Some(handle) = self.handle else { return CmdResult::Cancel; }; + if self.selected_edges.is_empty() { + return CmdResult::Cancel; + } CmdResult::SolidEdgeBlend { handle, - pick, - value, - fillet: matches!(self.operation, EdgeOperation::Fillet), + edges: self.selected_edges.clone(), + base_face: self.base_face, + value: self.default_value, + other_value: self.other_value, + fillet: self.operation == EdgeOperation::Fillet, } } } @@ -44,27 +313,114 @@ impl SolidEdgeCommand { impl CadCommand for SolidEdgeCommand { fn name(&self) -> &'static str { match self.operation { - EdgeOperation::Fillet => "SOLIDFILLET", - EdgeOperation::Chamfer => "SOLIDCHAMFER", + EdgeOperation::Fillet => "FILLETEDGE", + EdgeOperation::Chamfer => "CHAMFEREDGE", } } fn prompt(&self) -> String { - if self.picked.is_none() { - return crate::t!("Select a solid edge:").into_owned(); + match (self.operation, self.step, self.selection_mode) { + (EdgeOperation::Chamfer, EdgeStep::Selecting, _) => { + crate::t!("Select an edge or [Loop/Distance]:").into_owned() + } + (EdgeOperation::Chamfer, EdgeStep::PickingLoop, _) => { + crate::t!("Select edge of loop or [Edge/Distance]:").into_owned() + } + (EdgeOperation::Chamfer, EdgeStep::PreviewConfirm, _) => { + crate::t!("Press Enter to accept the chamfer or [Distance]:").into_owned() + } + (EdgeOperation::Chamfer, EdgeStep::ChamferDistance1, _) => crate::tf!( + "Specify Distance1 or [Expression] <{:.4}>:", + self.default_value + ) + .into_owned(), + (EdgeOperation::Chamfer, EdgeStep::ChamferDistance2, _) => crate::tf!( + "Specify Distance2 or [Expression] <{:.4}>:", + self.other_value + ) + .into_owned(), + (_, EdgeStep::Selecting, EdgeSelectionMode::Edge) => { + crate::t!("Select an edge or [Chain/Loop/Radius]:").into_owned() + } + (_, EdgeStep::Selecting, EdgeSelectionMode::Chain) => { + crate::t!("Select an edge chain or [Edge/Radius]:").into_owned() + } + (_, EdgeStep::PickingLoop, _) => { + crate::t!("Select edge of loop or [Edge/Chain/Radius]:").into_owned() + } + (_, EdgeStep::LoopConfirm, _) => { + crate::t!("Enter an option [Accept/Next] :").into_owned() + } + (_, EdgeStep::PreviewConfirm, _) => { + crate::t!("Press Enter to accept the fillet or [Radius]:").into_owned() + } + (_, EdgeStep::Radius, _) if self.value_return == EdgeStep::PreviewConfirm => crate::tf!( + "Specify Radius or [Expression] <{:.4}>:", + self.default_value + ) + .into_owned(), + (_, EdgeStep::Radius, _) => crate::tf!( + "Enter fillet radius or [Expression] <{:.4}>:", + self.default_value + ) + .into_owned(), + (_, EdgeStep::Expression, _) => crate::t!("Enter expression:").into_owned(), + _ => String::new(), } - match self.operation { - EdgeOperation::Fillet => { - crate::tf!("Specify fillet radius <{:.3}>:", self.default_value).into_owned() + } + + fn options(&self) -> Vec { + match (self.operation, self.step, self.selection_mode) { + (EdgeOperation::Chamfer, EdgeStep::Selecting, _) => vec![ + CmdOption::new("Loop", "L"), + CmdOption::new("Distance", "D"), + ], + (EdgeOperation::Chamfer, EdgeStep::PickingLoop, _) => vec![ + CmdOption::new("Edge", "E"), + CmdOption::new("Distance", "D"), + ], + (EdgeOperation::Chamfer, EdgeStep::LoopConfirm, _) => vec![ + CmdOption::new("Accept", "A"), + CmdOption::new("Next", "N"), + ], + (EdgeOperation::Chamfer, EdgeStep::PreviewConfirm, _) => { + vec![CmdOption::new("Distance", "D")] } - EdgeOperation::Chamfer => { - crate::tf!("Specify chamfer distance <{:.3}>:", self.default_value).into_owned() + ( + EdgeOperation::Chamfer, + EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2, + _, + ) => vec![CmdOption::new("Expression", "E")], + (EdgeOperation::Fillet, EdgeStep::Selecting, EdgeSelectionMode::Edge) => vec![ + CmdOption::new("Chain", "C"), + CmdOption::new("Loop", "L"), + CmdOption::new("Radius", "R"), + ], + (EdgeOperation::Fillet, EdgeStep::Selecting, EdgeSelectionMode::Chain) => vec![ + CmdOption::new("Edge", "E"), + CmdOption::new("Radius", "R"), + ], + (EdgeOperation::Fillet, EdgeStep::PickingLoop, _) => vec![ + CmdOption::new("Edge", "E"), + CmdOption::new("Chain", "C"), + CmdOption::new("Radius", "R"), + ], + (EdgeOperation::Fillet, EdgeStep::LoopConfirm, _) => vec![ + CmdOption::new("Accept", "A"), + CmdOption::new("Next", "N"), + ], + (EdgeOperation::Fillet, EdgeStep::PreviewConfirm, _) => { + vec![CmdOption::new("Radius", "R")] } + (EdgeOperation::Fillet, EdgeStep::Radius, _) => { + vec![CmdOption::new("Expression", "E")] + } + _ => Vec::new(), } } fn needs_entity_pick(&self) -> bool { - self.picked.is_none() + matches!(self.step, EdgeStep::Selecting | EdgeStep::PickingLoop) } fn entity_pick_includes_fills(&self) -> bool { @@ -80,52 +436,281 @@ impl CadCommand for SolidEdgeCommand { } fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult { - if handle.is_null() || self.target.is_some_and(|target| target != handle) { + if !self.pick_allowed(handle) { return CmdResult::NeedPoint; } - self.picked = Some((handle, point)); + let Some(seed) = self + .body(handle) + .and_then(|body| crate::scene::model::solid_model::nearest_edge(body, point.to_array())) + else { + return CmdResult::NeedPoint; + }; + self.handle = Some(handle); + self.last_pick = Some(point); + + if self.step == EdgeStep::PickingLoop { + self.loop_candidates = edge_loops(self.body(handle).expect("pick body exists"), seed); + if self.operation == EdgeOperation::Chamfer { + if let Some(base_face) = self.base_face { + self.loop_candidates + .retain(|(face, _)| *face == base_face); + } + } + self.loop_index = 0; + self.step = if self.loop_candidates.is_empty() { + EdgeStep::Selecting + } else { + EdgeStep::LoopConfirm + }; + self.rebuild_preview(); + return CmdResult::NeedPoint; + } + + if self.operation == EdgeOperation::Chamfer { + if self.base_face.is_none() { + self.base_face = self.body(handle).and_then(|body| { + nearest_edge_face(body, seed, point.to_array()) + }); + } + let Some(base_face) = self.base_face else { + return CmdResult::NeedPoint; + }; + if !edge_belongs_to_face(self.body(handle).expect("pick body exists"), seed, base_face) + { + return CmdResult::NeedPoint; + } + self.add_batch(vec![seed], point); + self.rebuild_preview(); + return CmdResult::NeedPoint; + } + + let edges = match self.selection_mode { + EdgeSelectionMode::Edge => vec![seed], + EdgeSelectionMode::Chain => edge_chain(self.body(handle).expect("pick body exists"), seed), + }; + self.add_batch(edges, point); + self.rebuild_preview(); CmdResult::NeedPoint } fn on_point(&mut self, point: DVec3) -> CmdResult { - let Some((_, pick)) = self.picked else { + if !matches!( + self.step, + EdgeStep::Radius | EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2 + ) { + return CmdResult::NeedPoint; + } + let Some(anchor) = self.last_pick else { return CmdResult::NeedPoint; }; - let value = point.distance(pick); - if value > 0.0 && value.is_finite() { - self.finish(value) - } else { - CmdResult::NeedPoint + let value = point.distance(anchor); + if value <= 0.0 || !value.is_finite() { + return CmdResult::NeedPoint; + } + match self.step { + EdgeStep::Radius => self.accept_radius(value).unwrap_or(CmdResult::NeedPoint), + EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2 => { + self.accept_distance(value).unwrap_or(CmdResult::NeedPoint) + } + _ => CmdResult::NeedPoint, } } fn on_text_input(&mut self, text: &str) -> Option { - if self.picked.is_none() { - return None; + let keyword = text.trim().to_ascii_uppercase(); + if self.operation == EdgeOperation::Chamfer { + return match self.step { + EdgeStep::Selecting => match keyword.as_str() { + "L" | "LOOP" => { + self.step = EdgeStep::PickingLoop; + Some(CmdResult::NeedPoint) + } + "D" | "DISTANCE" => Some(self.begin_distances(EdgeStep::Selecting)), + _ => None, + }, + EdgeStep::PickingLoop => match keyword.as_str() { + "E" | "EDGE" => { + self.step = EdgeStep::Selecting; + Some(CmdResult::NeedPoint) + } + "D" | "DISTANCE" => Some(self.begin_distances(EdgeStep::PickingLoop)), + _ => None, + }, + EdgeStep::LoopConfirm => match keyword.as_str() { + "A" | "ACCEPT" => Some(self.accept_loop()), + "N" | "NEXT" if !self.loop_candidates.is_empty() => { + self.loop_index = (self.loop_index + 1) % self.loop_candidates.len(); + self.rebuild_preview(); + Some(CmdResult::NeedPoint) + } + _ => None, + }, + EdgeStep::PreviewConfirm => match keyword.as_str() { + "D" | "DISTANCE" => Some(self.begin_distances(EdgeStep::PreviewConfirm)), + _ => None, + }, + EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2 => { + if matches!(keyword.as_str(), "E" | "EXPRESSION") { + self.expression_return = self.step; + self.step = EdgeStep::Expression; + Some(CmdResult::NeedPoint) + } else { + let value = keyword.parse::().ok()?; + self.accept_distance(value) + } + } + EdgeStep::Expression => { + let value = crate::app::expr_eval::eval_number(text)?; + self.step = self.expression_return; + self.accept_distance(value) + } + EdgeStep::Radius => None, + }; + } + + match self.step { + EdgeStep::Selecting => match keyword.as_str() { + "R" | "RADIUS" => Some(self.begin_radius(EdgeStep::Selecting)), + "C" | "CHAIN" => { + self.selection_mode = EdgeSelectionMode::Chain; + Some(CmdResult::NeedPoint) + } + "E" | "EDGE" if self.selection_mode == EdgeSelectionMode::Chain => { + self.selection_mode = EdgeSelectionMode::Edge; + Some(CmdResult::NeedPoint) + } + "L" | "LOOP" => { + self.step = EdgeStep::PickingLoop; + Some(CmdResult::NeedPoint) + } + _ => None, + }, + EdgeStep::PickingLoop => match keyword.as_str() { + "E" | "EDGE" => { + self.selection_mode = EdgeSelectionMode::Edge; + self.step = EdgeStep::Selecting; + Some(CmdResult::NeedPoint) + } + "C" | "CHAIN" => { + self.selection_mode = EdgeSelectionMode::Chain; + self.step = EdgeStep::Selecting; + Some(CmdResult::NeedPoint) + } + "R" | "RADIUS" => Some(self.begin_radius(EdgeStep::PickingLoop)), + _ => None, + }, + EdgeStep::LoopConfirm => match keyword.as_str() { + "A" | "ACCEPT" => Some(self.accept_loop()), + "N" | "NEXT" if !self.loop_candidates.is_empty() => { + self.loop_index = (self.loop_index + 1) % self.loop_candidates.len(); + self.rebuild_preview(); + Some(CmdResult::NeedPoint) + } + _ => None, + }, + EdgeStep::PreviewConfirm => match keyword.as_str() { + "R" | "RADIUS" => Some(self.begin_radius(EdgeStep::PreviewConfirm)), + _ => None, + }, + EdgeStep::Radius => { + if matches!(keyword.as_str(), "E" | "EXPRESSION") { + self.step = EdgeStep::Expression; + return Some(CmdResult::NeedPoint); + } + let value = keyword.parse::().ok()?; + self.accept_radius(value) + } + EdgeStep::Expression => { + let value = crate::app::expr_eval::eval_number(text)?; + self.accept_radius(value) + } + EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2 => None, } - let value = text.trim().parse::().ok()?; - (value > 0.0 && value.is_finite()).then(|| self.finish(value)) } fn on_enter(&mut self) -> CmdResult { - if self.picked.is_some() { - self.finish(self.default_value) - } else { - CmdResult::Cancel + match (self.operation, self.step) { + (EdgeOperation::Chamfer, EdgeStep::Selecting) if !self.selected_edges.is_empty() => { + self.step = EdgeStep::PreviewConfirm; + self.rebuild_preview(); + CmdResult::NeedPoint + } + (EdgeOperation::Chamfer, EdgeStep::LoopConfirm) => self.accept_loop(), + (EdgeOperation::Chamfer, EdgeStep::PreviewConfirm) => self.finish(), + (EdgeOperation::Chamfer, EdgeStep::ChamferDistance1) => self + .accept_distance(self.default_value) + .unwrap_or(CmdResult::NeedPoint), + (EdgeOperation::Chamfer, EdgeStep::ChamferDistance2) => self + .accept_distance(self.other_value) + .unwrap_or(CmdResult::NeedPoint), + (EdgeOperation::Chamfer, EdgeStep::Expression) => CmdResult::NeedPoint, + (EdgeOperation::Chamfer, _) => CmdResult::Cancel, + (_, EdgeStep::Selecting) if !self.selected_edges.is_empty() => { + self.step = EdgeStep::PreviewConfirm; + self.rebuild_preview(); + CmdResult::NeedPoint + } + (_, EdgeStep::LoopConfirm) => self.accept_loop(), + (_, EdgeStep::PreviewConfirm) => self.finish(), + (_, EdgeStep::Radius) => self + .accept_radius(self.default_value) + .unwrap_or(CmdResult::NeedPoint), + (_, EdgeStep::Expression) => CmdResult::NeedPoint, + _ => CmdResult::Cancel, } } + fn on_undo_step(&mut self) -> Option { + if self.step == EdgeStep::LoopConfirm { + self.loop_candidates.clear(); + self.loop_index = 0; + self.step = EdgeStep::Selecting; + self.rebuild_preview(); + return Some(CmdResult::NeedPoint); + } + let (batch, _) = self.selection_batches.pop()?; + for edge in batch { + if let Some(index) = self.selected_edges.iter().position(|candidate| *candidate == edge) { + self.selected_edges.remove(index); + } + } + if self.selected_edges.is_empty() { + self.handle = None; + self.base_face = None; + } + self.last_pick = self + .selection_batches + .last() + .map(|(_, anchor)| *anchor); + self.step = EdgeStep::Selecting; + self.rebuild_preview(); + Some(CmdResult::NeedPoint) + } + fn wants_text_input(&self) -> bool { - self.picked.is_some() + matches!( + self.step, + EdgeStep::Radius + | EdgeStep::Expression + | EdgeStep::ChamferDistance1 + | EdgeStep::ChamferDistance2 + ) } fn dyn_commit_as_text(&self) -> bool { - self.picked.is_some() + matches!( + self.step, + EdgeStep::Radius | EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2 + ) } fn dyn_spec(&self) -> Option { - let (_, pick) = self.picked?; - Some(DynSpec { + let pick = self.last_pick?; + matches!( + self.step, + EdgeStep::Radius | EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2 + ) + .then(|| DynSpec { anchor: DynAnchor::Point(pick), fields: vec![DynFieldSpec::new(match self.operation { EdgeOperation::Fillet => DynRole::Radius, @@ -137,10 +722,163 @@ impl CadCommand for SolidEdgeCommand { } fn dyn_live_value(&self, cursor: DVec3) -> Option { - self.picked.map(|(_, pick)| cursor.distance(pick)) + matches!( + self.step, + EdgeStep::Radius | EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2 + ) + .then(|| self.last_pick.map(|pick| cursor.distance(pick))) + .flatten() + } + + fn on_preview_wires(&mut self, cursor: DVec3) -> Vec { + if matches!( + self.step, + EdgeStep::Radius | EdgeStep::ChamferDistance1 | EdgeStep::ChamferDistance2 + ) { + if let Some(value) = self.last_pick.map(|pick| cursor.distance(pick)) { + if value > 0.0 && value.is_finite() { + let values = match self.step { + EdgeStep::ChamferDistance2 => (self.default_value, value), + _ => (value, self.other_value), + }; + if let Some((handle, wires)) = self.preview_for_values(values.0, values.1) { + self.preview_wires = wires; + self.preview_hidden.clear(); + self.preview_hidden.push(handle); + } else { + self.preview_wires.clear(); + self.preview_hidden.clear(); + } + } + } + } + self.preview_wires.clone() + } + + fn preview_hidden_handles(&self) -> &[Handle] { + &self.preview_hidden } } +fn edge_loops(body: &Body, seed: EdgeKey) -> Vec<(FaceKey, Vec)> { + let Some(edge) = body.edges.get(seed) else { + return Vec::new(); + }; + let mut candidates = Vec::new(); + for coedge_key in &edge.coedges { + let Some(coedge) = body.coedges.get(*coedge_key) else { + continue; + }; + let Some(edge_loop) = body.loops.get(coedge.owner) else { + continue; + }; + let edges = edge_loop + .coedges + .iter() + .filter_map(|key| body.coedges.get(*key).map(|coedge| coedge.edge)) + .collect::>(); + let candidate = (edge_loop.owner, edges); + if !candidate.1.is_empty() && !candidates.contains(&candidate) { + candidates.push(candidate); + } + } + candidates +} + +fn edge_belongs_to_face(body: &Body, edge: EdgeKey, face: FaceKey) -> bool { + body.edges.get(edge).is_some_and(|edge| { + edge.coedges.iter().any(|coedge| { + body.coedges + .get(*coedge) + .and_then(|coedge| body.loops.get(coedge.owner)) + .is_some_and(|edge_loop| edge_loop.owner == face) + }) + }) +} + +fn nearest_edge_face(body: &Body, edge: EdgeKey, point: [f64; 3]) -> Option { + let edge = body.edges.get(edge)?; + edge.coedges + .iter() + .filter_map(|coedge| { + let edge_loop = body.loops.get(body.coedges.get(*coedge)?.owner)?; + let face = body.faces.get(edge_loop.owner)?; + let surface = body.surfaces.get(face.surface)?; + let distance = surface.distance_to(point).abs(); + distance.is_finite().then_some((edge_loop.owner, distance)) + }) + .min_by(|first, second| first.1.total_cmp(&second.1)) + .map(|(face, _)| face) +} + +fn positive_default(value: f64) -> f64 { + if value.is_finite() && value > 0.0 { + value + } else { + 1.0 + } +} + +fn edge_chain(body: &Body, seed: EdgeKey) -> Vec { + let mut chain = vec![seed]; + let mut next = 0; + while next < chain.len() { + let current_key = chain[next]; + next += 1; + let Some(current) = body.edges.get(current_key) else { + continue; + }; + for vertex in [current.start, current.end] { + let Some(current_tangent) = edge_tangent_from_vertex(body, current_key, vertex) else { + continue; + }; + for candidate_key in body.edge_keys() { + if chain.contains(&candidate_key) { + continue; + } + let Some(candidate) = body.edges.get(candidate_key) else { + continue; + }; + if candidate.start != vertex && candidate.end != vertex { + continue; + } + let Some(candidate_tangent) = + edge_tangent_from_vertex(body, candidate_key, vertex) + else { + continue; + }; + if current_tangent.dot(candidate_tangent) < -0.999 { + chain.push(candidate_key); + } + } + } + } + chain +} + +fn edge_tangent_from_vertex( + body: &Body, + edge_key: EdgeKey, + vertex: cadkernel::brep::VertexKey, +) -> Option { + let edge = body.edges.get(edge_key)?; + let curve = body.curves.get(edge.curve)?; + let span = edge.end_parameter - edge.start_parameter; + if !span.is_finite() || span.abs() <= f64::EPSILON { + return None; + } + let step = span * 1.0e-6; + let (at, inside) = if edge.start == vertex { + (edge.start_parameter, edge.start_parameter + step) + } else if edge.end == vertex { + (edge.end_parameter, edge.end_parameter - step) + } else { + return None; + }; + let tangent = DVec3::from(curve.point_at(inside)) - DVec3::from(curve.point_at(at)); + (tangent.length_squared() > 0.0 && tangent.is_finite()).then(|| tangent.normalize()) +} + inventory::submit!(crate::command::CommandRegistration { - names: &["SOLIDFILLET", "SOLIDCHAMFER"] + names: &["FILLETEDGE", "SOLIDFILLET", "CHAMFEREDGE", "SOLIDCHAMFER"] }); diff --git a/src/modules/model/mod.rs b/src/modules/model/mod.rs index 12ed38ea2..546342d00 100644 --- a/src/modules/model/mod.rs +++ b/src/modules/model/mod.rs @@ -6,6 +6,7 @@ pub mod edge_cmd; pub mod polysolid_cmd; pub mod primitive_cmd; pub mod shell_cmd; +pub mod slice_cmd; use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef}; @@ -89,8 +90,8 @@ impl CadModule for ModelModule { RibbonGroup { title: "Edges", tools: vec![ - RibbonItem::LargeTool(tool("SOLIDFILLET", "Fillet", FILLET_ICON)), - RibbonItem::LargeTool(tool("SOLIDCHAMFER", "Chamfer", CHAMFER_ICON)), + RibbonItem::LargeTool(tool("FILLETEDGE", "Fillet Edge", FILLET_ICON)), + RibbonItem::LargeTool(tool("CHAMFEREDGE", "Chamfer", CHAMFER_ICON)), RibbonItem::LargeTool(tool("SHELL", "Shell", PRESSPULL_ICON)), ], }, diff --git a/src/modules/model/slice_cmd.rs b/src/modules/model/slice_cmd.rs new file mode 100644 index 000000000..b64909bb8 --- /dev/null +++ b/src/modules/model/slice_cmd.rs @@ -0,0 +1,510 @@ +use acadrust::{EntityType, Handle}; +use cadkernel::space::Plane; +use glam::DVec3; + +use crate::command::{CadCommand, CmdOption, CmdResult, SelectionEntity, WorkingPlane}; +use crate::scene::model::wire_model::WireModel; + +#[derive(Clone, Copy)] +enum PlaneKind { + Xy, + Yz, + Zx, + View, +} + +enum Step { + Targets, + FirstPoint, + ThreeFirst, + ThreeSecond(DVec3), + ThirdPoint(DVec3, DVec3), + AxisFirst, + AxisSecond(DVec3), + PlanePoint(PlaneKind), + PickObject, + PickSurface, + Side(Plane), + SurfaceSide, +} + +/// Interactive front-end for plane-based SLICE. +pub struct SliceCommand { + step: Step, + targets: Vec, + selected: Vec, + working: WorkingPlane, + view_normal: DVec3, + preview_center: DVec3, + preview_radius: f64, + picked: Option, + surface_tool: Option, + notice: Option<&'static str>, +} + +impl SliceCommand { + pub fn new( + targets: Vec, + view_normal: DVec3, + preview_center: DVec3, + preview_radius: f64, + ) -> Self { + let step = if targets.is_empty() { Step::Targets } else { Step::FirstPoint }; + Self { + step, + targets, + selected: Vec::new(), + working: WorkingPlane::default(), + view_normal: view_normal.normalize_or(DVec3::Z), + preview_center, + preview_radius: preview_radius.max(1.0), + picked: None, + surface_tool: None, + notice: None, + } + } + + fn plane(origin: DVec3, x: DVec3, normal: DVec3) -> Option { + Plane::orthonormal(origin.to_array(), x.to_array(), normal.to_array()) + } + + fn enter_side(&mut self, plane: Plane) -> CmdResult { + self.notice = None; + self.step = Step::Side(plane); + CmdResult::NeedPoint + } + + fn finish(&mut self, keep_point: Option) -> CmdResult { + let Step::Side(plane) = self.step else { + return CmdResult::NeedPoint; + }; + CmdResult::SliceEntities { + targets: std::mem::take(&mut self.targets), + plane, + keep_point, + } + } + + fn picked_plane(&self) -> Option { + let entity = self.picked.as_ref()?; + if !matches!( + entity, + EntityType::Arc(_) + | EntityType::Circle(_) + | EntityType::Ellipse(_) + | EntityType::Spline(_) + | EntityType::LwPolyline(_) + | EntityType::Polyline(_) + ) { + return None; + } + crate::scene::model::presspull_model::profile_geometry(entity) + .map(|(plane, _, _)| plane) + } + + fn finish_surface(&mut self, keep_point: Option) -> CmdResult { + let Some(cutter) = self.surface_tool.take() else { + return CmdResult::NeedPoint; + }; + CmdResult::SliceSurfaceEntities { + targets: std::mem::take(&mut self.targets), + cutter: Box::new(cutter), + keep_point, + } + } + + fn plane_at(&self, point: DVec3, kind: PlaneKind) -> Option { + let (x, normal) = match kind { + PlaneKind::Xy => (self.working.x, self.working.z), + PlaneKind::Yz => (self.working.y, self.working.x), + PlaneKind::Zx => (self.working.z, self.working.y), + PlaneKind::View => { + let candidate = if self.working.x.dot(self.view_normal).abs() < 0.98 { + self.working.x + } else { + self.working.y + }; + (candidate, self.view_normal) + } + }; + Self::plane(point, x, normal) + } +} + +impl CadCommand for SliceCommand { + fn name(&self) -> &'static str { + "SLICE" + } + + fn prompt(&self) -> String { + if let Some(notice) = self.notice { + return crate::t!(notice).into_owned(); + } + match self.step { + Step::Targets => crate::t!( + "SLICE Select solids or surfaces to slice, then press Enter:" + ) + .into_owned(), + Step::FirstPoint => crate::t!( + "SLICE Specify first point on slicing plane or [planar Object/Surface/Zaxis/View/XY/YZ/ZX/3points] <3points>:" + ) + .into_owned(), + Step::ThreeFirst => { + crate::t!("SLICE Specify first point on slicing plane:").into_owned() + } + Step::ThreeSecond(_) => { + crate::t!("SLICE Specify second point on slicing plane:").into_owned() + } + Step::ThirdPoint(_, _) => { + crate::t!("SLICE Specify third point on slicing plane:").into_owned() + } + Step::AxisFirst => { + crate::t!("SLICE Specify a point on the slicing plane:").into_owned() + } + Step::AxisSecond(_) => { + crate::t!("SLICE Specify a point on the plane's normal axis:").into_owned() + } + Step::PlanePoint(PlaneKind::View) => { + crate::t!("SLICE Specify a point on the current view plane:").into_owned() + } + Step::PlanePoint(_) => { + crate::t!("SLICE Specify a point on the current UCS plane:").into_owned() + } + Step::PickObject => crate::t!("SLICE Select a planar curve object:").into_owned(), + Step::PickSurface => crate::t!("SLICE Select a cutting surface:").into_owned(), + Step::Side(_) => crate::t!( + "SLICE Specify a point on the desired side or [Both] :" + ) + .into_owned(), + Step::SurfaceSide => crate::t!( + "SLICE Select a sliced object to keep or [Both] :" + ) + .into_owned(), + } + } + + fn options(&self) -> Vec { + match self.step { + Step::FirstPoint => vec![ + CmdOption::new(crate::t!("planar Object").as_ref(), "O"), + CmdOption::new(crate::t!("Surface").as_ref(), "S"), + CmdOption::new(crate::t!("Zaxis").as_ref(), "Z"), + CmdOption::new(crate::t!("View").as_ref(), "V"), + CmdOption::new("XY", "XY"), + CmdOption::new("YZ", "YZ"), + CmdOption::new("ZX", "ZX"), + CmdOption::new(crate::t!("3points").as_ref(), "3"), + ], + Step::Side(_) | Step::SurfaceSide => { + vec![CmdOption::new(crate::t!("Both").as_ref(), "B")] + } + _ => Vec::new(), + } + } + + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.working = plane; + } + + fn on_point(&mut self, point: DVec3) -> CmdResult { + self.notice = None; + match self.step { + Step::FirstPoint => { + self.step = Step::ThreeSecond(point); + CmdResult::NeedPoint + } + Step::ThreeFirst => { + self.step = Step::ThreeSecond(point); + CmdResult::NeedPoint + } + Step::ThreeSecond(first) => { + self.step = Step::ThirdPoint(first, point); + CmdResult::NeedPoint + } + Step::ThirdPoint(first, second) => { + let x = second - first; + let normal = x.cross(point - first); + Self::plane(first, x, normal) + .map_or(CmdResult::NeedPoint, |plane| self.enter_side(plane)) + } + Step::AxisFirst => { + self.step = Step::AxisSecond(point); + CmdResult::NeedPoint + } + Step::AxisSecond(origin) => { + let normal = point - origin; + let x = if self.working.x.dot(normal.normalize_or(DVec3::Z)).abs() < 0.98 { + self.working.x + } else { + self.working.y + }; + Self::plane(origin, x, normal) + .map_or(CmdResult::NeedPoint, |plane| self.enter_side(plane)) + } + Step::PlanePoint(kind) => self + .plane_at(point, kind) + .map_or(CmdResult::NeedPoint, |plane| self.enter_side(plane)), + Step::Side(_) => self.finish(Some(point)), + Step::SurfaceSide => self.finish_surface(Some(point)), + _ => CmdResult::NeedPoint, + } + } + + fn on_enter(&mut self) -> CmdResult { + match self.step { + Step::Targets if !self.selected.is_empty() => { + self.targets = std::mem::take(&mut self.selected); + self.step = Step::FirstPoint; + CmdResult::DeselectAndContinue + } + Step::FirstPoint => { + self.step = Step::ThreeFirst; + CmdResult::NeedPoint + } + Step::Side(_) => self.finish(None), + Step::SurfaceSide => self.finish_surface(None), + _ => CmdResult::NeedPoint, + } + } + + fn on_text_input(&mut self, text: &str) -> Option { + let keyword = text.trim().to_ascii_uppercase(); + Some(match self.step { + Step::FirstPoint => match keyword.as_str() { + "O" | "OBJECT" => { + self.step = Step::PickObject; + CmdResult::NeedPoint + } + "S" | "SURFACE" => { + self.step = Step::PickSurface; + CmdResult::NeedPoint + } + "Z" | "ZAXIS" => { + self.step = Step::AxisFirst; + CmdResult::NeedPoint + } + "V" | "VIEW" => { + self.step = Step::PlanePoint(PlaneKind::View); + CmdResult::NeedPoint + } + "XY" => { + self.step = Step::PlanePoint(PlaneKind::Xy); + CmdResult::NeedPoint + } + "YZ" => { + self.step = Step::PlanePoint(PlaneKind::Yz); + CmdResult::NeedPoint + } + "ZX" | "XZ" => { + self.step = Step::PlanePoint(PlaneKind::Zx); + CmdResult::NeedPoint + } + "3" | "3P" | "3POINTS" => { + self.step = Step::ThreeFirst; + CmdResult::NeedPoint + } + _ => CmdResult::NeedPoint, + }, + Step::Side(_) if matches!(keyword.as_str(), "" | "B" | "BOTH") => self.finish(None), + Step::SurfaceSide if matches!(keyword.as_str(), "" | "B" | "BOTH") => { + self.finish_surface(None) + } + _ => CmdResult::NeedPoint, + }) + } + + fn point_step_accepts_keywords(&self) -> bool { + matches!(self.step, Step::FirstPoint | Step::Side(_) | Step::SurfaceSide) + } + + fn is_selection_gathering(&self) -> bool { + matches!(self.step, Step::Targets) + } + + fn selection_forces_add(&self) -> bool { + true + } + + fn inject_selection_entities(&mut self, entities: Vec) { + self.selected = entities + .into_iter() + .filter_map(|item| { + matches!(item.entity, EntityType::Solid3D(_) | EntityType::Surface(_)) + .then_some(item.handle) + }) + .collect(); + } + + fn on_selection_complete(&mut self, _handles: Vec) -> CmdResult { + CmdResult::NeedPoint + } + + fn needs_entity_pick(&self) -> bool { + matches!( + self.step, + Step::PickObject | Step::PickSurface | Step::SurfaceSide + ) + } + + fn entity_pick_highlights_hover(&self) -> bool { + self.needs_entity_pick() + } + + fn inject_before_entity_pick(&self) -> bool { + matches!(self.step, Step::PickObject | Step::PickSurface) + } + + fn entity_pick_uses_surface_point(&self) -> bool { + matches!(self.step, Step::SurfaceSide) + } + + fn inject_picked_entity(&mut self, entity: EntityType) { + self.picked = Some(entity); + } + + fn on_entity_pick(&mut self, _handle: Handle, _point: DVec3) -> CmdResult { + if matches!(self.step, Step::SurfaceSide) { + return self.finish_surface(Some(_point)); + } + let surface_only = matches!(self.step, Step::PickSurface); + if surface_only { + let body = self.picked.as_ref().and_then(|entity| match entity { + EntityType::Surface(surface) => { + crate::scene::convert::solid3d_tess::kernel_surface_body(surface) + } + _ => None, + }); + if let Some(body) = body { + if let Some(plane) = planar_body_plane(&body) { + return self.enter_side(plane); + } + if body.face_keys().count() == 1 { + self.surface_tool = Some(body); + self.notice = None; + self.step = Step::SurfaceSide; + return CmdResult::NeedPoint; + } + } + self.notice = Some( + "SLICE The selected surface does not define one supported cutting sheet. Select another surface:", + ); + return CmdResult::NeedPoint; + } + match self.picked_plane() { + Some(plane) => self.enter_side(plane), + None => { + self.notice = Some( + "SLICE The selected object does not define a plane. Select a planar curve object:", + ); + CmdResult::NeedPoint + } + } + } + + fn on_preview_wires(&mut self, point: DVec3) -> Vec { + let mut wires = Vec::new(); + let plane = match self.step { + Step::ThirdPoint(first, second) => { + wires.push(WireModel::solid_f64( + "slice_triangle".into(), + vec![ + first.to_array(), + second.to_array(), + point.to_array(), + first.to_array(), + ], + WireModel::CYAN, + false, + )); + Self::plane(first, second - first, (second - first).cross(point - first)) + } + Step::AxisSecond(origin) => { + wires.push(WireModel::solid_f64( + "slice_normal".into(), + vec![origin.to_array(), point.to_array()], + WireModel::CYAN, + false, + )); + let normal = point - origin; + let x = if self.working.x.dot(normal.normalize_or(DVec3::Z)).abs() < 0.98 { + self.working.x + } else { + self.working.y + }; + Self::plane(origin, x, normal) + } + Step::PlanePoint(kind) => self.plane_at(point, kind), + Step::Side(plane) => Some(plane), + _ => None, + }; + if let Some(plane) = plane { + wires.extend(plane_grid( + plane, + self.preview_center, + self.preview_radius, + )); + } + wires + } +} + +fn planar_body_plane(body: &cadkernel::brep::Body) -> Option { + let mut faces = body.face_keys(); + let first = cadkernel::brep::planar_face_profile(body, faces.next()?)?.plane; + let normal = DVec3::from_array(first.normal()?); + let tolerance = cadkernel::brep::operation_tolerance(&[body]); + faces + .all(|face| { + let Some(profile) = cadkernel::brep::planar_face_profile(body, face) else { + return false; + }; + profile.plane.normal().is_some_and(|other| { + normal.dot(DVec3::from_array(other)).abs() >= 1.0 - 1e-9 + && first + .distance_to(profile.plane.origin) + .is_some_and(|gap| gap.abs() <= tolerance * 4.0) + }) + }) + .then_some(first) +} + +fn plane_grid(plane: Plane, centre: DVec3, radius: f64) -> Vec { + let normal = DVec3::from_array(plane.normal().unwrap_or([0.0, 0.0, 1.0])); + let projected = centre + - normal + * plane + .distance_to(centre.to_array()) + .unwrap_or_default(); + let x = DVec3::from_array(plane.x_axis); + let y = DVec3::from_array(plane.y_axis); + let color = [0.2, 0.55, 1.0, 1.0]; + (-5..=5) + .flat_map(|index| { + let offset = radius * index as f64 / 5.0; + [ + WireModel::solid_f64( + format!("slice_grid_x_{index}"), + vec![ + (projected + x * offset - y * radius).to_array(), + (projected + x * offset + y * radius).to_array(), + ], + color, + false, + ), + WireModel::solid_f64( + format!("slice_grid_y_{index}"), + vec![ + (projected + y * offset - x * radius).to_array(), + (projected + y * offset + x * radius).to_array(), + ], + color, + false, + ), + ] + }) + .collect() +} + +inventory::submit!(crate::command::CommandRegistration { + names: &["SLICE", "SL"] +}); diff --git a/src/scene/convert/acis_export.rs b/src/scene/convert/acis_export.rs index 48e3730ce..42875bb84 100644 --- a/src/scene/convert/acis_export.rs +++ b/src/scene/convert/acis_export.rs @@ -21,3 +21,54 @@ pub fn solid_to_sat(body: &Body) -> Option { let binary = SabWriter::write(&document); valid(&SabReader::read(&binary).ok()?).then_some(document) } + +#[cfg(test)] +mod tests { + use super::*; + use cadkernel::geom2d::{Arc, Curve}; + use cadkernel::space::Plane; + + fn circle_section(z: f64, radius: f64) -> (Plane, Vec) { + let curves = (0..4) + .map(|part| { + let start = std::f64::consts::FRAC_PI_2 * part as f64; + Curve::Arc(Arc { + centre: [0.0, 0.0], + radius, + start_angle: start, + end_angle: start + std::f64::consts::FRAC_PI_2, + }) + }) + .collect(); + ( + Plane::from_axes([0.0, 0.0, z], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]), + curves, + ) + } + + #[test] + fn a_curved_loft_round_trips_through_text_and_binary_acis() { + let body = cadkernel::brep::loft(&[ + circle_section(0.0, 5.0), + circle_section(10.0, 2.0), + ]) + .unwrap(); + assert!(body.validate().is_empty()); + + let mut document = SatDocument::new(); + cadkernel::acis::append(&body, &mut document).unwrap(); + let text = document.to_sat_string(); + let parsed = SatDocument::parse(&text).unwrap(); + let (restored, text_loss) = cadkernel::acis::lift(&parsed); + assert!(text_loss.is_empty(), "{text_loss:?}"); + assert_eq!(restored.len(), 1); + assert!(restored[0].validate().is_empty()); + + let binary = SabWriter::write(&parsed); + let parsed_binary = SabReader::read(&binary).unwrap(); + let (restored, binary_loss) = cadkernel::acis::lift(&parsed_binary); + assert!(binary_loss.is_empty(), "{binary_loss:?}"); + assert_eq!(restored.len(), 1); + assert!(restored[0].validate().is_empty()); + } +} diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 31762e586..cbbfacd7a 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -219,7 +219,33 @@ fn split_mixed_polyline( // Emit each arc segment as an analytical wire (CircleGpu target) for tg in tangent_geoms { - if matches!(tg, TangentGeom::Arc { .. }) { + if let TangentGeom::Arc { + center, + axis_x, + axis_y, + radius, + start_angle, + end_angle, + } = *tg + { + let mut arc_pts = Vec::with_capacity(17); + let n = 16; + let sweep = if end_angle >= start_angle { + end_angle - start_angle + } else { + end_angle - start_angle + std::f64::consts::TAU + }; + for s in 0..=n { + let frac = s as f64 / n as f64; + let ang = start_angle + frac * sweep; + let p = [ + center[0] + radius * (ang.cos() * axis_x[0] + ang.sin() * axis_y[0]), + center[1] + radius * (ang.cos() * axis_x[1] + ang.sin() * axis_y[1]), + center[2] + radius * (ang.cos() * axis_x[2] + ang.sin() * axis_y[2]), + ]; + arc_pts.push(p); + } + let (points, points_low) = points_to_ds(arc_pts); out.push(WireModel { bg_adapt: None, point_marker: None, @@ -238,8 +264,8 @@ fn split_mixed_polyline( dash_align_end: None, text_verts: Vec::new(), name: name.to_string(), - points: Vec::new(), - points_low: Vec::new(), + points, + points_low, color, selected, pattern_length, diff --git a/src/scene/entity.rs b/src/scene/entity.rs index a1dbbf3af..ca4609047 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -596,8 +596,8 @@ impl Scene { .filter_map(|&handle| { let from_history = self .document - .solid_history_operation(handle) - .and_then(|operation| cadkernel::acis::rebuild_body(operation).ok()); + .solid_history_operations(handle) + .and_then(|operations| cadkernel::acis::rebuild_history(&operations).ok()); let body = from_history.or_else(|| match self.document.get_entity(handle) { Some(EntityType::Solid3D(solid)) => { crate::scene::convert::solid3d_tess::kernel_body(solid) diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 07d219156..f559ff4bc 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -437,6 +437,8 @@ pub struct PreparedOpenGeometry { pub tess_guard: u64, /// Loader block definitions; the receiving scene supplies its own epoch. pub block_defns: Vec<(u64, Arc)>, + /// Loader-computed draw-order map, re-stamped by the receiving scene. + pub(crate) draw_depths: Option, } impl std::fmt::Debug for PreparedOpenGeometry { @@ -557,7 +559,8 @@ fn inserted_draw_depth_label(order: &[DrawDepthEntry], position: usize) -> Optio } } -struct DrawDepthCache { +#[derive(Clone)] +pub(crate) struct DrawDepthCache { epoch: u64, depths: Arc>, /// Per-block stable order labels, including deleted tombstones retained for @@ -1026,6 +1029,7 @@ pub fn prepare_open_geometry( // Taken, not cloned: the scene is discarded on the next line. let tess_memo = std::mem::take(&mut *scene.resident_tess_memo.borrow_mut()); let tess_guard = scene.resident_tess_guard.get(); + let draw_depths = scene.draw_depth_cache.borrow_mut().take(); let block_defns: Vec<(u64, Arc)> = std::mem::take(&mut *scene.block_defn_cache.borrow_mut()) .into_iter() @@ -1040,6 +1044,7 @@ pub fn prepare_open_geometry( tess_memo, tess_guard, block_defns, + draw_depths, }, ) } @@ -1432,6 +1437,8 @@ pub(crate) enum NavPerfOp { Pan, Zoom, Rotate, + /// A message that changed geometry before the next rendered frame. + Edit, } impl NavPerfOp { @@ -1440,6 +1447,7 @@ impl NavPerfOp { Self::Pan => "pan", Self::Zoom => "zoom", Self::Rotate => "rotate", + Self::Edit => "edit", } } } @@ -1447,6 +1455,8 @@ impl NavPerfOp { #[derive(Clone, Copy, Debug)] pub(in crate::scene) struct NavPerfSample { pub(in crate::scene) op: NavPerfOp, + /// Message label that caused the sample. + pub(in crate::scene) cause: &'static str, pub(in crate::scene) space: &'static str, pub(in crate::scene) mode: &'static str, pub(in crate::scene) started: iced::time::Instant, @@ -1699,6 +1709,9 @@ pub struct Scene { /// constants. `[depth, half]` retains a fixed child sub-range for block /// composition. Full sort/layout/block changes rebuild the labels. draw_depth_cache: RefCell>, + /// Bumped when a full draw-order rebuild can move existing depths. + /// Cached uploads compare it before reusing baked depth values. + draw_depth_generation: std::cell::Cell, /// Shared empty map for 3-D wireframe, where true depth wins instead of /// the entity submission order used by the optimized 2-D style. no_draw_depths: Arc>, @@ -1847,11 +1860,16 @@ pub struct Scene { block_members_cache: RefCell>, /// Insert/Viewport/Block/BlockEnd handles omitted by the spatial index. unindexable_cache: RefCell)>>, - /// Distinct entity type names present in a layout, keyed by - /// (geometry_epoch, layout block). The status bar asks for this on every - /// frame to populate the selection-filter menu, but the answer only - /// changes when entities are added or removed. - layout_type_names_cache: RefCell>)>>, + /// `(epoch, layout block, present type names, list exposed to the UI)`. + /// The set lets pure additions update without rebuilding the list. + layout_type_names_cache: RefCell< + Option<( + u64, + Handle, + std::collections::BTreeSet, + std::sync::Arc>, + )>, + >, /// Reverse dependencies from layer/style/block definitions to the top-level /// entities whose resident wire runs actually change. Kept independent from /// `geometry_epoch`: a layer colour toggle can reuse the index, invalidate @@ -2046,6 +2064,7 @@ impl Scene { interaction_handle_index_cache: RefCell::new(None), sort_cache: RefCell::new(None), draw_depth_cache: RefCell::new(None), + draw_depth_generation: std::cell::Cell::new(0), no_draw_depths: Arc::new(HashMap::default()), hatch_cache: RefCell::new(HashMap::default()), wipeout_cache: RefCell::new(HashMap::default()), @@ -2250,6 +2269,11 @@ impl Scene { *self.resident_tess_memo.borrow_mut() = prepared.tess_memo; self.resident_tess_guard.set(prepared.tess_guard); } + // Reuse the loader's draw-order map with this scene's epoch. + if let Some(mut depths) = prepared.draw_depths { + depths.epoch = self.geometry_epoch; + *self.draw_depth_cache.borrow_mut() = Some(depths); + } // Same for the block definitions, stamped with this scene's block // epoch: the document has not been touched between the loader // finishing and this call, so they describe it exactly. @@ -2875,6 +2899,15 @@ impl Scene { } pub(crate) fn record_nav_perf(&self, op: NavPerfOp, started: iced::time::Instant) { + self.record_nav_perf_caused(op, "-", started); + } + + pub(crate) fn record_nav_perf_caused( + &self, + op: NavPerfOp, + cause: &'static str, + started: iced::time::Instant, + ) { if !crate::perf::enabled() { return; } @@ -2887,6 +2920,7 @@ impl Scene { }; self.nav_perf_pending.set(Some(NavPerfSample { op, + cause, space, mode, started, @@ -5982,6 +6016,11 @@ impl Scene { /// A full build assigns sparse labels in effective draw order. Incremental /// Add/Remove then changes only the named handle: existing siblings retain /// their depth, avoiding an O(all entities) map rewrite and GPU const upload. + /// See [`Self::draw_depth_generation`]. + pub(in crate::scene) fn draw_depth_generation(&self) -> u64 { + self.draw_depth_generation.get() + } + pub(super) fn draw_depth_map(&self) -> Arc> { { let cache = self.draw_depth_cache.borrow(); @@ -6111,6 +6150,11 @@ impl Scene { } } + // From here the labels are assigned afresh, so an existing entity's + // depth can move. Anything holding a baked depth has to rebuild. + self.draw_depth_generation + .set(self.draw_depth_generation.get().wrapping_add(1)); + use acadrust::objects::ObjectType; // Per-block SortEntitiesTable overrides: block -> (entity_val -> sort_val). let mut overrides: HashMap> = HashMap::default(); @@ -8298,24 +8342,33 @@ impl Scene { let Some(indices) = lookup.get(&handle) else { continue; }; - if indices - .iter() - .filter_map(|&index| meshes.get(index as usize)) - .any(|set| { - set.geometry_lods().first().is_some_and(|mesh| { - !pick::hit_test::mesh_box_hit( - a, - b, - crossing, - std::iter::once((handle, mesh, set.instance_transform)), - view_rot, - eye, - bounds, - ) - .is_empty() - }) + let test = |set: &MeshLodSet| { + set.geometry_lods().first().is_some_and(|mesh| { + !pick::hit_test::mesh_box_hit( + a, + b, + crossing, + std::iter::once((handle, mesh, set.instance_transform)), + view_rot, + eye, + bounds, + ) + .is_empty() }) - { + }; + let mut sets = indices + .iter() + .filter_map(|&index| meshes.get(index as usize)); + let hit = if crossing { + sets.any(test) + } else { + let mut count = 0; + sets.all(|set| { + count += 1; + test(set) + }) && count > 0 + }; + if hit { out.push(handle); } } @@ -8466,23 +8519,32 @@ impl Scene { let Some(indices) = lookup.get(&handle) else { continue; }; - if indices - .iter() - .filter_map(|&index| meshes.get(index as usize)) - .any(|set| { - set.geometry_lods().first().is_some_and(|mesh| { - !pick::hit_test::mesh_poly_hit( - poly, - crossing, - std::iter::once((handle, mesh, set.instance_transform)), - view_rot, - eye, - bounds, - ) - .is_empty() - }) + let test = |set: &MeshLodSet| { + set.geometry_lods().first().is_some_and(|mesh| { + !pick::hit_test::mesh_poly_hit( + poly, + crossing, + std::iter::once((handle, mesh, set.instance_transform)), + view_rot, + eye, + bounds, + ) + .is_empty() }) - { + }; + let mut sets = indices + .iter() + .filter_map(|&index| meshes.get(index as usize)); + let hit = if crossing { + sets.any(test) + } else { + let mut count = 0; + sets.all(|set| { + count += 1; + test(set) + }) && count > 0 + }; + if hit { out.push(handle); } } @@ -8724,23 +8786,32 @@ impl Scene { let Some(indices) = lookup.get(&handle) else { continue; }; - let hit = indices + let test = |set: &MeshLodSet| { + set.geometry_lods().first().is_some_and(|mesh| { + !pick::hit_test::mesh_box_hit( + a, + b, + crossing, + std::iter::once((handle, mesh, set.instance_transform)), + view_rot, + eye, + bounds, + ) + .is_empty() + }) + }; + let mut sets = indices .iter() - .filter_map(|&index| meshes.get(index as usize)) - .any(|set| { - set.geometry_lods().first().is_some_and(|mesh| { - !pick::hit_test::mesh_box_hit( - a, - b, - crossing, - std::iter::once((handle, mesh, set.instance_transform)), - view_rot, - eye, - bounds, - ) - .is_empty() - }) - }); + .filter_map(|&index| meshes.get(index as usize)); + let hit = if crossing { + sets.any(test) + } else { + let mut count = 0; + sets.all(|set| { + count += 1; + test(set) + }) && count > 0 + }; if hit { out.push(handle); } @@ -8775,22 +8846,31 @@ impl Scene { let Some(indices) = lookup.get(&handle) else { continue; }; - let hit = indices + let test = |set: &MeshLodSet| { + set.geometry_lods().first().is_some_and(|mesh| { + !pick::hit_test::mesh_poly_hit( + poly, + crossing, + std::iter::once((handle, mesh, set.instance_transform)), + view_rot, + eye, + bounds, + ) + .is_empty() + }) + }; + let mut sets = indices .iter() - .filter_map(|&index| meshes.get(index as usize)) - .any(|set| { - set.geometry_lods().first().is_some_and(|mesh| { - !pick::hit_test::mesh_poly_hit( - poly, - crossing, - std::iter::once((handle, mesh, set.instance_transform)), - view_rot, - eye, - bounds, - ) - .is_empty() - }) - }); + .filter_map(|&index| meshes.get(index as usize)); + let hit = if crossing { + sets.any(test) + } else { + let mut count = 0; + sets.all(|set| { + count += 1; + test(set) + }) && count > 0 + }; if hit { out.push(handle); } @@ -8810,6 +8890,27 @@ impl Scene { frozen_layers: Option<&HashSet>, annotation_scale_handle: Option, all_visible: bool, + ) -> bool { + let layer = self.document.layers.get(&e.common().layer); + self.resident_entity_visible_with_layer( + e, + block_handle, + frozen_layers, + annotation_scale_handle, + all_visible, + layer, + ) + } + + /// Visibility predicate with the layer resolved by the caller. + fn resident_entity_visible_with_layer( + &self, + e: &EntityType, + block_handle: Handle, + frozen_layers: Option<&HashSet>, + annotation_scale_handle: Option, + all_visible: bool, + layer: Option<&acadrust::tables::layer::Layer>, ) -> bool { let c = e.common(); if c.invisible { @@ -8823,7 +8924,6 @@ impl Scene { if matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_)) { return false; } - let layer = self.document.layers.get(&c.layer); if layer .map(|l| l.flags.off || l.flags.frozen) .unwrap_or(false) @@ -8850,8 +8950,8 @@ impl Scene { self.belongs_to_visible_block(c.handle, c.owner_handle, block_handle) } - fn wires_for_block_culled( - &self, + fn wires_for_block_culled<'doc>( + &'doc self, block_handle: Handle, view_aabb: Option<[f32; 4]>, wpp: Option, @@ -8905,13 +9005,25 @@ impl Scene { // Visibility test reused by both paths below — and by the resident // incremental patch, so a changed entity is included/excluded exactly as // a from-scratch build would (no divergence). - let visibility_ok = |e: &EntityType| { - self.resident_entity_visible( + // Resolve each distinct layer name once for this candidate pass. + type LayerRef<'a> = Option<&'a acadrust::tables::layer::Layer>; + let layer_of: RefCell>> = + RefCell::new(rustc_hash::FxHashMap::default()); + let visibility_ok = |e: &'doc EntityType| { + let name = e.common().layer.as_str(); + let layer = match layer_of.borrow_mut().entry(name) { + std::collections::hash_map::Entry::Occupied(hit) => *hit.get(), + std::collections::hash_map::Entry::Vacant(slot) => { + *slot.insert(self.document.layers.get(name)) + } + }; + self.resident_entity_visible_with_layer( e, block_handle, frozen_layers, annotation_scale_handle, all_visible, + layer, ) }; @@ -8923,6 +9035,9 @@ impl Scene { // version of this diagnostic report a constant. let visible_path: &str; let mut visible_candidates: usize; + // PERF-only breakdown for index maintenance and handle resolution. + let mut visible_index_ms = 0.0f64; + let mut visible_probe_ms = 0.0f64; // Phase 2.1 — quadtree-driven candidate selection. When a view // AABB exists (Model layout with a settled camera), only iterate @@ -8990,16 +9105,12 @@ impl Scene { .filter(|e| visibility_ok(e)) .collect() } else { - // Ask the per-epoch membership index for this block's candidates - // instead of testing every entity in the document. For a paper - // sheet holding 43 entities in a 1.17 M-entity drawing that is the - // difference between 313 ms and nothing measurable. - // - // The index is filled by walking `document.entities()`, so each - // block's list is already in document order and the wires come out - // in exactly the order the full scan produced. + // The per-epoch index preserves document order for each block. visible_path = "block-index"; + // Time index maintenance separately from the candidate loop. + let t_members = perf.then(iced::time::Instant::now); let members = self.block_members(); + visible_index_ms = crate::perf::elapsed_ms(t_members); let (_, by_block) = &*members; let claimed = by_block.get(&block_handle).map(Vec::as_slice).unwrap_or(&[]); visible_candidates = claimed.len(); @@ -9011,6 +9122,18 @@ impl Scene { } } } + if perf { + // PERF-only lower bound for warm handle resolution. + let t_probe = iced::time::Instant::now(); + let mut resolved = 0usize; + for &handle in claimed { + if self.document.get_entity(handle).is_some() { + resolved += 1; + } + } + std::hint::black_box(resolved); + visible_probe_ms = t_probe.elapsed().as_secs_f64() * 1000.0; + } out }; @@ -9255,7 +9378,8 @@ impl Scene { "[perf] wires-build total={:.1}ms sort_cache={:.1} visible={:.1} blk_cache={:.1} colors={:.1} \ build={:.1} [classify={:.1} hits={:.1} tess={:.1} materialize={:.1}] sort={:.1}({}) \ entities={} memo_hit={} memo_miss={} wires={} memo={} visible_path={} candidates={} \ -guard={:016x} guard_stale={} avp={} anno={:.4} anno_h={} all_vis={} sdf_gen={}", +guard={:016x} guard_stale={} avp={} anno={:.4} anno_h={} all_vis={} sdf_gen={} \ +vis_index={:.1} visible_probe={:.1}", crate::perf::elapsed_ms(t_fn), sort_cache_ms, visible_ms, @@ -9282,6 +9406,8 @@ guard={:016x} guard_stale={} avp={} anno={:.4} anno_h={} all_vis={} sdf_gen={}", annotation_scale_handle.map(|h| h.value()).unwrap_or(0), all_visible, crate::scene::text::sdf_atlas::generation(), + visible_index_ms, + visible_probe_ms, ); } wires @@ -9308,15 +9434,50 @@ guard={:016x} guard_stale={} avp={} anno={:.4} anno_h={} all_vis={} sdf_gen={}", std::cell::Ref::map(self.unindexable_cache.borrow(), |c| c.as_ref().unwrap()) } - /// Resolve owner handles and block-record membership once per geometry epoch. + /// Resolve block membership once per geometry epoch. A single addition is + /// appended safely; any other delta rebuilds the document-ordered index. fn block_members(&self) -> std::cell::Ref<'_, BlockMembers> { + let mut cached_epoch = None; { let cache = self.block_members_cache.borrow(); - if cache.as_ref().is_some_and(|(epoch, _)| *epoch == self.geometry_epoch) { - drop(cache); - return std::cell::Ref::map(self.block_members_cache.borrow(), |c| { - c.as_ref().unwrap() + if let Some((epoch, _)) = cache.as_ref() { + if *epoch == self.geometry_epoch { + drop(cache); + return std::cell::Ref::map(self.block_members_cache.borrow(), |c| { + c.as_ref().unwrap() + }); + } + cached_epoch = Some(*epoch); + } + } + + if let Some(since) = cached_epoch { + let lone_add = self.replay_since(since).filter(|deltas| { + deltas.len() == 1 && deltas[0].1 == ChangeKind::Added + }); + if let Some(deltas) = lone_add { + let handle = deltas[0].0; + // Resolve the owner before touching the cache: both lookups + // borrow, and `entity_block_map` is its own `RefCell`. + let owner = self.document.get_entity(handle).and_then(|entity| { + let owner = entity.common().owner_handle; + if !owner.is_null() { + Some(owner) + } else { + self.entity_block_map().get(&handle).copied() + } }); + let mut cache = self.block_members_cache.borrow_mut(); + if let Some((epoch, members)) = cache.as_mut() { + if let Some(owner) = owner { + members.entry(owner).or_default().push(handle); + } + *epoch = self.geometry_epoch; + drop(cache); + return std::cell::Ref::map(self.block_members_cache.borrow(), |c| { + c.as_ref().unwrap() + }); + } } } let mut members: HashMap> = HashMap::default(); @@ -10491,6 +10652,116 @@ mod journal_tests { // Differential oracle: the incrementally-patched entity index must always // equal a from-scratch rebuild after any add / move / erase. + /// A line has tangent geometry but does not feed the analytical uploads. + #[test] + fn the_partition_membership_test_separates_lines_from_curves() { + use acadrust::entities::{Circle, Line}; + use acadrust::types::Vector3; + use crate::scene::pipeline::wire_arena::feeds_analytical_uploads; + + let mut scene = Scene::new(); + let feeds = |scene: &Scene, handle: Handle| { + let entity = scene.document.get_entity(handle).unwrap().clone(); + scene + .tessellate_one(&entity) + .iter() + .any(feeds_analytical_uploads) + }; + + let line = scene.add_entity(EntityType::Line(Line::from_points( + Vector3::new(0.0, 0.0, 0.0), + Vector3::new(10.0, 5.0, 0.0), + ))); + assert!( + !feeds(&scene, line), + "a drawn line must not force the partition walk", + ); + + let mut circle = Circle::new(); + circle.radius = 4.0; + let circle = scene.add_entity(EntityType::Circle(circle)); + assert!( + feeds(&scene, circle), + "a drawn circle feeds the analytical uploads and must force the walk", + ); + } + + /// Incremental additions preserve existing depths; full rebuilds invalidate them. + #[test] + fn adding_an_entity_leaves_existing_draw_depths_alone() { + use acadrust::entities::Line; + use acadrust::types::Vector3; + + fn add(scene: &mut Scene, x: f64) -> Handle { + scene.add_entity(EntityType::Line(Line::from_points( + Vector3::new(x, 0.0, 0.0), + Vector3::new(x + 1.0, 0.0, 0.0), + ))) + } + let mut scene = Scene::new(); + let first = add(&mut scene, 0.0); + let second = add(&mut scene, 1.0); + let before = scene.draw_depth_map(); + let generation = scene.draw_depth_generation(); + + add(&mut scene, 2.0); + let after = scene.draw_depth_map(); + assert_eq!( + scene.draw_depth_generation(), + generation, + "an incremental add must not invalidate baked depths", + ); + for handle in [first, second] { + assert_eq!( + after.get(&handle.value()), + before.get(&handle.value()), + "an existing entity's depth must not move when another is added", + ); + } + + // A rebuild from scratch reassigns labels, so it has to say so. + scene.draw_depth_cache.borrow_mut().take(); + let _ = scene.draw_depth_map(); + assert_ne!( + scene.draw_depth_generation(), + generation, + "a full rebuild must invalidate anything holding a baked depth", + ); + } + + /// A folded addition must match the full document-order scan. + #[test] + fn folding_one_addition_matches_the_full_scan() { + use acadrust::entities::Line; + use acadrust::types::Vector3; + + let mut scene = Scene::new(); + fn add(scene: &mut Scene, x: f64) -> Handle { + scene.add_entity(EntityType::Line(Line::from_points( + Vector3::new(x, 0.0, 0.0), + Vector3::new(x + 1.0, 0.0, 0.0), + ))) + } + add(&mut scene, 0.0); + add(&mut scene, 1.0); + // Build the index, then add one more so the next call folds. + let block = scene.model_space_block_handle(); + let before = scene.block_members().1.get(&block).cloned().unwrap_or_default(); + let third = add(&mut scene, 2.0); + let folded = scene.block_members().1.get(&block).cloned().unwrap_or_default(); + + scene.block_members_cache.borrow_mut().take(); + let rebuilt = scene.block_members().1.get(&block).cloned().unwrap_or_default(); + + assert_eq!(folded, rebuilt, "the folded index must equal a full rebuild"); + assert_eq!( + folded.last(), + Some(&third), + "an appended entity belongs at the end of its block's list", + ); + assert_eq!(folded.len(), before.len() + 1); + } + #[test] fn block_member_index_matches_the_full_scan() { use acadrust::entities::Line; @@ -10543,6 +10814,109 @@ mod journal_tests { assert_eq!(indexed(&s, block), scanned(&s, block), "after modify"); } + /// Raw layer spellings used as memo keys must resolve through the + /// case-insensitive table. + #[test] + fn the_layer_memo_respects_case_insensitive_layer_names() { + use acadrust::entities::Line; + use acadrust::tables::layer::Layer; + use acadrust::types::Vector3; + + let mut scene = Scene::new(); + let mut hidden = Layer::new("Walls"); + hidden.flags.off = true; + scene.document.layers.add_or_replace(hidden); + + let mut on_layer = |layer: &str| { + let mut line = Line::from_points( + Vector3::new(0.0, 0.0, 0.0), + Vector3::new(1.0, 0.0, 0.0), + ); + line.common.layer = layer.to_string(); + scene.add_entity(EntityType::Line(line)) + }; + // Spellings of the switched-off layer must all remain hidden. + on_layer("Walls"); + on_layer("WALLS"); + on_layer("walls"); + on_layer("Doors"); + + let block = scene.model_space_block_handle(); + let visible: Vec<_> = scene + .document + .entities() + .filter(|e| scene.resident_entity_visible(e, block, None, None, true)) + .map(|e| e.common().layer.clone()) + .collect(); + assert_eq!( + visible, + vec!["Doors".to_string()], + "every spelling of a switched-off layer must be hidden", + ); + + // Each spelling resolves to the same table entry. + let resolved: Vec<_> = ["Walls", "WALLS", "walls"] + .iter() + .map(|name| scene.document.layers.get(name).map(|l| l.name.clone())) + .collect(); + assert_eq!( + resolved, + vec![Some("Walls".to_string()); 3], + "the table is case-insensitive, which is what lets the memo key on \ + the raw name", + ); + } + + /// The loader's handed-over draw depths must equal a local rebuild. + #[test] + fn the_handed_over_draw_depth_map_matches_a_recompute() { + use acadrust::entities::{Circle, Line}; + use acadrust::types::Vector3; + + let mut scene = Scene::new(); + scene.add_entity(EntityType::Line(Line::from_points( + Vector3::new(0.0, 0.0, 0.0), + Vector3::new(1.0, 0.0, 0.0), + ))); + scene.add_entity(EntityType::Circle(Circle::new())); + scene.add_entity(EntityType::Line(Line::from_points( + Vector3::new(2.0, 0.0, 0.0), + Vector3::new(3.0, 0.0, 0.0), + ))); + + // Capture the map the loader would hand over. + let expected = scene.draw_depth_map(); + let handed_over = scene + .draw_depth_cache + .borrow_mut() + .take() + .expect("the map was just computed, so it is cached"); + + // Install it under the receiving scene's epoch. + scene.bump_geometry(); + scene.install_prepared_open_geometry(PreparedOpenGeometry { + wires: Arc::new(Vec::new()), + interaction_index: None, + tess_memo: HashMap::default(), + tess_guard: 0, + block_defns: Vec::new(), + draw_depths: Some(handed_over), + }); + assert_eq!( + *scene.draw_depth_map(), + *expected, + "the handed-over map must be served as-is", + ); + + // A local rebuild must agree. + *scene.draw_depth_cache.borrow_mut() = None; + assert_eq!( + *scene.draw_depth_map(), + *expected, + "a recompute must agree with what was handed over", + ); + } + #[test] fn entity_index_incremental_matches_full_rebuild() { use acadrust::entities::Line; diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index 7b0fc6909..1718313ee 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -1,12 +1,13 @@ use acadrust::entities::{EmbeddedEntity, Solid3D}; use acadrust::objects::{ - DynamicBlockData, ObjectType, SolidHistoryBox, SolidHistoryBrep, SolidHistoryCone, - SolidHistoryCylinder, SolidHistoryLoft, SolidHistoryLoftParameters, SolidHistoryNodeBase, SolidHistoryOperation, + DynamicBlockData, ObjectType, SolidHistoryBox, SolidHistoryBrep, SolidHistoryChamfer, + SolidHistoryCone, SolidHistoryCylinder, SolidHistoryFillet, SolidHistoryLoft, + SolidHistoryLoftParameters, SolidHistoryNodeBase, SolidHistoryOperation, SolidHistoryPyramid, SolidHistoryRevolve, SolidHistorySphere, SolidHistorySweep, SolidHistoryTorus, }; use acadrust::EntityType; -use cadkernel::brep::Body; +use cadkernel::brep::{Body, Surface}; use crate::command::EntityTransform; use crate::entities::traits::EntityTypeOps; @@ -32,6 +33,9 @@ pub const GRIP_SWEEP_PATH_FIRST: usize = 30_000; pub const GRIP_LOFT_SECTION_FIRST: usize = 40_000; pub const GRIP_REVOLVE_PROFILE_FIRST: usize = 50_000; pub const GRIP_REVOLVE_AXIS: usize = 10_013; +pub const GRIP_FILLET_RADIUS: usize = 10_014; +pub const GRIP_CHAMFER_DISTANCE1: usize = 10_015; +pub const GRIP_CHAMFER_DISTANCE2: usize = 10_016; pub const GRIP_BOX_CORNER_FIRST: usize = 10_100; pub const GRIP_BOX_FACE_X_MIN: usize = 10_110; pub const GRIP_BOX_FACE_X_MAX: usize = 10_111; @@ -197,7 +201,7 @@ pub fn has_specialized_primitive_properties( handle: acadrust::Handle, ) -> bool { matches!( - document.solid_history_operation(handle), + primitive_property_operation(document, handle).as_ref(), Some( SolidHistoryOperation::Box(_) | SolidHistoryOperation::Wedge(_) @@ -214,6 +218,19 @@ pub fn has_specialized_primitive_properties( ) } +/// Return the primitive that owns the public Geometry rows. Later edge +/// operations refine that primitive without replacing its editable type, +/// position, or dimensions. +pub fn primitive_property_operation( + document: &acadrust::CadDocument, + handle: acadrust::Handle, +) -> Option { + document + .solid_history_operations(handle) + .and_then(|operations| operations.into_iter().next()) + .or_else(|| document.solid_history_operation(handle).cloned()) +} + /// Solid history results whose public Properties palette is fully described by /// the common entity rows plus [`primitive_properties`]. A generic B-rep has /// no stable primitive dimensions to expose, but it still uses the compact @@ -1474,10 +1491,10 @@ pub fn primitive_properties( document: &acadrust::CadDocument, handle: acadrust::Handle, ) -> Vec { - let Some(operation) = document.solid_history_operation(handle) else { + let Some(operation) = primitive_property_operation(document, handle) else { return brep_properties(document, handle); }; - match operation { + match &operation { SolidHistoryOperation::Box(value) => { rectangular_properties(document, handle, value, "Box") } @@ -3161,6 +3178,149 @@ fn grip( } } +pub fn fillet_radius_grip(body: &Body, radius: f64) -> Option { + if !radius.is_finite() || radius <= 0.0 { + return None; + } + for (face_key, face) in body.faces.iter() { + let Some(Surface::Cylinder(cylinder)) = body.surfaces.get(face.surface) else { + continue; + }; + if (cylinder.radius.abs() - radius).abs() > radius.max(1.0) * 1.0e-7 { + continue; + } + let axis = glam::DVec3::from_array(cylinder.base.normal()?).try_normalize()?; + let origin = glam::DVec3::from_array(cylinder.base.origin); + let points = body + .face_coedges(face_key) + .into_iter() + .filter_map(|coedge| body.coedges.get(coedge)) + .filter_map(|coedge| body.edges.get(coedge.edge)) + .flat_map(|edge| [edge.start, edge.end]) + .filter_map(|vertex| body.vertices.get(vertex)) + .map(|vertex| glam::DVec3::from_array(vertex.point)) + .collect::>(); + let minimum = points + .iter() + .map(|point| (*point - origin).dot(axis)) + .reduce(f64::min)?; + let maximum = points + .iter() + .map(|point| (*point - origin).dot(axis)) + .reduce(f64::max)?; + let center = origin + axis * ((minimum + maximum) * 0.5); + let radial = points + .iter() + .map(|point| *point - center - axis * (*point - center).dot(axis)) + .find_map(|radial| radial.try_normalize())?; + return Some(grip( + GRIP_FILLET_RADIUS, + center + radial * radius, + GripShape::Square, + Some(radial), + )); + } + None +} + +pub fn chamfer_distance_grips( + document: &acadrust::CadDocument, + handle: acadrust::Handle, + value: &SolidHistoryChamfer, +) -> Vec { + let Some(operations) = document.solid_history_operations(handle) else { + return Vec::new(); + }; + let Some(chamfer_index) = operations + .iter() + .rposition(|operation| matches!(operation, SolidHistoryOperation::Chamfer(_))) + else { + return Vec::new(); + }; + let Ok(source) = cadkernel::acis::rebuild_history(&operations[..chamfer_index]) else { + return Vec::new(); + }; + let edges = source.edge_keys().collect::>(); + let faces = source.face_keys().collect::>(); + let Some(edge) = value + .edges + .first() + .and_then(|ordinal| usize::try_from(*ordinal).ok()) + .and_then(|ordinal| edges.get(ordinal).copied()) + .and_then(|edge| source.edges.get(edge)) + else { + return Vec::new(); + }; + let Some(base_face) = usize::try_from(value.base_face) + .ok() + .and_then(|ordinal| faces.get(ordinal).copied()) + else { + return Vec::new(); + }; + let adjacent = edge + .coedges + .iter() + .filter_map(|coedge| source.coedges.get(*coedge)) + .filter_map(|coedge| source.loops.get(coedge.owner)) + .map(|edge_loop| edge_loop.owner) + .collect::>(); + let Some(other_face) = adjacent.iter().copied().find(|face| *face != base_face) else { + return Vec::new(); + }; + let outward = |face_key| { + let face = source.faces.get(face_key)?; + let Surface::Plane(plane) = source.surfaces.get(face.surface)? else { + return None; + }; + let normal = glam::DVec3::from_array(plane.normal()?).try_normalize()?; + Some(if face.forward { normal } else { -normal }) + }; + let Some(base_normal) = outward(base_face) else { + return Vec::new(); + }; + let Some(other_normal) = outward(other_face) else { + return Vec::new(); + }; + let dot = base_normal.dot(other_normal); + let Some(base_inward) = (-(other_normal - base_normal * dot)).try_normalize() else { + return Vec::new(); + }; + let Some(other_inward) = (-(base_normal - other_normal * dot)).try_normalize() else { + return Vec::new(); + }; + let Some(start) = source.vertices.get(edge.start) else { + return Vec::new(); + }; + let Some(end) = source.vertices.get(edge.end) else { + return Vec::new(); + }; + let midpoint = + (glam::DVec3::from_array(start.point) + glam::DVec3::from_array(end.point)) * 0.5; + [ + ( + GRIP_CHAMFER_DISTANCE1, + value.base_distance, + base_inward, + ), + ( + GRIP_CHAMFER_DISTANCE2, + value.other_distance, + other_inward, + ), + ] + .into_iter() + .filter(|(_, distance, _)| distance.is_finite() && *distance > 0.0) + .map(|(id, distance, axis)| { + grip( + id, + midpoint + axis * distance, + GripShape::Square, + Some(axis), + ) + }) + .collect() +} + pub fn primitive_grips( document: &acadrust::CadDocument, handle: acadrust::Handle, @@ -3168,6 +3328,21 @@ pub fn primitive_grips( let Some(operation) = document.solid_history_operation(handle) else { return Vec::new(); }; + if let SolidHistoryOperation::Fillet(value) = operation { + let Some(radius) = value.radii.first().copied() else { + return Vec::new(); + }; + let Some(EntityType::Solid3D(solid)) = document.get_entity(handle) else { + return Vec::new(); + }; + return crate::scene::convert::solid3d_tess::kernel_body(solid) + .and_then(|body| fillet_radius_grip(&body, radius)) + .into_iter() + .collect(); + } + if let SolidHistoryOperation::Chamfer(value) = operation { + return chamfer_distance_grips(document, handle, value); + } let mut grips = Vec::new(); let mut add = |id, transform, point, shape, axis: Option<[f64; 3]>| { if let Some(world) = world_point(transform, point) { @@ -3788,3 +3963,32 @@ pub fn brep_op(body: &Body) -> SolidHistoryOperation { ..SolidHistoryBrep::default() }) } + +pub fn fillet_op(edges: Vec, radius: f64) -> SolidHistoryOperation { + SolidHistoryOperation::Fillet(SolidHistoryFillet { + base: base(glam::DMat4::IDENTITY.to_cols_array()), + operation_major: 1, + method: 0, + edges, + radii: vec![radius], + ..SolidHistoryFillet::default() + }) +} + +pub fn chamfer_op( + edges: Vec, + base_face: i32, + base_distance: f64, + other_distance: f64, +) -> SolidHistoryOperation { + SolidHistoryOperation::Chamfer(SolidHistoryChamfer { + base: base(glam::DMat4::IDENTITY.to_cols_array()), + operation_major: 1, + method: 0, + base_distance, + other_distance, + edges, + base_face, + ..SolidHistoryChamfer::default() + }) +} diff --git a/src/scene/model/wire_model.rs b/src/scene/model/wire_model.rs index 1f76e7129..30905d7a3 100644 --- a/src/scene/model/wire_model.rs +++ b/src/scene/model/wire_model.rs @@ -58,6 +58,201 @@ pub enum TangentGeom { }, } +impl TangentGeom { + pub fn translated(&self, delta: glam::DVec3) -> Self { + match self { + Self::Line { p1, p2 } => Self::Line { + p1: [p1[0] + delta.x as f32, p1[1] + delta.y as f32, p1[2] + delta.z as f32], + p2: [p2[0] + delta.x as f32, p2[1] + delta.y as f32, p2[2] + delta.z as f32], + }, + Self::Circle { center, radius } => Self::Circle { + center: [center[0] + delta.x as f32, center[1] + delta.y as f32, center[2] + delta.z as f32], + radius: *radius, + }, + Self::PlanarCircle { center, axis_x, axis_y, radius } => Self::PlanarCircle { + center: [center[0] + delta.x, center[1] + delta.y, center[2] + delta.z], + axis_x: *axis_x, + axis_y: *axis_y, + radius: *radius, + }, + Self::Arc { center, axis_x, axis_y, radius, start_angle, end_angle } => Self::Arc { + center: [center[0] + delta.x, center[1] + delta.y, center[2] + delta.z], + axis_x: *axis_x, + axis_y: *axis_y, + radius: *radius, + start_angle: *start_angle, + end_angle: *end_angle, + }, + Self::PlanarEllipse { center, major_axis, normal, minor_axis_ratio, start_param, end_param } => Self::PlanarEllipse { + center: [center[0] + delta.x, center[1] + delta.y, center[2] + delta.z], + major_axis: *major_axis, + normal: *normal, + minor_axis_ratio: *minor_axis_ratio, + start_param: *start_param, + end_param: *end_param, + }, + } + } + + pub fn rotated(&self, center: glam::DVec3, axis: glam::DVec3, angle_rad: f64) -> Self { + let quat = glam::DQuat::from_axis_angle(axis.normalize_or(glam::DVec3::Z), angle_rad); + let rot_pt = |p: [f64; 3]| (center + quat * (glam::DVec3::from_array(p) - center)).to_array(); + let rot_vec = |v: [f64; 3]| (quat * glam::DVec3::from_array(v)).to_array(); + match self { + Self::Line { p1, p2 } => { + let p1_d = [p1[0] as f64, p1[1] as f64, p1[2] as f64]; + let p2_d = [p2[0] as f64, p2[1] as f64, p2[2] as f64]; + let r1 = rot_pt(p1_d); + let r2 = rot_pt(p2_d); + Self::Line { + p1: [r1[0] as f32, r1[1] as f32, r1[2] as f32], + p2: [r2[0] as f32, r2[1] as f32, r2[2] as f32], + } + } + Self::Circle { center: c, radius } => { + let c_d = [c[0] as f64, c[1] as f64, c[2] as f64]; + let rc = rot_pt(c_d); + Self::Circle { + center: [rc[0] as f32, rc[1] as f32, rc[2] as f32], + radius: *radius, + } + } + Self::PlanarCircle { center: c, axis_x, axis_y, radius } => Self::PlanarCircle { + center: rot_pt(*c), + axis_x: rot_vec(*axis_x), + axis_y: rot_vec(*axis_y), + radius: *radius, + }, + Self::Arc { center: c, axis_x, axis_y, radius, start_angle, end_angle } => Self::Arc { + center: rot_pt(*c), + axis_x: rot_vec(*axis_x), + axis_y: rot_vec(*axis_y), + radius: *radius, + start_angle: *start_angle, + end_angle: *end_angle, + }, + Self::PlanarEllipse { center: c, major_axis, normal, minor_axis_ratio, start_param, end_param } => Self::PlanarEllipse { + center: rot_pt(*c), + major_axis: rot_vec(*major_axis), + normal: rot_vec(*normal), + minor_axis_ratio: *minor_axis_ratio, + start_param: *start_param, + end_param: *end_param, + }, + } + } + + pub fn scaled(&self, center: glam::DVec3, factor: f64) -> Self { + let scale_pt = |p: [f64; 3]| (center + (glam::DVec3::from_array(p) - center) * factor).to_array(); + match self { + Self::Line { p1, p2 } => { + let p1_d = [p1[0] as f64, p1[1] as f64, p1[2] as f64]; + let p2_d = [p2[0] as f64, p2[1] as f64, p2[2] as f64]; + let s1 = scale_pt(p1_d); + let s2 = scale_pt(p2_d); + Self::Line { + p1: [s1[0] as f32, s1[1] as f32, s1[2] as f32], + p2: [s2[0] as f32, s2[1] as f32, s2[2] as f32], + } + } + Self::Circle { center: c, radius } => { + let c_d = [c[0] as f64, c[1] as f64, c[2] as f64]; + let sc = scale_pt(c_d); + Self::Circle { + center: [sc[0] as f32, sc[1] as f32, sc[2] as f32], + radius: *radius * factor.abs() as f32, + } + } + Self::PlanarCircle { center: c, axis_x, axis_y, radius } => Self::PlanarCircle { + center: scale_pt(*c), + axis_x: *axis_x, + axis_y: *axis_y, + radius: *radius * factor.abs(), + }, + Self::Arc { center: c, axis_x, axis_y, radius, start_angle, end_angle } => Self::Arc { + center: scale_pt(*c), + axis_x: *axis_x, + axis_y: *axis_y, + radius: *radius * factor.abs(), + start_angle: *start_angle, + end_angle: *end_angle, + }, + Self::PlanarEllipse { center: c, major_axis, normal, minor_axis_ratio, start_param, end_param } => Self::PlanarEllipse { + center: scale_pt(*c), + major_axis: (glam::DVec3::from_array(*major_axis) * factor.abs()).to_array(), + normal: *normal, + minor_axis_ratio: *minor_axis_ratio, + start_param: *start_param, + end_param: *end_param, + }, + } + } + + pub fn mirrored(&self, p1: glam::DVec3, normal: glam::DVec3) -> Self { + let refl_pt = |p: [f64; 3]| { + let pt = glam::DVec3::from_array(p); + (pt - 2.0 * normal.dot(pt - p1) * normal).to_array() + }; + let refl_vec = |v: [f64; 3]| { + let vec = glam::DVec3::from_array(v); + (vec - 2.0 * normal.dot(vec) * normal).to_array() + }; + match self { + Self::Line { p1: lp1, p2: lp2 } => { + let p1_d = [lp1[0] as f64, lp1[1] as f64, lp1[2] as f64]; + let p2_d = [lp2[0] as f64, lp2[1] as f64, lp2[2] as f64]; + let m1 = refl_pt(p1_d); + let m2 = refl_pt(p2_d); + Self::Line { + p1: [m1[0] as f32, m1[1] as f32, m1[2] as f32], + p2: [m2[0] as f32, m2[1] as f32, m2[2] as f32], + } + } + Self::Circle { center: c, radius } => { + let c_d = [c[0] as f64, c[1] as f64, c[2] as f64]; + let mc = refl_pt(c_d); + Self::Circle { + center: [mc[0] as f32, mc[1] as f32, mc[2] as f32], + radius: *radius, + } + } + Self::PlanarCircle { center: c, axis_x, axis_y, radius } => { + let ax = refl_vec(*axis_x); + let ay = refl_vec(*axis_y); + Self::PlanarCircle { + center: refl_pt(*c), + axis_x: ax, + axis_y: [-ay[0], -ay[1], -ay[2]], + radius: *radius, + } + } + Self::Arc { center: c, axis_x, axis_y, radius, start_angle, end_angle } => { + let ax = refl_vec(*axis_x); + let ay = refl_vec(*axis_y); + Self::Arc { + center: refl_pt(*c), + axis_x: ax, + axis_y: [-ay[0], -ay[1], -ay[2]], + radius: *radius, + start_angle: -end_angle, + end_angle: -start_angle, + } + } + Self::PlanarEllipse { center: c, major_axis, normal: n, minor_axis_ratio, start_param, end_param } => { + let norm = refl_vec(*n); + Self::PlanarEllipse { + center: refl_pt(*c), + major_axis: refl_vec(*major_axis), + normal: [-norm[0], -norm[1], -norm[2]], + minor_axis_ratio: *minor_axis_ratio, + start_param: *start_param, + end_param: *end_param, + } + } + } + } +} + /// Viewport-relative display transform for a point marker. #[derive(Clone, Copy, Debug, PartialEq)] pub struct PointMarker { @@ -413,6 +608,11 @@ impl WireModel { out.text_verts = map_text_verts(&self.text_verts, |x, y, z| (x + dx, y + dy, z + dz)); } + out.tangent_geoms = self + .tangent_geoms + .iter() + .map(|tg| tg.translated(delta.as_dvec3())) + .collect(); out } @@ -465,6 +665,11 @@ impl WireModel { (mapped.x, mapped.y, mapped.z) }); } + out.tangent_geoms = self + .tangent_geoms + .iter() + .map(|tg| tg.rotated(center.as_dvec3(), axis.as_dvec3(), angle_rad as f64)) + .collect(); out } @@ -499,6 +704,11 @@ impl WireModel { (cx + (x - cx) * f, cy + (y - cy) * f, cz + (z - cz) * f) }); } + out.tangent_geoms = self + .tangent_geoms + .iter() + .map(|tg| tg.scaled(center.as_dvec3(), factor as f64)) + .collect(); out } @@ -639,6 +849,11 @@ pub fn stretched_windows( (mapped.x, mapped.y, mapped.z) }); } + out.tangent_geoms = self + .tangent_geoms + .iter() + .map(|tg| tg.mirrored(p1.as_dvec3(), plane_normal.as_dvec3())) + .collect(); out } @@ -764,3 +979,104 @@ impl Default for WireModel { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn preview_transformations_preserve_and_update_tangent_geoms() { + let mut wire = WireModel::default(); + wire.tangent_geoms.push(TangentGeom::PlanarCircle { + center: [10.0, 20.0, 0.0], + axis_x: [1.0, 0.0, 0.0], + axis_y: [0.0, 1.0, 0.0], + radius: 5.0, + }); + wire.tangent_geoms.push(TangentGeom::Arc { + center: [10.0, 20.0, 0.0], + axis_x: [1.0, 0.0, 0.0], + axis_y: [0.0, 1.0, 0.0], + radius: 5.0, + start_angle: 0.0, + end_angle: std::f64::consts::FRAC_PI_2, + }); + wire.tangent_geoms.push(TangentGeom::PlanarEllipse { + center: [10.0, 20.0, 0.0], + major_axis: [10.0, 0.0, 0.0], + normal: [0.0, 0.0, 1.0], + minor_axis_ratio: 0.5, + start_param: 0.0, + end_param: std::f64::consts::PI, + }); + + // Test translated + let trans = wire.translated(glam::Vec3::new(5.0, -5.0, 1.0)); + assert_eq!(trans.tangent_geoms.len(), 3); + if let TangentGeom::PlanarCircle { center, radius, .. } = trans.tangent_geoms[0] { + assert!((center[0] - 15.0).abs() < 1e-5); + assert!((center[1] - 15.0).abs() < 1e-5); + assert!((center[2] - 1.0).abs() < 1e-5); + assert!((radius - 5.0).abs() < 1e-5); + } else { + panic!("Expected PlanarCircle"); + } + if let TangentGeom::Arc { center, radius, start_angle, end_angle, .. } = trans.tangent_geoms[1] { + assert!((center[0] - 15.0).abs() < 1e-5); + assert!((center[1] - 15.0).abs() < 1e-5); + assert!((center[2] - 1.0).abs() < 1e-5); + assert!((radius - 5.0).abs() < 1e-5); + assert_eq!(start_angle, 0.0); + assert_eq!(end_angle, std::f64::consts::FRAC_PI_2); + } else { + panic!("Expected Arc"); + } + if let TangentGeom::PlanarEllipse { center, major_axis, minor_axis_ratio, .. } = trans.tangent_geoms[2] { + assert!((center[0] - 15.0).abs() < 1e-5); + assert!((center[1] - 15.0).abs() < 1e-5); + assert!((center[2] - 1.0).abs() < 1e-5); + assert_eq!(major_axis, [10.0, 0.0, 0.0]); + assert_eq!(minor_axis_ratio, 0.5); + } else { + panic!("Expected PlanarEllipse"); + } + + // Test scaled + let scaled = wire.scaled(glam::Vec3::new(10.0, 20.0, 0.0), 2.0); + if let TangentGeom::PlanarCircle { radius, .. } = scaled.tangent_geoms[0] { + assert!((radius - 10.0).abs() < 1e-5); + } else { + panic!("Expected PlanarCircle"); + } + if let TangentGeom::Arc { radius, .. } = scaled.tangent_geoms[1] { + assert!((radius - 10.0).abs() < 1e-5); + } else { + panic!("Expected Arc"); + } + if let TangentGeom::PlanarEllipse { major_axis, .. } = scaled.tangent_geoms[2] { + assert!((major_axis[0] - 20.0).abs() < 1e-5); + } else { + panic!("Expected PlanarEllipse"); + } + + // Test rotated + let rot = wire.rotated(glam::Vec3::ZERO, std::f32::consts::FRAC_PI_2); + if let TangentGeom::PlanarCircle { center, axis_x, axis_y, .. } = rot.tangent_geoms[0] { + assert!((center[0] - -20.0).abs() < 1e-4); + assert!((center[1] - 10.0).abs() < 1e-4); + assert!((axis_x[1] - 1.0).abs() < 1e-4); + assert!((axis_y[0] - -1.0).abs() < 1e-4); + } else { + panic!("Expected PlanarCircle"); + } + + // Test mirrored across Y=0 plane (normal = Y) + let mirr = wire.mirrored(glam::Vec3::new(0.0, 0.0, 0.0), glam::Vec3::new(1.0, 0.0, 0.0)); + if let TangentGeom::PlanarCircle { center, .. } = mirr.tangent_geoms[0] { + assert!((center[0] - 10.0).abs() < 1e-4); + assert!((center[1] - -20.0).abs() < 1e-4); + } else { + panic!("Expected PlanarCircle"); + } + } +} diff --git a/src/scene/modify.rs b/src/scene/modify.rs index 8059b1e6f..0b9fdc6a3 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -802,11 +802,39 @@ impl Scene { true } + pub fn append_solid_history( + &mut self, + handle: Handle, + operation: acadrust::objects::SolidHistoryOperation, + ) -> bool { + let previous = self + .document + .solid_history_graph(handle) + .map(|graph| graph.nodes) + .unwrap_or_default(); + self.record_solid_history_before(handle); + let Some(graph) = self.document.append_solid_history(handle, operation) else { + return false; + }; + for node in graph.nodes { + if !previous.contains(&node) { + self.record_undo_object_before(node, None); + } + } + self.sync_solid_reference_point(handle); + true + } + pub(crate) fn sync_solid_reference_point(&mut self, handle: Handle) { let reference = self .document - .solid_history_operation(handle) - .and_then(crate::scene::model::solid_history::reference_point); + .solid_history_operations(handle) + .and_then(|operations| { + operations + .iter() + .rev() + .find_map(crate::scene::model::solid_history::reference_point) + }); let Some(reference) = reference else { return; }; @@ -855,8 +883,31 @@ impl Scene { (EntityType::Surface(_), SolidHistoryOperation::Extrusion(value)) => { cadkernel::acis::rebuild_extrusion_with_mode(value, true).ok() } - (EntityType::Surface(_), SolidHistoryOperation::Loft(_)) - | (EntityType::Solid3D(_), _) => cadkernel::acis::rebuild_body(operation).ok(), + (EntityType::Surface(_), SolidHistoryOperation::Loft(_)) => { + cadkernel::acis::rebuild_body(operation).ok() + } + (EntityType::Solid3D(_), _) => { + let mut operations = self.document.solid_history_operations(handle)?; + let replacement_id = operation.base().map(|base| { + if base.eval.node_id > 0 { + base.eval.node_id + } else { + base.step_id + } + })?; + let target = operations.iter_mut().find(|candidate| { + candidate.base().is_some_and(|base| { + let node_id = if base.eval.node_id > 0 { + base.eval.node_id + } else { + base.step_id + }; + node_id == replacement_id + }) + })?; + *target = operation.clone(); + cadkernel::acis::rebuild_history(&operations).ok() + } _ => None, } } @@ -909,7 +960,7 @@ impl Scene { self.record_solid_history_before(handle); if self .document - .update_solid_history(handle, operation) + .update_solid_history_step(handle, operation) .is_none() { return false; @@ -1122,6 +1173,79 @@ impl Scene { let Some(mut operation) = self.document.solid_history_operation(handle).cloned() else { return false; }; + if grip_id == crate::scene::model::solid_history::GRIP_FILLET_RADIUS { + let acadrust::objects::SolidHistoryOperation::Fillet(value) = &mut operation else { + return false; + }; + let Some(radius) = value.radii.first().copied() else { + return false; + }; + self.restore_solid_models(&[handle]); + let Some(definition) = self + .solid_models + .get(&handle) + .and_then(|body| { + crate::scene::model::solid_history::fillet_radius_grip(body, radius) + }) + else { + return false; + }; + let Some(axis) = definition.axis else { + return false; + }; + let change = match apply { + GripApply::Absolute(world) => (world - definition.world).dot(axis), + GripApply::Translate(delta) => delta.dot(axis), + }; + let radius = radius + change; + if !radius.is_finite() || radius <= 1.0e-6 { + return false; + } + value.radii.clear(); + value.radii.push(radius); + return self.preview_solid_history(handle, operation); + } + if matches!( + grip_id, + crate::scene::model::solid_history::GRIP_CHAMFER_DISTANCE1 + | crate::scene::model::solid_history::GRIP_CHAMFER_DISTANCE2 + ) { + let definitions = { + let acadrust::objects::SolidHistoryOperation::Chamfer(value) = &operation else { + return false; + }; + crate::scene::model::solid_history::chamfer_distance_grips( + &self.document, + handle, + value, + ) + }; + let Some(definition) = definitions.into_iter().find(|grip| grip.id == grip_id) else { + return false; + }; + let Some(axis) = definition.axis else { + return false; + }; + let change = match apply { + GripApply::Absolute(world) => (world - definition.world).dot(axis), + GripApply::Translate(delta) => delta.dot(axis), + }; + let acadrust::objects::SolidHistoryOperation::Chamfer(value) = &mut operation else { + return false; + }; + let distance = if grip_id + == crate::scene::model::solid_history::GRIP_CHAMFER_DISTANCE1 + { + &mut value.base_distance + } else { + &mut value.other_distance + }; + *distance += change; + if !distance.is_finite() || *distance <= 1.0e-6 { + return false; + } + return self.preview_solid_history(handle, operation); + } if !crate::scene::model::solid_history::apply_primitive_grip( &mut operation, grip_id, @@ -1138,7 +1262,12 @@ impl Scene { field: &str, value: &str, ) -> bool { - let Some(mut operation) = self.document.solid_history_operation(handle).cloned() else { + let Some(mut operation) = + crate::scene::model::solid_history::primitive_property_operation( + &self.document, + handle, + ) + else { return false; }; if !crate::scene::model::solid_history::apply_primitive_property( diff --git a/src/scene/pick/hit_test.rs b/src/scene/pick/hit_test.rs index 209a88ba1..f9520c760 100644 --- a/src/scene/pick/hit_test.rs +++ b/src/scene/pick/hit_test.rs @@ -1615,10 +1615,10 @@ pub fn box_hit<'a, W: WireSource + ?Sized>( ); } - // Q: lazy projection — accumulate screen points without allocating per-wire Vec. - wires - .iter() - .filter_map(|wire| { + if crossing { + let mut out = Vec::new(); + let mut seen = HashSet::default(); + for wire in wires.iter() { // Fallback: when wire has no line geometry (e.g. greek text emits // only fill_tris) treat the AABB rectangle as the hit-test shape // so low-LOD text stays selectable. See #19. @@ -1639,7 +1639,7 @@ pub fn box_hit<'a, W: WireSource + ?Sized>( ]; &aabb_pts } else { - return None; + continue; }; // Low residual parallel to `pts` (empty for the AABB fallback, @@ -1650,6 +1650,85 @@ pub fn box_hit<'a, W: WireSource + ?Sized>( &[] }; let mut hit = false; + let mut prev: Option = None; + + for (i, &[px, py, pz]) in pts.iter().enumerate() { + if px.is_nan() { + prev = None; + continue; + } + let world = if !wire.points.is_empty() { + wire_point_world(wire, i, view_rot, eye) + } else { + wp64([px, py, pz], low, i) + }; + let sp = world_to_screen(world, view_rot, eye, bounds); + if inside(sp) { + hit = true; + } + if let Some(p0) = prev { + if !hit { + hit = segments_intersect(p0, sp, box_tl, box_tr) + || segments_intersect(p0, sp, box_tr, box_br) + || segments_intersect(p0, sp, box_br, box_bl) + || segments_intersect(p0, sp, box_bl, box_tl); + } + } + prev = Some(sp); + } + + let mut glyph_crosses = false; + for start in (0..wire.text_verts.len()).step_by(6) { + let Some(screen) = projected_text_quad(wire, start, view_rot, eye, bounds) else { + continue; + }; + if [0usize, 3].into_iter().any(|offset| { + triangle_crosses_box( + [screen[offset], screen[offset + 1], screen[offset + 2]], + box_corners, + ) + }) { + glyph_crosses = true; + break; + } + } + + if (hit || glyph_crosses) && seen.insert(wire.name.as_str()) { + out.push(wire.name.as_str()); + } + } + out + } else { + let mut qualified = Vec::new(); + let mut disqualified = HashSet::default(); + let mut seen = HashSet::default(); + + for wire in wires.iter() { + let aabb_pts: Vec<[f32; 3]>; + let empty_pts: [[f32; 3]; 0] = []; + let pts: &[[f32; 3]] = if !wire.points.is_empty() { + &wire.points + } else if !wire.text_verts.is_empty() { + &empty_pts + } else if wire.aabb != WireModel::UNBOUNDED_AABB { + let [ax, ay, bx, by] = wire.aabb; + aabb_pts = vec![ + [ax, ay, 0.0], + [bx, ay, 0.0], + [bx, by, 0.0], + [ax, by, 0.0], + [ax, ay, 0.0], + ]; + &aabb_pts + } else { + continue; + }; + + let low: &[[f32; 3]] = if !wire.points.is_empty() { + &wire.points_low + } else { + &[] + }; let mut all_inside = true; let mut prev: Option = None; @@ -1664,61 +1743,42 @@ pub fn box_hit<'a, W: WireSource + ?Sized>( wp64([px, py, pz], low, i) }; let sp = world_to_screen(world, view_rot, eye, bounds); - if crossing { - if inside(sp) { - hit = true; - } - if let Some(p0) = prev { - if !hit { - hit = segments_intersect(p0, sp, box_tl, box_tr) - || segments_intersect(p0, sp, box_tr, box_br) - || segments_intersect(p0, sp, box_br, box_bl) - || segments_intersect(p0, sp, box_bl, box_tl); - } - } - } else { - if !inside(sp) { - all_inside = false; - } + if !inside(sp) { + all_inside = false; } prev = Some(sp); } let glyphs_present = !wire.text_verts.is_empty(); - let mut glyph_crosses = false; let mut glyphs_inside = true; for start in (0..wire.text_verts.len()).step_by(6) { let Some(screen) = projected_text_quad(wire, start, view_rot, eye, bounds) else { continue; }; - if crossing { - if [0usize, 3].into_iter().any(|offset| { - triangle_crosses_box( - [screen[offset], screen[offset + 1], screen[offset + 2]], - box_corners, - ) - }) { - glyph_crosses = true; - break; - } - } else if !screen.iter().copied().all(inside) { + if !screen.iter().copied().all(inside) { glyphs_inside = false; break; } } - let result = if crossing { - hit || glyph_crosses - } else { - all_inside && glyphs_inside && (prev.is_some() || glyphs_present) - }; - if result { - Some(wire.name.as_str()) - } else { - None + let has_geom = prev.is_some() || glyphs_present; + if !has_geom { + continue; } - }) - .collect() + + let name = wire.name.as_str(); + if all_inside && glyphs_inside { + if seen.insert(name) { + qualified.push(name); + } + } else { + disqualified.insert(name); + } + } + + qualified.retain(|name| !disqualified.contains(name)); + qualified + } } // ── Polygon / lasso selection ───────────────────────────────────────────── @@ -1743,10 +1803,10 @@ pub fn poly_hit<'a, W: WireSource + ?Sized>( return indexed_polygon_crossing_hits(wires, poly, view_rot, eye, bounds); } - // Q: lazy projection — no Vec allocation per wire. - wires - .iter() - .filter_map(|wire| { + if crossing { + let mut out = Vec::new(); + let mut seen = HashSet::default(); + for wire in wires.iter() { // Same AABB fallback as `box_hit`: when a wire has no line // geometry (e.g. greek-LOD text emits only fill_tris) treat the // AABB rectangle as the hit-test shape so low-LOD text stays @@ -1768,7 +1828,7 @@ pub fn poly_hit<'a, W: WireSource + ?Sized>( ]; &aabb_pts } else { - return None; + continue; }; let low: &[[f32; 3]] = if !wire.points.is_empty() { @@ -1777,6 +1837,88 @@ pub fn poly_hit<'a, W: WireSource + ?Sized>( &[] }; let mut hit = false; + let mut prev: Option = None; + + for (i, &[px, py, pz]) in pts.iter().enumerate() { + if px.is_nan() { + prev = None; + continue; + } + let world = if !wire.points.is_empty() { + wire_point_world(wire, i, view_rot, eye) + } else { + wp64([px, py, pz], low, i) + }; + let sp = world_to_screen(world, view_rot, eye, bounds); + if sp.x < 0.0 || sp.x > bounds.width || sp.y < 0.0 || sp.y > bounds.height { + prev = None; + continue; + } + if point_in_polygon(sp, poly) { + hit = true; + } + if !hit { + if let Some(p0) = prev { + if segment_crosses_polygon(p0, sp, poly) { + hit = true; + } + } + } + prev = Some(sp); + } + + let mut glyph_crosses = false; + for start in (0..wire.text_verts.len()).step_by(6) { + let Some(screen) = projected_text_quad(wire, start, view_rot, eye, bounds) else { + continue; + }; + if [0usize, 3].into_iter().any(|offset| { + triangle_crosses_polygon( + [screen[offset], screen[offset + 1], screen[offset + 2]], + poly, + ) + }) { + glyph_crosses = true; + break; + } + } + + if (hit || glyph_crosses) && seen.insert(wire.name.as_str()) { + out.push(wire.name.as_str()); + } + } + out + } else { + let mut qualified = Vec::new(); + let mut disqualified = HashSet::default(); + let mut seen = HashSet::default(); + + for wire in wires.iter() { + let aabb_pts: Vec<[f32; 3]>; + let empty_pts: [[f32; 3]; 0] = []; + let pts: &[[f32; 3]] = if !wire.points.is_empty() { + &wire.points + } else if !wire.text_verts.is_empty() { + &empty_pts + } else if wire.aabb != WireModel::UNBOUNDED_AABB { + let [ax, ay, bx, by] = wire.aabb; + aabb_pts = vec![ + [ax, ay, 0.0], + [bx, ay, 0.0], + [bx, by, 0.0], + [ax, by, 0.0], + [ax, ay, 0.0], + ]; + &aabb_pts + } else { + continue; + }; + + let low: &[[f32; 3]] = if !wire.points.is_empty() { + &wire.points_low + } else { + &[] + }; let mut all_inside = true; let mut prev: Option = None; @@ -1798,43 +1940,19 @@ pub fn poly_hit<'a, W: WireSource + ?Sized>( prev = None; continue; } - if crossing { - if point_in_polygon(sp, poly) { - hit = true; - } - if !hit { - if let Some(p0) = prev { - if segment_crosses_polygon(p0, sp, poly) { - hit = true; - } - } - } - } else { - if !point_in_polygon(sp, poly) { - all_inside = false; - } + if !point_in_polygon(sp, poly) { + all_inside = false; } prev = Some(sp); } let glyphs_present = !wire.text_verts.is_empty(); - let mut glyph_crosses = false; let mut glyphs_inside = true; for start in (0..wire.text_verts.len()).step_by(6) { let Some(screen) = projected_text_quad(wire, start, view_rot, eye, bounds) else { continue; }; - if crossing { - if [0usize, 3].into_iter().any(|offset| { - triangle_crosses_polygon( - [screen[offset], screen[offset + 1], screen[offset + 2]], - poly, - ) - }) { - glyph_crosses = true; - break; - } - } else if !screen.iter().copied().all(|point| { + if !screen.iter().copied().all(|point| { point.x >= 0.0 && point.x <= bounds.width && point.y >= 0.0 @@ -1846,18 +1964,24 @@ pub fn poly_hit<'a, W: WireSource + ?Sized>( } } - let result = if crossing { - hit || glyph_crosses - } else { - all_inside && glyphs_inside && (prev.is_some() || glyphs_present) - }; - if result { - Some(wire.name.as_str()) - } else { - None + let has_geom = prev.is_some() || glyphs_present; + if !has_geom { + continue; } - }) - .collect() + + let name = wire.name.as_str(); + if all_inside && glyphs_inside { + if seen.insert(name) { + qualified.push(name); + } + } else { + disqualified.insert(name); + } + } + + qualified.retain(|name| !disqualified.contains(name)); + qualified + } } // ── Helpers ─────────────────────────────────────────────────────────────── @@ -2511,4 +2635,80 @@ mod aabb_reject_tests { vec!["479"], ); } + + #[test] + fn window_selection_multi_wire_entity_behavior() { + let bounds = Rectangle { + x: 0.0, + y: 0.0, + width: 200.0, + height: 200.0, + }; + // In identity ortho view: + // world (0.0, 0.0) -> screen (100.0, 100.0) + // world (0.1, 0.1) -> screen (110.0, 90.0) + // world (0.8, 0.8) -> screen (180.0, 20.0) + let wire_arc = wire( + "4F", + vec![[0.0, 0.0, 0.0], [0.1, 0.1, 0.0]], + [0.0, 0.0, 0.1, 0.1], + ); + let wire_lines = wire( + "4F", + vec![[0.1, 0.1, 0.0], [0.8, 0.8, 0.0]], + [0.1, 0.1, 0.8, 0.8], + ); + let wires = [wire_arc, wire_lines]; + + // Box enclosing ONLY wire_arc (screen 95..115, 85..105): + let box_a = Point::new(95.0, 85.0); + let box_b = Point::new(115.0, 105.0); + let poly_small = [ + Point::new(95.0, 85.0), + Point::new(115.0, 85.0), + Point::new(115.0, 105.0), + Point::new(95.0, 105.0), + ]; + + // Crossing mode selects "4F" because wire_arc is inside: + assert_eq!( + box_hit(box_a, box_b, true, &wires, Mat4::IDENTITY, glam::DVec3::ZERO, bounds), + vec!["4F"] + ); + assert_eq!( + poly_hit(&poly_small, true, &wires, Mat4::IDENTITY, glam::DVec3::ZERO, bounds), + vec!["4F"] + ); + + // Window mode MUST NOT select "4F" because wire_lines has a vertex at (0.8, 0.8) -> (180.0, 20.0) outside: + assert_eq!( + box_hit(box_a, box_b, false, &wires, Mat4::IDENTITY, glam::DVec3::ZERO, bounds), + Vec::<&str>::new() + ); + assert_eq!( + poly_hit(&poly_small, false, &wires, Mat4::IDENTITY, glam::DVec3::ZERO, bounds), + Vec::<&str>::new() + ); + + // Big box enclosing BOTH wire_arc and wire_lines (screen 50..190, 10..110): + let big_a = Point::new(50.0, 10.0); + let big_b = Point::new(190.0, 110.0); + let poly_big = [ + Point::new(50.0, 10.0), + Point::new(190.0, 10.0), + Point::new(190.0, 110.0), + Point::new(50.0, 110.0), + ]; + + // Window mode now selects "4F" once (deduplicated): + assert_eq!( + box_hit(big_a, big_b, false, &wires, Mat4::IDENTITY, glam::DVec3::ZERO, bounds), + vec!["4F"] + ); + assert_eq!( + poly_hit(&poly_big, false, &wires, Mat4::IDENTITY, glam::DVec3::ZERO, bounds), + vec!["4F"] + ); + } } + diff --git a/src/scene/pipeline/circle_gpu.rs b/src/scene/pipeline/circle_gpu.rs index 92d7332fb..d62902a64 100644 --- a/src/scene/pipeline/circle_gpu.rs +++ b/src/scene/pipeline/circle_gpu.rs @@ -237,7 +237,7 @@ pub fn extract_circle_instance_from_geom( ), _ => return None, }; - if radius <= 0.0 || !radius.is_finite() || !start_angle.is_finite() || !end_angle.is_finite() { + if radius <= 0.0 || !radius.is_finite() || !start_angle.is_finite() || !end_angle.is_finite() || radius > 1e6 { return None; } diff --git a/src/scene/pipeline/ellipse_gpu.rs b/src/scene/pipeline/ellipse_gpu.rs index 1291be8f5..50bc2e59d 100644 --- a/src/scene/pipeline/ellipse_gpu.rs +++ b/src/scene/pipeline/ellipse_gpu.rs @@ -218,7 +218,7 @@ pub fn extract_ellipse_instance_from_geom( let major_len_sq = major_axis[0] * major_axis[0] + major_axis[1] * major_axis[1] + major_axis[2] * major_axis[2]; let norm_len_sq = normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]; - if major_len_sq <= 1e-12 || !major_len_sq.is_finite() + if major_len_sq <= 1e-12 || !major_len_sq.is_finite() || major_len_sq > 1e12 || norm_len_sq <= 1e-12 || !norm_len_sq.is_finite() || minor_axis_ratio <= 1e-6 || !minor_axis_ratio.is_finite() || !start_param.is_finite() || !end_param.is_finite() diff --git a/src/scene/pipeline/gpu_tests.rs b/src/scene/pipeline/gpu_tests.rs index 90064d106..6ff34c4d0 100644 --- a/src/scene/pipeline/gpu_tests.rs +++ b/src/scene/pipeline/gpu_tests.rs @@ -408,3 +408,303 @@ fn late_device_errors_invalidate_uploaded_content_and_targets() { "the same error must not invalidate twice" ); } + +#[test] +#[ignore = "requires a GPU adapter"] +fn test_selected_circle_arc_ellipse_highlight_overlay() { + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()); + let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default())) + .expect("GPU adapter"); + let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor { + required_limits: adapter.limits(), + ..Default::default() + })) + .expect("GPU device"); + let mut pipeline = Pipeline::new(&device, &queue, wgpu::TextureFormat::Bgra8UnormSrgb); + let depth = rustc_hash::FxHashMap::default(); + + let mut circle_wire = WireModel::default(); + circle_wire.name = "circle_wire".into(); + circle_wire.tangent_geoms.push(crate::scene::model::wire_model::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, + }); + + let handle = acadrust::Handle::new(100); + let mut selected_handles = rustc_hash::FxHashSet::default(); + selected_handles.insert(handle); + let hover_handles = rustc_hash::FxHashSet::default(); + + let mut index = rustc_hash::FxHashMap::default(); + index.insert(handle.value(), vec![0]); + pipeline.wire_handle_index = std::sync::Arc::new(index); + + let wires = vec![circle_wire]; + pipeline.upload_selected_wires( + &device, + &queue, + &wires, + &selected_handles, + &hover_handles, + &[], + &depth, + Some([0.0, 1.0, 1.0, 1.0]), + ); + + assert_eq!(pipeline.gpu_selected_circles.len(), 1); + assert_eq!(pipeline.gpu_selected_circles[0].instance_count, 1); + assert!(pipeline.gpu_selected_wires.is_empty()); + + // Verify classic highlight mode (SELECTIONEFFECT 0 / selected_tint: None) + pipeline.upload_selected_wires( + &device, + &queue, + &wires, + &selected_handles, + &hover_handles, + &[], + &depth, + None, + ); + assert_eq!(pipeline.gpu_selected_circles.len(), 1); + assert_eq!(pipeline.gpu_selected_circles[0].instance_count, 1); + + // Verify hover highlight + let mut circle_hover = rustc_hash::FxHashSet::default(); + circle_hover.insert(handle); + let empty_selected = rustc_hash::FxHashSet::default(); + pipeline.upload_selected_wires( + &device, + &queue, + &wires, + &empty_selected, + &circle_hover, + &[], + &depth, + None, + ); + assert_eq!(pipeline.gpu_selected_circles.len(), 1); + assert_eq!(pipeline.gpu_selected_circles[0].instance_count, 1); + + pipeline.ensure_depth_texture(&device, iced::Size::new(512, 512)); + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("test_target"), + size: wgpu::Extent3d { + width: 512, + height: 512, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Bgra8UnormSrgb, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }); + let target = texture.create_view(&wgpu::TextureViewDescriptor::default()); + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("test_encoder"), + }); + pipeline.render( + &mut encoder, + &target, + iced::Size::new(512, 512), + iced::Rectangle { x: 0, y: 0, width: 512, height: 512 }, + [0.0, 0.0, 0.0, 1.0], + false, + false, + false, + ); + queue.submit(Some(encoder.finish())); + device.poll(wgpu::PollType::wait_indefinitely()).unwrap(); +} + +#[test] +#[ignore = "requires a GPU adapter"] +fn test_selected_ellipse_overlay() { + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()); + let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default())) + .expect("GPU adapter"); + let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor { + required_limits: adapter.limits(), + ..Default::default() + })) + .expect("GPU device"); + let mut pipeline = Pipeline::new(&device, &queue, wgpu::TextureFormat::Bgra8UnormSrgb); + let depth = rustc_hash::FxHashMap::default(); + + let mut ellipse_wire = WireModel::default(); + ellipse_wire.name = "ellipse_wire".into(); + ellipse_wire.tangent_geoms.push(crate::scene::model::wire_model::TangentGeom::PlanarEllipse { + center: [0.0, 0.0, 0.0], + major_axis: [10.0, 0.0, 0.0], + normal: [0.0, 0.0, 1.0], + minor_axis_ratio: 0.5, + start_param: 0.0, + end_param: std::f64::consts::TAU, + }); + + let handle = acadrust::Handle::new(200); + let mut selected_handles = rustc_hash::FxHashSet::default(); + selected_handles.insert(handle); + let hover_handles = rustc_hash::FxHashSet::default(); + + let mut index = rustc_hash::FxHashMap::default(); + index.insert(handle.value(), vec![0]); + pipeline.wire_handle_index = std::sync::Arc::new(index); + + let wires = vec![ellipse_wire]; + // Standard tint + pipeline.upload_selected_wires( + &device, + &queue, + &wires, + &selected_handles, + &hover_handles, + &[], + &depth, + Some([0.0, 1.0, 1.0, 1.0]), + ); + + assert_eq!(pipeline.gpu_selected_ellipses.len(), 1); + assert_eq!(pipeline.gpu_selected_ellipses[0].instance_count, 1); + assert!(pipeline.gpu_selected_wires.is_empty()); + + // Classic highlight mode (SELECTIONEFFECT 0 / selected_tint: None) + pipeline.upload_selected_wires( + &device, + &queue, + &wires, + &selected_handles, + &hover_handles, + &[], + &depth, + None, + ); + assert_eq!(pipeline.gpu_selected_ellipses.len(), 1); + assert_eq!(pipeline.gpu_selected_ellipses[0].instance_count, 1); + + // Hover highlight + let mut ellipse_hover = rustc_hash::FxHashSet::default(); + ellipse_hover.insert(handle); + let empty_selected = rustc_hash::FxHashSet::default(); + pipeline.upload_selected_wires( + &device, + &queue, + &wires, + &empty_selected, + &ellipse_hover, + &[], + &depth, + None, + ); + assert_eq!(pipeline.gpu_selected_ellipses.len(), 1); + assert_eq!(pipeline.gpu_selected_ellipses[0].instance_count, 1); + + pipeline.ensure_depth_texture(&device, iced::Size::new(512, 512)); + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("test_target_ellipse"), + size: wgpu::Extent3d { + width: 512, + height: 512, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Bgra8UnormSrgb, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }); + let target = texture.create_view(&wgpu::TextureViewDescriptor::default()); + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("test_encoder_ellipse"), + }); + pipeline.render( + &mut encoder, + &target, + iced::Size::new(512, 512), + iced::Rectangle { x: 0, y: 0, width: 512, height: 512 }, + [0.0, 0.0, 0.0, 1.0], + false, + false, + false, + ); + queue.submit(Some(encoder.finish())); + device.poll(wgpu::PollType::wait_indefinitely()).unwrap(); +} + +#[test] +#[ignore = "requires a GPU adapter"] +fn test_pline_arc_switch_preview_and_render() { + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()); + let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions::default())) + .expect("GPU adapter"); + let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor { + required_limits: adapter.limits(), + ..Default::default() + })) + .expect("GPU device"); + let mut pipeline = Pipeline::new(&device, &queue, wgpu::TextureFormat::Bgra8UnormSrgb); + let depth = rustc_hash::FxHashMap::default(); + use crate::command::CadCommand; + + let mut cmd = crate::modules::draw::draw::polyline::PlineCommand::new(); + cmd.on_point(glam::DVec3::new(0.0, 0.0, 0.0)); + let _res = cmd.on_point(glam::DVec3::new(10.0, 0.0, 0.0)); + cmd.set_live_handle(acadrust::Handle::new(1)); + cmd.on_text_input("A"); + + let test_points = [ + glam::DVec3::new(10.0, 0.0, 0.0), + glam::DVec3::new(10.0000001, 0.0, 0.0), + glam::DVec3::new(10.0, 0.0000001, 0.0), + glam::DVec3::new(10.0, -0.0000001, 0.0), + glam::DVec3::new(9.9999999, 0.0, 0.0), + glam::DVec3::new(15.0, 5.0, 0.0), + glam::DVec3::new(10.0, 10.0, 0.0), + glam::DVec3::new(5.0, 5.0, 0.0), + glam::DVec3::new(0.0, 0.0, 0.0), + ]; + + pipeline.ensure_depth_texture(&device, iced::Size::new(512, 512)); + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("test_target"), + size: wgpu::Extent3d { + width: 512, + height: 512, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Bgra8UnormSrgb, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }); + let target = texture.create_view(&wgpu::TextureViewDescriptor::default()); + + for pt in test_points { + let wire = cmd.on_mouse_move(pt); + if let Some(w) = wire { + pipeline.upload_preview_wires(&device, &queue, &[w], &depth); + let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("test_encoder"), + }); + pipeline.render( + &mut encoder, + &target, + iced::Size::new(512, 512), + iced::Rectangle { x: 0, y: 0, width: 512, height: 512 }, + [0.0, 0.0, 0.0, 1.0], + false, + false, + false, + ); + queue.submit(Some(encoder.finish())); + device.poll(wgpu::PollType::wait_indefinitely()).unwrap(); + } + } +} diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index 9e27c2a00..880464b12 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -251,6 +251,11 @@ pub struct Pipeline { pub(crate) wire_arena_fallback_handles: rustc_hash::FxHashSet, /// The Model content id both arenas currently mirror (`u64::MAX` = none). pub(crate) wire_arena_id: u64, + /// Handles that contributed to this slot's retained analytical uploads. + pub(crate) partition_contributors: rustc_hash::FxHashSet, + /// The draw-depth generation those uploads baked. A full depth rebuild + /// reassigns every label, so they stop being reusable when it moves. + pub(crate) partition_depth_generation: u64, /// Last content/camera/viewport tuple used to derive visible instance /// ranges from the resident arena. pub(crate) wire_cull_key: (u64, u64, u32, u32), @@ -273,6 +278,8 @@ pub struct Pipeline { /// frame they are present (small), drawn on top of the base wire pass — so /// a live drag never re-uploads the resident base buffer. gpu_preview_wires: Vec, + gpu_preview_circles: Vec, + gpu_preview_ellipses: Vec, /// Wipeout masks — solid fills rendered after wires in a separate pass via /// the legacy per-primitive `WipeoutGpu` renderer. gpu_wipeouts: Vec, @@ -2336,6 +2343,8 @@ impl Pipeline { wire_arena_fallback_kind: None, wire_arena_fallback_handles: rustc_hash::FxHashSet::default(), wire_arena_id: u64::MAX, + partition_contributors: rustc_hash::FxHashSet::default(), + partition_depth_generation: u64::MAX, wire_cull_key: (u64::MAX, u64::MAX, 0, 0), hatch_lod_key: (usize::MAX, u64::MAX, 0, 0, false), wipeout_lod_key: (usize::MAX, u64::MAX, 0, 0, false), @@ -2346,6 +2355,8 @@ impl Pipeline { gpu_selected_ellipses: vec![], gpu_selected_block_wires: vec![], gpu_preview_wires: vec![], + gpu_preview_circles: vec![], + gpu_preview_ellipses: vec![], gpu_wipeouts: vec![], wipeout_skip_flags: vec![], gpu_images: vec![], @@ -2630,6 +2641,9 @@ impl Pipeline { slots.sort_unstable(); for &i in &slots { if let Some(w) = wires.get(i as usize) { + if !w.display_visible { + continue; + } selected_wires.push(w); } } @@ -2641,12 +2655,18 @@ impl Pipeline { slots.sort_unstable(); for &i in &slots { if let Some(w) = wires.get(i as usize) { + if !w.display_visible { + continue; + } hover_wires.push(w); } } } } for wire in annotation_context_wires { + if !wire.display_visible { + continue; + } if wire.selected { selected_wires.push(wire); } else { @@ -2654,13 +2674,12 @@ impl Pipeline { } } let mut selected_circles: Vec = Vec::new(); + let circle_sel_tint = selected_tint.unwrap_or(WireModel::SELECTED); for &wire in &selected_wires { let depth = wire_gpu::wire_draw_depth(wire, depth_map); if let Some(insts) = circle_gpu::extract_circle_instances(wire, depth) { for mut inst in insts { - if let Some(tint) = selected_tint { - inst.color = tint; - } + inst.color = circle_sel_tint; selected_circles.push(inst); } } @@ -2686,13 +2705,12 @@ impl Pipeline { }; let mut selected_ellipses: Vec = Vec::new(); + let ellipse_sel_tint = selected_tint.unwrap_or(WireModel::SELECTED); for &wire in &selected_wires { let depth = wire_gpu::wire_draw_depth(wire, depth_map); if let Some(insts) = ellipse_gpu::extract_ellipse_instances(wire, depth) { for mut inst in insts { - if let Some(tint) = selected_tint { - inst.color = tint; - } + inst.color = ellipse_sel_tint; selected_ellipses.push(inst); } } @@ -2940,11 +2958,47 @@ impl Pipeline { wires: &[WireModel], depth_map: &rustc_hash::FxHashMap, ) { - self.gpu_preview_wires = if wires.is_empty() { - vec![] + if wires.is_empty() { + self.gpu_preview_wires.clear(); + self.gpu_preview_circles.clear(); + self.gpu_preview_ellipses.clear(); } else { - WireGpu::from_run(device, queue, wires, depth_map, false, self.wire_const_bgl.as_ref()) - }; + let partitioned = wire_arena::partition_wires(wires, depth_map); + let mut non_analytical = partitioned.regular; + non_analytical.extend(partitioned.mesh); + self.gpu_preview_wires = if non_analytical.is_empty() { + vec![] + } else { + WireGpu::from_run_refs( + device, + queue, + &non_analytical, + depth_map, + false, + self.wire_const_bgl.as_ref(), + ) + }; + self.gpu_preview_circles = if partitioned.circle_instances.is_empty() { + vec![] + } else { + vec![CircleGpu::from_instances( + device, + queue, + "preview.circles", + &partitioned.circle_instances, + )] + }; + self.gpu_preview_ellipses = if partitioned.ellipse_instances.is_empty() { + vec![] + } else { + vec![EllipseGpu::from_instances( + device, + queue, + "preview.ellipses", + &partitioned.ellipse_instances, + )] + }; + } } /// Upload the live grip-drag / command-preview SDF glyph quads. Re-uploaded @@ -4611,6 +4665,24 @@ impl Pipeline { } } } + if self.gpu_preview_circles.iter().any(|cg| cg.instance_count > 0) { + pass.set_pipeline(&self.circle_xray_pipeline); + for cg in &self.gpu_preview_circles { + if cg.instance_count > 0 { + pass.set_vertex_buffer(0, cg.instance_buffer.slice(..)); + pass.draw(0..6, 0..cg.instance_count); + } + } + } + if self.gpu_preview_ellipses.iter().any(|eg| eg.instance_count > 0) { + pass.set_pipeline(&self.ellipse_xray_pipeline); + for eg in &self.gpu_preview_ellipses { + if eg.instance_count > 0 { + pass.set_vertex_buffer(0, eg.instance_buffer.slice(..)); + pass.draw(0..6, 0..eg.instance_count); + } + } + } } // ── Pass 5c: SDF text quads (drawn over wires) ──────────────────── @@ -4721,6 +4793,8 @@ impl Pipeline { || !self.block_text_highlight_gpu.is_empty(); if !self.gpu_selected_wires.is_empty() || !self.gpu_selected_block_wires.is_empty() + || !self.gpu_selected_circles.is_empty() + || !self.gpu_selected_ellipses.is_empty() || have_text_highlight { let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { @@ -4787,6 +4861,7 @@ impl Pipeline { } else { &self.circle_xray_pipeline }); + pass.set_bind_group(0, &self.uniform_bind_group, &[]); for cg in &self.gpu_selected_circles { if cg.instance_count > 0 { pass.set_vertex_buffer(0, cg.instance_buffer.slice(..)); @@ -4800,6 +4875,7 @@ impl Pipeline { } else { &self.ellipse_xray_pipeline }); + pass.set_bind_group(0, &self.uniform_bind_group, &[]); for eg in &self.gpu_selected_ellipses { if eg.instance_count > 0 { pass.set_vertex_buffer(0, eg.instance_buffer.slice(..)); @@ -4892,12 +4968,7 @@ impl Pipeline { } } - /// Forget every cache key that describes this slot's uploaded content. - /// - /// Shared by slot reuse and by [`Self::release_heavy_resources`] so the two - /// cannot disagree about what "this slot holds nothing you can trust" - /// means. Keys only — the buffers themselves are dropped by the caller that - /// wants the memory back. + /// Reset every cache key that describes this slot's uploaded content. pub(crate) fn forget_cached_keys(&mut self) { self.cached_epoch = (u64::MAX, u64::MAX, u64::MAX); self.cached_wire_id = u64::MAX; @@ -4920,6 +4991,9 @@ impl Pipeline { self.silhouette_key = (usize::MAX, u64::MAX, [u32::MAX; 3], false); self.silhouette_source_key = (usize::MAX, usize::MAX, u64::MAX); self.render_sig = u64::MAX; + // The retained-upload record is part of the slot's cached state. + self.partition_contributors.clear(); + self.partition_depth_generation = u64::MAX; } /// Catch errors delivered after the previous upload/submit completed. @@ -4933,6 +5007,8 @@ impl Pipeline { self.wire_arena = None; self.wire_arena_mesh = None; self.wire_arena_id = u64::MAX; + self.partition_contributors.clear(); + self.partition_depth_generation = u64::MAX; self.alloc_size = Size::new(0, 0); self.shadow_full = None; self.background_source_id = usize::MAX; @@ -4964,12 +5040,16 @@ impl Pipeline { self.wire_arena_fallback_kind = None; self.wire_arena_fallback_handles.clear(); self.wire_arena_id = u64::MAX; + self.partition_contributors.clear(); + self.partition_depth_generation = u64::MAX; self.gpu_selected_wires.clear(); self.gpu_selected_block_wires.clear(); self.gpu_selected_circles.clear(); self.gpu_selected_ellipses.clear(); self.gpu_preview_wires.clear(); + self.gpu_preview_circles.clear(); + self.gpu_preview_ellipses.clear(); self.hatch_gpu.clear(); self.gpu_wipeouts.clear(); self.wipeout_skip_flags.clear(); diff --git a/src/scene/pipeline/wire_arena.rs b/src/scene/pipeline/wire_arena.rs index d4e927da4..265ef257d 100644 --- a/src/scene/pipeline/wire_arena.rs +++ b/src/scene/pipeline/wire_arena.rs @@ -174,6 +174,16 @@ pub struct PartitionedWires<'a> { pub instanced: Vec<&'a WireModel>, pub circle_instances: Vec, pub ellipse_instances: Vec, + /// Handles that contributed to the retained analytical uploads. + pub contributors: rustc_hash::FxHashSet, +} + +/// Whether this wire contributes to an upload retained across arena patches. +pub fn feeds_analytical_uploads(wire: &WireModel) -> bool { + matches!( + classify_wire(wire), + WireKind::Block | WireKind::Circle | WireKind::Ellipse + ) } /// Single-pass classification and extraction of all viewport wire categories. @@ -190,26 +200,36 @@ pub fn partition_wires<'a>( let mut instanced = Vec::new(); let mut circle_instances = Vec::new(); let mut ellipse_instances = Vec::new(); + let mut contributors: rustc_hash::FxHashSet = rustc_hash::FxHashSet::default(); + let note = |wire: &WireModel, set: &mut rustc_hash::FxHashSet| { + if let Some(handle) = handle_of(wire) { + set.insert(handle); + } + }; for wire in wires { if !wire.display_visible { continue; } if wire.render_instance.is_some() { + note(wire, &mut contributors); instanced.push(wire); continue; } - let depth = super::wire_gpu::wire_draw_depth(wire, depth_map); + // Resolve draw depth only for wires eligible for analytical extraction. if !wire.tangent_geoms.is_empty() && wire.fill_tris.is_empty() && wire.pick_tris.is_empty() && wire.text_verts.is_empty() { + let depth = super::wire_gpu::wire_draw_depth(wire, depth_map); if let Some(insts) = super::circle_gpu::extract_circle_instances(wire, depth) { + note(wire, &mut contributors); circle_instances.extend(insts); continue; } if let Some(insts) = super::ellipse_gpu::extract_ellipse_instances(wire, depth) { + note(wire, &mut contributors); ellipse_instances.extend(insts); continue; } @@ -229,6 +249,7 @@ pub fn partition_wires<'a>( instanced, circle_instances, ellipse_instances, + contributors, } } @@ -1644,4 +1665,48 @@ mod tests { assert!(blank_const().pattern_length < 0.0); assert!(blank_packed_instance().pattern_length < 0.0); } + + #[test] + fn partition_wires_partitions_preview_analytical_curves() { + use crate::scene::model::wire_model::TangentGeom; + let mut circle_preview = WireModel::default(); + circle_preview.tangent_geoms.push(TangentGeom::PlanarCircle { + center: [1.0, 2.0, 3.0], + axis_x: [1.0, 0.0, 0.0], + axis_y: [0.0, 1.0, 0.0], + radius: 10.0, + }); + + let mut arc_preview = WireModel::default(); + arc_preview.tangent_geoms.push(TangentGeom::Arc { + center: [4.0, 5.0, 6.0], + axis_x: [1.0, 0.0, 0.0], + axis_y: [0.0, 1.0, 0.0], + radius: 8.0, + start_angle: 0.1, + end_angle: 2.0, + }); + + let mut ellipse_preview = WireModel::default(); + ellipse_preview.tangent_geoms.push(TangentGeom::PlanarEllipse { + center: [7.0, 8.0, 9.0], + major_axis: [5.0, 0.0, 0.0], + normal: [0.0, 0.0, 1.0], + minor_axis_ratio: 0.6, + start_param: 0.0, + end_param: std::f64::consts::TAU, + }); + + let mut regular_line = WireModel::default(); + regular_line.points.push([0.0, 0.0, 0.0]); + regular_line.points.push([1.0, 1.0, 1.0]); + + let wires = vec![circle_preview, arc_preview, ellipse_preview, regular_line]; + let depth_map = rustc_hash::FxHashMap::default(); + let partitioned = partition_wires(&wires, &depth_map); + + assert_eq!(partitioned.circle_instances.len(), 2); + assert_eq!(partitioned.ellipse_instances.len(), 1); + assert_eq!(partitioned.regular.len(), 1); + } } diff --git a/src/scene/selection.rs b/src/scene/selection.rs index 590635f04..d77e6d88a 100644 --- a/src/scene/selection.rs +++ b/src/scene/selection.rs @@ -360,19 +360,62 @@ impl Scene { self.selected.len() } - /// Sorted entity types in the current layout, cached until geometry or layout changes. + /// Entity type names in the current layout for the selection-filter menu. + /// Pure additions are folded into the cached set; other changes rebuild it. pub fn entity_type_names_in_layout(&self) -> std::sync::Arc> { use crate::entities::traits::entity_type_name; let block = self.current_layout_block_handle(); + let mut cached_epoch = None; { let cache = self.layout_type_names_cache.borrow(); - if let Some((epoch, cached_block, names)) = cache.as_ref() { - if *epoch == self.geometry_epoch && *cached_block == block { - return std::sync::Arc::clone(names); + if let Some((epoch, cached_block, _, names)) = cache.as_ref() { + if *cached_block == block { + if *epoch == self.geometry_epoch { + return std::sync::Arc::clone(names); + } + cached_epoch = Some(*epoch); } } } - let mut names: std::collections::BTreeSet<&str> = + + // Incremental: fold the changes since the cached epoch into the set. + if let Some(since) = cached_epoch { + if let Some(deltas) = self.replay_since(since) { + if deltas + .iter() + .all(|(_, kind)| *kind == ChangeKind::Added) + { + let mut cache = self.layout_type_names_cache.borrow_mut(); + if let Some((epoch, _, present, names)) = cache.as_mut() { + let mut added = false; + for (handle, _) in &deltas { + if let Some(entity) = self.document.get_entity(*handle) { + if entity.common().owner_handle == block { + // `contains` first: the common case is a + // type already present, and that path must + // not allocate. + let name = entity_type_name(entity); + if !present.contains(name) { + present.insert(name.to_string()); + added = true; + } + } + } + } + // Only rebuild the list when the set actually moved; + // otherwise the existing `Arc` is still the answer. + if added { + *names = + std::sync::Arc::new(present.iter().cloned().collect()); + } + *epoch = self.geometry_epoch; + return std::sync::Arc::clone(names); + } + } + } + } + + let mut present: std::collections::BTreeSet = std::collections::BTreeSet::new(); if let Some(record) = self .document @@ -382,15 +425,21 @@ impl Scene { { for &handle in &record.entity_handles { if let Some(entity) = self.document.get_entity(handle) { - names.insert(entity_type_name(entity)); + let name = entity_type_name(entity); + if !present.contains(name) { + present.insert(name.to_string()); + } } } } - let names: std::sync::Arc> = std::sync::Arc::new( - names.into_iter().map(str::to_string).collect(), - ); - *self.layout_type_names_cache.borrow_mut() = - Some((self.geometry_epoch, block, std::sync::Arc::clone(&names))); + let names: std::sync::Arc> = + std::sync::Arc::new(present.iter().cloned().collect()); + *self.layout_type_names_cache.borrow_mut() = Some(( + self.geometry_epoch, + block, + present, + std::sync::Arc::clone(&names), + )); names } @@ -954,6 +1003,60 @@ impl Scene { mod tests { use super::*; + #[test] + fn adding_a_type_already_present_reuses_the_list() { + use acadrust::entities::{Circle, EntityType, Line}; + use acadrust::types::Vector3; + use std::sync::Arc; + + let line = || { + EntityType::Line(Line::from_points( + Vector3::new(0.0, 0.0, 0.0), + Vector3::new(1.0, 0.0, 0.0), + )) + }; + let mut scene = Scene::new(); + scene.add_entity(line()); + let first = scene.entity_type_names_in_layout(); + + scene.add_entity(line()); + let second = scene.entity_type_names_in_layout(); + assert!( + Arc::ptr_eq(&first, &second), + "a second line changes no type name, so the list must be reused", + ); + + scene.add_entity(EntityType::Circle(Circle::new())); + let third = scene.entity_type_names_in_layout(); + assert_eq!(third.as_slice(), ["Circle", "Line"], "a new type must appear"); + + // The incremental answer has to be the answer a full walk gives. + scene.layout_type_names_cache.borrow_mut().take(); + assert_eq!( + scene.entity_type_names_in_layout().as_slice(), + third.as_slice(), + "folding edits in must match rebuilding from scratch", + ); + } + + #[test] + fn changing_an_entity_type_rebuilds_the_type_names() { + use acadrust::entities::{Circle, EntityType, Line}; + use acadrust::types::Vector3; + + let mut scene = Scene::new(); + let handle = scene.add_entity(EntityType::Line(Line::from_points( + Vector3::new(0.0, 0.0, 0.0), + Vector3::new(1.0, 0.0, 0.0), + ))); + assert_eq!(scene.entity_type_names_in_layout().as_slice(), ["Line"]); + + let mut circle = Circle::new(); + circle.common.handle = handle; + assert!(scene.update_entity(EntityType::Circle(circle))); + assert_eq!(scene.entity_type_names_in_layout().as_slice(), ["Circle"]); + } + #[test] fn layout_type_cache_reuses_and_invalidates_on_edits_undo_and_layout() { use acadrust::entities::{Circle, EntityType, Line}; diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index b25fb4810..331a536f5 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -149,6 +149,9 @@ pub struct ViewportData { /// by the wire / face3d pipelines as a clip-z bias. WireModels carry no /// depth field (84 construction sites); the bias is looked up by handle /// at GPU-upload time from this map instead. + /// See `Scene::draw_depth_generation`. Cached uploads that bake a depth are + /// only reusable while this is unchanged. + pub(in crate::scene) draw_depth_generation: u64, pub(in crate::scene) draw_depths: std::sync::Weak>, pub(in crate::scene) hatches: Arc>, @@ -839,31 +842,83 @@ impl shader::Primitive for Primitive { gpus.extend(arena.wire_gpus()); } inner.gpu_wires = std::sync::Arc::new(gpus); - let partitioned = - wire_arena::partition_wires(&vp_wires, &draw_depths); - inner.gpu_block_wires = std::sync::Arc::new( - inner.upload_block_wires( - device, - queue, - &partitioned.instanced, - &draw_depths, - &mut pipeline.block_geometry, - ), - ); - inner.gpu_circles = std::sync::Arc::new( - inner.upload_circles_from_instances( - device, - queue, - &partitioned.circle_instances, - ), - ); - inner.gpu_ellipses = std::sync::Arc::new( - inner.upload_ellipses_from_instances( - device, - queue, - &partitioned.ellipse_instances, - ), - ); + // Retain analytical uploads when a patch changes neither + // their contributors nor their baked draw depths. + let analytical_untouched = _patched + && inner.partition_depth_generation == vp.draw_depth_generation + && patch.is_some_and(|patch| { + patch.changes.iter().all(|(handle, _)| { + !inner.partition_contributors.contains(handle) + && !patch.runs.get(handle).is_some_and(|run| { + run.iter() + .any(wire_arena::feeds_analytical_uploads) + }) + }) + }); + let t_part = _perf.then(iced::time::Instant::now); + let partitioned = (!analytical_untouched) + .then(|| wire_arena::partition_wires(&vp_wires, &draw_depths)); + let part_ms = t_part + .map_or(0.0, |t| t.elapsed().as_secs_f64() * 1000.0); + let t_blk = _perf.then(iced::time::Instant::now); + let mut blk_ms = 0.0f64; + let mut curve_ms = 0.0f64; + // `None` means the three uploads this slot holds are + // still the answer, so they are left alone. + if let Some(partitioned) = &partitioned { + inner.gpu_block_wires = std::sync::Arc::new( + inner.upload_block_wires( + device, + queue, + &partitioned.instanced, + &draw_depths, + &mut pipeline.block_geometry, + ), + ); + blk_ms = t_blk + .map_or(0.0, |t| t.elapsed().as_secs_f64() * 1000.0); + let t_curve = _perf.then(iced::time::Instant::now); + inner.gpu_circles = std::sync::Arc::new( + inner.upload_circles_from_instances( + device, + queue, + &partitioned.circle_instances, + ), + ); + inner.gpu_ellipses = std::sync::Arc::new( + inner.upload_ellipses_from_instances( + device, + queue, + &partitioned.ellipse_instances, + ), + ); + inner + .partition_contributors + .clone_from(&partitioned.contributors); + inner.partition_depth_generation = vp.draw_depth_generation; + curve_ms = t_curve + .map_or(0.0, |t| t.elapsed().as_secs_f64() * 1000.0); + } + if _perf { + // Report counts only when partitioning ran. + match &partitioned { + Some(partitioned) => crate::perf_record!( + "[perf] arena-post partition={part_ms:.1}ms \ +blocks={blk_ms:.1}ms curves={curve_ms:.1}ms skipped=false wires={} instanced={} \ +circles={} ellipses={}", + vp_wires.len(), + partitioned.instanced.len(), + partitioned.circle_instances.len(), + partitioned.ellipse_instances.len(), + ), + None => crate::perf_record!( + "[perf] arena-post skipped=true wires={} \ +retained_contributors={}", + vp_wires.len(), + inner.partition_contributors.len(), + ), + } + } if _patched { wire_arena::patch_handle_index( &mut inner.wire_handle_index, @@ -1216,8 +1271,9 @@ impl shader::Primitive for Primitive { let prepare_ms = nav_prepare_started.elapsed().as_secs_f64() * 1000.0; if let Some(sample) = self.nav_perf { crate::perf::record(format_args!( - "[perf] nav-prepare op={} space={} mode={} input={:.2}ms build={:.2}ms prepare={:.2}ms elapsed={:.2}ms viewports={}", + "[perf] nav-prepare op={} cause={} space={} mode={} input={:.2}ms build={:.2}ms prepare={:.2}ms elapsed={:.2}ms viewports={}", sample.op.label(), + sample.cause, sample.space, sample.mode, sample.input_ms, @@ -4238,6 +4294,7 @@ impl Scene { face3d_wires, text_verts, preview_text_verts, + draw_depth_generation: self.draw_depth_generation(), draw_depths: Arc::downgrade(&draw_depths), hatches, wipeout_hatches, diff --git a/src/ui/color_select.rs b/src/ui/color_select.rs index 054d80812..bd05c1db7 100644 --- a/src/ui/color_select.rs +++ b/src/ui/color_select.rs @@ -5,6 +5,7 @@ use crate::app::Message; use crate::ui::properties::acad_color_display; +use crate::ui::ROW_H; use acadrust::types::Color as AcadColor; use iced::widget::{button, column, container, row, text}; use iced::{Background, Border, Color, Element, Length, Theme}; @@ -223,6 +224,7 @@ fn color_selector_with_indicator<'a>( ) .on_press(on_toggle) .padding([3, 6]) + .height(ROW_H) .width(Length::Fill); if !open { diff --git a/src/ui/ribbon/widgets.rs b/src/ui/ribbon/widgets.rs index 333147351..7a77c6d9d 100644 --- a/src/ui/ribbon/widgets.rs +++ b/src/ui/ribbon/widgets.rs @@ -128,14 +128,13 @@ fn measure_large_width(renderer: &iced::Renderer, label: &str) -> f32 { .0; let max_label_height = line_height * LARGE_LABEL_LINES + 0.5; let fits = |width: f32| { - ribbon_label_bounds( + let bounds = ribbon_label_bounds( renderer, label, width, - advanced_text::Wrapping::WordOrGlyph, - ) - .height - <= max_label_height + advanced_text::Wrapping::Word, + ); + bounds.width <= width + 0.5 && bounds.height <= max_label_height }; if fits(base_inner) { @@ -685,7 +684,7 @@ pub(super) fn render_large_dropdown<'a>( .size(10) .width(Fill) .align_x(iced::Center) - .wrapping(advanced_text::Wrapping::WordOrGlyph), + .wrapping(advanced_text::Wrapping::Word), ] .align_x(iced::Center) .spacing(0) @@ -818,7 +817,7 @@ pub(super) fn render_large<'a>( .size(10) .width(Fill) .align_x(iced::Center) - .wrapping(advanced_text::Wrapping::WordOrGlyph), + .wrapping(advanced_text::Wrapping::Word), ] .align_x(iced::Center) .spacing(0)