feat(model): 3D solid modelling tab — primitives + boolean ops
Rename the Home tab to "Draw" and add a Model tab beside it with two groups: - Model: BOX / CYLINDER / CONE / SPHERE / WEDGE / TORUS, placed with a footprint + height. Each is built as a real ACIS Solid3D (acadrust primitive builders) plus a truck B-rep, tessellated into the shaded mesh pipeline, with edge wires for picking + wireframe. - Design: UNION / SUBTRACT / INTERSECT via truck-shapeops. A session-only Scene.model_solids cache holds each solid's truck B-rep so the boolean tools can combine and chain them. Supersedes the old command-line BOX/SPHERE/CYLINDER (insert::solid3d_cmds keeps EXTRUDE/ REVOLVE/SWEEP/LOFT). Known limitations: booleans need solids created this session; curved ACIS primitives render in-session but not after reload (the SAT tessellator handles only planar caps); geometry assumes world_offset 0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
4acb820e01
commit
7bf9ce31ae
17 changed files with 1095 additions and 397 deletions
337
Cargo.lock
generated
337
Cargo.lock
generated
|
|
@ -2,6 +2,16 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "Inflector"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "OpenCADStudio"
|
||||
version = "0.4.6"
|
||||
|
|
@ -20,6 +30,7 @@ dependencies = [
|
|||
"truck-meshalgo",
|
||||
"truck-modeling",
|
||||
"truck-polymesh",
|
||||
"truck-shapeops",
|
||||
"ureq",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
|
@ -90,6 +101,15 @@ dependencies = [
|
|||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr 2.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aliasable"
|
||||
version = "0.1.3"
|
||||
|
|
@ -533,7 +553,7 @@ dependencies = [
|
|||
"allsorts",
|
||||
"azul-core",
|
||||
"azul-css",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"hyphenation",
|
||||
"lru",
|
||||
"roxmltree 0.21.1",
|
||||
|
|
@ -555,6 +575,12 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8259ebd5ee48a37fd8931116405456fd67efbb5435b4789e45bdbccf5d7dea7e"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
|
|
@ -852,6 +878,19 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
|
|
@ -925,7 +964,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"memchr",
|
||||
"memchr 2.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1010,7 +1049,7 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"memchr 2.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1173,6 +1212,17 @@ dependencies = [
|
|||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive-new"
|
||||
version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.20"
|
||||
|
|
@ -1662,7 +1712,7 @@ dependencies = [
|
|||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"memchr 2.8.0",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
|
@ -2027,6 +2077,30 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iced"
|
||||
version = "0.14.0"
|
||||
|
|
@ -2774,6 +2848,23 @@ dependencies = [
|
|||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lz4_flex"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05304f8e67dfc93d1b4b990137fd1a7a4c6ad44b60a9c486c8c4486f9d2027ae"
|
||||
|
||||
[[package]]
|
||||
name = "lzma-sys"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mac"
|
||||
version = "0.1.1"
|
||||
|
|
@ -2850,6 +2941,15 @@ dependencies = [
|
|||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
|
|
@ -3062,13 +3162,22 @@ version = "0.1.14"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "3.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05aec50c70fd288702bcd93284a8444607f3292dbdf2a30de5ea5dcdbe72287b"
|
||||
dependencies = [
|
||||
"memchr 1.0.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"memchr 2.8.0",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
|
|
@ -3078,7 +3187,7 @@ version = "8.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"memchr 2.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3088,7 +3197,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d"
|
||||
dependencies = [
|
||||
"bytecount",
|
||||
"memchr",
|
||||
"memchr 2.8.0",
|
||||
"nom 8.0.0",
|
||||
]
|
||||
|
||||
|
|
@ -3123,6 +3232,17 @@ version = "0.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.4.2"
|
||||
|
|
@ -3180,7 +3300,7 @@ version = "0.7.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
|
|
@ -3944,7 +4064,7 @@ dependencies = [
|
|||
"azul-core",
|
||||
"azul-css",
|
||||
"azul-layout",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"flate2",
|
||||
"getrandom 0.3.4",
|
||||
"kuchiki",
|
||||
|
|
@ -3960,13 +4080,23 @@ dependencies = [
|
|||
"xmlparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "1.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"toml_edit 0.19.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||
dependencies = [
|
||||
"toml_edit",
|
||||
"toml_edit 0.25.10+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4048,13 +4178,23 @@ version = "2.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b"
|
||||
dependencies = [
|
||||
"memchr 2.8.0",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.39.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"memchr 2.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4223,7 +4363,7 @@ dependencies = [
|
|||
"maybe-rayon",
|
||||
"new_debug_unreachable",
|
||||
"noop_proc_macro",
|
||||
"num-derive",
|
||||
"num-derive 0.4.2",
|
||||
"num-traits",
|
||||
"paste",
|
||||
"profiling",
|
||||
|
|
@ -4339,6 +4479,35 @@ dependencies = [
|
|||
"bitflags 2.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr 2.8.0",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr 2.8.0",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "renderdoc-sys"
|
||||
version = "1.1.0"
|
||||
|
|
@ -4430,7 +4599,7 @@ version = "0.21.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"memchr 2.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4440,7 +4609,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "ba4f18e13793c89413ba7e7c991db5f56527803d4706df6a14291d045cd67db2"
|
||||
dependencies = [
|
||||
"allsorts",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"mmapio",
|
||||
"xmlparser",
|
||||
]
|
||||
|
|
@ -4527,6 +4696,36 @@ dependencies = [
|
|||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruststep"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5df866eb24b48dd4bb8a3cb0dfeb3a25ff2c251ed93aae1cbfc03eb8ee3c7dcc"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"derive-new",
|
||||
"derive_more",
|
||||
"itertools 0.10.5",
|
||||
"nom 7.1.3",
|
||||
"ruststep-derive",
|
||||
"serde",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruststep-derive"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81bfdd035ae42e977d18e3197065392784566ef230eda4241d15799ea2154e84"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"proc-macro-crate 1.3.1",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
|
|
@ -4673,7 +4872,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa 1.0.18",
|
||||
"memchr",
|
||||
"memchr 2.8.0",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
|
|
@ -5343,6 +5542,12 @@ dependencies = [
|
|||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
|
|
@ -5352,6 +5557,17 @@ dependencies = [
|
|||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.19.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime 0.6.11",
|
||||
"winnow 0.5.40",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.25.10+spec-1.1.0"
|
||||
|
|
@ -5359,7 +5575,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "a82418ca169e235e6c399a84e395ab6debeb3bc90edc959bf0f48647c6a32d1b"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime",
|
||||
"toml_datetime 1.1.1+spec-1.1.0",
|
||||
"toml_parser",
|
||||
"winnow 1.0.1",
|
||||
]
|
||||
|
|
@ -5470,6 +5686,7 @@ dependencies = [
|
|||
"truck-geometry",
|
||||
"truck-polymesh",
|
||||
"truck-topology",
|
||||
"vtkio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5505,6 +5722,41 @@ dependencies = [
|
|||
"truck-geotrait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truck-shapeops"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd6e83ea4d36d00229d38ab985a991e32f58c3ed2971083011ac66fb57e26d5c"
|
||||
dependencies = [
|
||||
"derive_more",
|
||||
"itertools 0.13.0",
|
||||
"rustc-hash 2.1.2",
|
||||
"truck-base",
|
||||
"truck-geometry",
|
||||
"truck-geotrait",
|
||||
"truck-meshalgo",
|
||||
"truck-stepio",
|
||||
"truck-topology",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truck-stepio"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d29cf5b0dec4b927e80b5dca26ce3ca69601c77ea5b9c953e30bfc3e123d189"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"derive_more",
|
||||
"ruststep",
|
||||
"serde",
|
||||
"truck-derivers",
|
||||
"truck-geometry",
|
||||
"truck-geotrait",
|
||||
"truck-modeling",
|
||||
"truck-polymesh",
|
||||
"truck-topology",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truck-topology"
|
||||
version = "0.6.0"
|
||||
|
|
@ -5658,7 +5910,7 @@ version = "3.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"rustls",
|
||||
|
|
@ -5674,7 +5926,7 @@ version = "0.6.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
|
|
@ -5686,7 +5938,7 @@ version = "0.45.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"data-url",
|
||||
"flate2",
|
||||
"fontdb",
|
||||
|
|
@ -5747,6 +5999,25 @@ version = "0.9.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "vtkio"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "abbe89e5b97b472d57abeb02755a06d75b28d2df7d1fe3df5baf032281a65c16"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
"flate2",
|
||||
"lz4_flex",
|
||||
"nom 3.2.1",
|
||||
"num-derive 0.3.3",
|
||||
"num-traits",
|
||||
"quick-xml 0.22.0",
|
||||
"serde",
|
||||
"xz2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
|
|
@ -6009,7 +6280,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quick-xml",
|
||||
"quick-xml 0.39.2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
|
|
@ -6649,13 +6920,22 @@ dependencies = [
|
|||
"xkbcommon-dl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
|
||||
dependencies = [
|
||||
"memchr 2.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"memchr 2.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -6664,7 +6944,7 @@ version = "1.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"memchr 2.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -6830,6 +7110,15 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "xz2"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
|
||||
dependencies = [
|
||||
"lzma-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "y4m"
|
||||
version = "0.8.0"
|
||||
|
|
@ -6883,7 +7172,7 @@ version = "5.14.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
|
|
@ -7000,7 +7289,7 @@ version = "5.10.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ image = { version = "0.25", default-features = false, features = ["png", "jpeg",
|
|||
rayon = "1"
|
||||
ureq = { version = "3", default-features = false, features = ["rustls"] }
|
||||
inventory = "0.3"
|
||||
truck-shapeops = "0.4"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
|
||||
|
|
|
|||
2
build.rs
2
build.rs
|
|
@ -22,7 +22,7 @@ use std::fs;
|
|||
use std::path::Path;
|
||||
|
||||
// Display order for known modules; unknown modules follow alphabetically.
|
||||
const PRIORITY: &[&str] = &["home", "insert", "annotate", "view", "manage"];
|
||||
const PRIORITY: &[&str] = &["home", "model", "insert", "annotate", "view", "manage"];
|
||||
|
||||
fn main() {
|
||||
let mods_dir = Path::new("src/modules");
|
||||
|
|
|
|||
|
|
@ -125,6 +125,16 @@ impl OpenCADStudio {
|
|||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
}
|
||||
CmdResult::CommitSolid { entity, solid } => {
|
||||
let label = self.history_label_from_active_cmd(i, "SOLID");
|
||||
self.push_undo_snapshot(i, label);
|
||||
self.add_model_solid(entity, *solid);
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.restore_pre_cmd_tangent();
|
||||
}
|
||||
CmdResult::CommitAndEditText(entity) => {
|
||||
let label = self.history_label_from_active_cmd(i, "ENTITY");
|
||||
self.push_undo_snapshot(i, label);
|
||||
|
|
|
|||
|
|
@ -1190,6 +1190,22 @@ impl OpenCADStudio {
|
|||
}
|
||||
}
|
||||
|
||||
// ── Model commands (3D primitives) ─────────────────────────────
|
||||
"BOX" | "WEDGE" | "CYLINDER" | "CONE" | "SPHERE" | "TORUS" => {
|
||||
use crate::modules::model::primitive_cmd::PrimitiveCommand;
|
||||
let new_cmd = PrimitiveCommand::new(cmd);
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
|
||||
// ── Design commands (solid booleans) ───────────────────────────
|
||||
"UNION" | "SUBTRACT" | "INTERSECT" => {
|
||||
use crate::modules::model::boolean_cmd::BoolOp;
|
||||
if let Some(op) = BoolOp::from_id(cmd) {
|
||||
return self.solid_boolean(op);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Annotate commands ──────────────────────────────────────────
|
||||
"TEXT" | "T" | "DT" => {
|
||||
use crate::modules::annotate::text::TextCommand;
|
||||
|
|
@ -5038,32 +5054,8 @@ impl OpenCADStudio {
|
|||
}
|
||||
}
|
||||
|
||||
// ── 3D Primitive — BOX ────────────────────────────────────────
|
||||
"BOX" => {
|
||||
use crate::modules::insert::solid3d_cmds::BoxCommand;
|
||||
let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
|
||||
let cmd = BoxCommand::new(color);
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
}
|
||||
|
||||
// ── 3D Primitive — SPHERE ─────────────────────────────────────
|
||||
"SPHERE" => {
|
||||
use crate::modules::insert::solid3d_cmds::SphereCommand;
|
||||
let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
|
||||
let cmd = SphereCommand::new(color);
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
}
|
||||
|
||||
// ── 3D Primitive — CYLINDER ───────────────────────────────────
|
||||
"CYLINDER" => {
|
||||
use crate::modules::insert::solid3d_cmds::CylinderCommand;
|
||||
let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
|
||||
let cmd = CylinderCommand::new(color);
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
}
|
||||
// BOX / SPHERE / CYLINDER / CONE / WEDGE / TORUS are handled by the
|
||||
// Model-tab primitive command above (with truck boolean caching).
|
||||
|
||||
// ── EXTRUDE ────────────────────────────────────────────────────
|
||||
"EXTRUDE" | "EXT" => {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ mod helpers;
|
|||
mod history;
|
||||
mod layers;
|
||||
mod mtext_editor;
|
||||
mod model_ops;
|
||||
mod properties;
|
||||
mod text_inline;
|
||||
mod update;
|
||||
|
|
|
|||
75
src/app/model_ops.rs
Normal file
75
src/app/model_ops.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// 3D solid modelling support on the App: committing Model-tab primitives,
|
||||
// and the Design-group boolean operations (truck-shapeops) over the scene's
|
||||
// session-cached truck B-reps.
|
||||
|
||||
use acadrust::entities::Solid3D;
|
||||
use acadrust::{EntityType, Handle};
|
||||
use iced::Task;
|
||||
use truck_modeling::Solid;
|
||||
|
||||
use super::Message;
|
||||
use crate::modules::model::boolean_cmd::BoolOp;
|
||||
use crate::scene::model_solid::{self, Bool};
|
||||
|
||||
impl super::OpenCADStudio {
|
||||
/// Commit a Model-tab solid: add its acadrust entity to the document, then
|
||||
/// register the truck B-rep (caches it for booleans + tessellates it into
|
||||
/// the shaded mesh pipeline). Returns the new entity handle.
|
||||
pub(super) fn add_model_solid(&mut self, entity: EntityType, solid: Solid) -> Handle {
|
||||
let i = self.active_tab;
|
||||
let Some(handle) = self.commit_entity_handle(entity) else {
|
||||
return Handle::NULL;
|
||||
};
|
||||
self.tabs[i].scene.register_model_solid(handle, solid);
|
||||
handle
|
||||
}
|
||||
|
||||
/// Run a boolean (`union` / `subtract` / `intersect`) on exactly two
|
||||
/// selected solids whose truck B-reps are in the session cache.
|
||||
pub(super) fn solid_boolean(&mut self, op: BoolOp) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
// Selected entities that have a cached truck B-rep.
|
||||
let handles: Vec<Handle> = self.tabs[i]
|
||||
.scene
|
||||
.selected
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|h| self.tabs[i].scene.model_solids.contains_key(h))
|
||||
.collect();
|
||||
if handles.len() != 2 {
|
||||
self.command_line.push_error(
|
||||
"Boolean: select exactly two solids created this session.",
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
let a = self.tabs[i].scene.model_solids[&handles[0]].clone();
|
||||
let b = self.tabs[i].scene.model_solids[&handles[1]].clone();
|
||||
let kind = match op {
|
||||
BoolOp::Union => Bool::Union,
|
||||
BoolOp::Subtract => Bool::Subtract,
|
||||
BoolOp::Intersect => Bool::Intersect,
|
||||
};
|
||||
let Some(result) = model_solid::boolean(kind, &a, &b) else {
|
||||
self.command_line
|
||||
.push_error("Boolean failed — the solids may not overlap.");
|
||||
return Task::none();
|
||||
};
|
||||
|
||||
self.push_undo_snapshot(i, "BOOLEAN");
|
||||
// Remove the two operands (entity + mesh + cached B-rep).
|
||||
self.tabs[i].scene.erase_entities(&handles);
|
||||
// The result is freshly combined geometry with no ACIS parametrisation,
|
||||
// so it lives as a Solid3D whose render/boolean data is the injected
|
||||
// truck mesh + cached B-rep; its edge wires make it pickable.
|
||||
let mut s3d = Solid3D::new();
|
||||
s3d.wires = model_solid::edge_wires(&result);
|
||||
let handle = self.add_model_solid(EntityType::Solid3D(s3d), result);
|
||||
self.tabs[i].scene.deselect_all();
|
||||
if !handle.is_null() {
|
||||
self.tabs[i].scene.select_entity(handle, false);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
|
|
@ -51,6 +51,13 @@ pub enum CmdResult {
|
|||
CommitEntity(EntityType),
|
||||
/// Commit an acadrust entity to the document and end the command.
|
||||
CommitAndExit(EntityType),
|
||||
/// Commit a Model-tab 3D solid: the acadrust entity (for selection /
|
||||
/// persistence) plus its truck B-rep (cached for boolean ops + shaded
|
||||
/// rendering). Ends the command.
|
||||
CommitSolid {
|
||||
entity: EntityType,
|
||||
solid: Box<truck_modeling::Solid>,
|
||||
},
|
||||
/// Commit an acadrust entity, end the command, and open the in-place text
|
||||
/// editor on it (used by MLEADER to type the annotation after placement).
|
||||
CommitAndEditText(EntityType),
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ impl CadModule for HomeModule {
|
|||
"home"
|
||||
}
|
||||
fn title(&self) -> &'static str {
|
||||
"Home"
|
||||
"Draw"
|
||||
}
|
||||
|
||||
fn ribbon_groups(&self) -> Vec<RibbonGroup> {
|
||||
|
|
|
|||
|
|
@ -1,352 +1,20 @@
|
|||
// 3D solid primitive commands — BOX, SPHERE, CYLINDER
|
||||
// and 2D→3D extrusion commands — EXTRUDE, REVOLVE.
|
||||
// 2D→3D modelling commands — EXTRUDE, REVOLVE, SWEEP, LOFT.
|
||||
//
|
||||
// All create a minimal Solid3D entity (empty ACIS, just as a document
|
||||
// placeholder to hold the handle) and a MeshModel built with truck.
|
||||
// The mesh is manually inserted into scene.meshes so it renders immediately.
|
||||
//
|
||||
// Round-trip limitation: saving and reopening the file will not restore
|
||||
// the mesh because the ACIS data is empty. Full ACIS generation requires
|
||||
// a separate step (out of scope here).
|
||||
//
|
||||
// Coordinate convention in OpenCADStudio: the viewport is Y-up (OpenGL style),
|
||||
// so screen X→DXF X, screen Z→DXF Y, screen Y→DXF Z (height).
|
||||
// truck works in standard math coordinates; we map accordingly.
|
||||
// Each picks profile/path entities and emits a `CmdResult` whose handler
|
||||
// builds a truck solid and inserts its MeshModel into scene.meshes under a
|
||||
// minimal placeholder Solid3D entity (empty ACIS — saving will not restore
|
||||
// the mesh). The standalone primitives (BOX/CYLINDER/CONE/SPHERE/WEDGE/TORUS)
|
||||
// live in the Model tab (`modules::model::primitive_cmd`).
|
||||
|
||||
use acadrust::{entities::Solid3D, EntityType};
|
||||
use glam::Vec3;
|
||||
use truck_modeling::builder;
|
||||
use truck_modeling::{Point3, Rad, Vector3 as TruckVec3};
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::scene::mesh_model::MeshModel;
|
||||
use crate::scene::truck_tess;
|
||||
|
||||
// ── Tessellation helper ────────────────────────────────────────────────────
|
||||
|
||||
fn solid_to_mesh(solid: &truck_modeling::Solid, color: [f32; 4], name: &str) -> Option<MeshModel> {
|
||||
match truck_tess::tessellate_solid(solid, [0.0; 3]) {
|
||||
truck_tess::TruckTessResult::Mesh {
|
||||
verts,
|
||||
normals,
|
||||
indices,
|
||||
} => Some(MeshModel {
|
||||
name: name.to_string(),
|
||||
verts,
|
||||
normals,
|
||||
indices,
|
||||
color,
|
||||
selected: false,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn shell_to_mesh(shell: &truck_modeling::Shell, color: [f32; 4], name: &str) -> Option<MeshModel> {
|
||||
match truck_tess::tessellate_shell(shell, [0.0; 3]) {
|
||||
truck_tess::TruckTessResult::Mesh {
|
||||
verts,
|
||||
normals,
|
||||
indices,
|
||||
} => Some(MeshModel {
|
||||
name: name.to_string(),
|
||||
verts,
|
||||
normals,
|
||||
indices,
|
||||
color,
|
||||
selected: false,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// ── BOX command ────────────────────────────────────────────────────────────
|
||||
|
||||
pub struct BoxCommand {
|
||||
step: BoxStep,
|
||||
p1: Vec3,
|
||||
p2: Vec3,
|
||||
color: [f32; 4],
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum BoxStep {
|
||||
Corner1,
|
||||
Corner2,
|
||||
Height,
|
||||
}
|
||||
|
||||
impl BoxCommand {
|
||||
pub fn new(color: [f32; 4]) -> Self {
|
||||
Self {
|
||||
step: BoxStep::Corner1,
|
||||
p1: Vec3::ZERO,
|
||||
p2: Vec3::ZERO,
|
||||
color,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for BoxCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"BOX"
|
||||
}
|
||||
fn prompt(&self) -> String {
|
||||
match self.step {
|
||||
BoxStep::Corner1 => "BOX First corner:".into(),
|
||||
BoxStep::Corner2 => "BOX Opposite corner (XY):".into(),
|
||||
BoxStep::Height => "BOX Height:".into(),
|
||||
}
|
||||
}
|
||||
fn on_point(&mut self, pt: Vec3) -> CmdResult {
|
||||
match self.step {
|
||||
BoxStep::Corner1 => {
|
||||
self.p1 = pt;
|
||||
self.step = BoxStep::Corner2;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
BoxStep::Corner2 => {
|
||||
self.p2 = pt;
|
||||
self.step = BoxStep::Height;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
BoxStep::Height => commit_box(
|
||||
self.p1,
|
||||
self.p2,
|
||||
(pt.y - self.p1.y).abs().max(1e-4),
|
||||
self.color,
|
||||
),
|
||||
}
|
||||
}
|
||||
fn wants_text_input(&self) -> bool {
|
||||
self.step == BoxStep::Height
|
||||
}
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
text.trim()
|
||||
.parse::<f32>()
|
||||
.ok()
|
||||
.filter(|&h| h.abs() > 1e-6)
|
||||
.map(|h| commit_box(self.p1, self.p2, h.abs(), self.color))
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
||||
fn commit_box(p1: Vec3, p2: Vec3, height: f32, color: [f32; 4]) -> CmdResult {
|
||||
// Map OpenCADStudio coords to truck: x→x, z→y, y→z
|
||||
let x0 = p1.x.min(p2.x) as f64;
|
||||
let y0 = p1.z.min(p2.z) as f64;
|
||||
let x1 = p1.x.max(p2.x) as f64;
|
||||
let y1 = p1.z.max(p2.z) as f64;
|
||||
let z0 = p1.y as f64;
|
||||
let h = height as f64;
|
||||
|
||||
// Build face at z=z0, then sweep to z0+h.
|
||||
let v00 = builder::vertex(Point3::new(x0, y0, z0));
|
||||
let v10 = builder::vertex(Point3::new(x1, y0, z0));
|
||||
let v11 = builder::vertex(Point3::new(x1, y1, z0));
|
||||
let v01 = builder::vertex(Point3::new(x0, y1, z0));
|
||||
let e0 = builder::line(&v00, &v10);
|
||||
let e1 = builder::line(&v10, &v11);
|
||||
let e2 = builder::line(&v11, &v01);
|
||||
let e3 = builder::line(&v01, &v00);
|
||||
let wire: truck_modeling::Wire = [e0, e1, e2, e3].into_iter().collect();
|
||||
let face = match builder::try_attach_plane(&[wire]) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return CmdResult::Cancel,
|
||||
};
|
||||
// tsweep on Face → Solid
|
||||
let solid = builder::tsweep(&face, TruckVec3::new(0.0, 0.0, h));
|
||||
CmdResult::CommitSolid3D {
|
||||
mesh_fn: Box::new(move |name| solid_to_mesh(&solid, color, &name)),
|
||||
}
|
||||
}
|
||||
|
||||
// ── SPHERE command ─────────────────────────────────────────────────────────
|
||||
|
||||
pub struct SphereCommand {
|
||||
step: SphereStep,
|
||||
center: Vec3,
|
||||
color: [f32; 4],
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum SphereStep {
|
||||
Center,
|
||||
Radius,
|
||||
}
|
||||
|
||||
impl SphereCommand {
|
||||
pub fn new(color: [f32; 4]) -> Self {
|
||||
Self {
|
||||
step: SphereStep::Center,
|
||||
center: Vec3::ZERO,
|
||||
color,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for SphereCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"SPHERE"
|
||||
}
|
||||
fn prompt(&self) -> String {
|
||||
match self.step {
|
||||
SphereStep::Center => "SPHERE Center:".into(),
|
||||
SphereStep::Radius => "SPHERE Radius:".into(),
|
||||
}
|
||||
}
|
||||
fn on_point(&mut self, pt: Vec3) -> CmdResult {
|
||||
match self.step {
|
||||
SphereStep::Center => {
|
||||
self.center = pt;
|
||||
self.step = SphereStep::Radius;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
SphereStep::Radius => {
|
||||
commit_sphere(self.center, (pt - self.center).length(), self.color)
|
||||
}
|
||||
}
|
||||
}
|
||||
fn wants_text_input(&self) -> bool {
|
||||
self.step == SphereStep::Radius
|
||||
}
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
text.trim()
|
||||
.parse::<f32>()
|
||||
.ok()
|
||||
.filter(|&r| r > 1e-6)
|
||||
.map(|r| commit_sphere(self.center, r, self.color))
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
||||
fn commit_sphere(center: Vec3, radius: f32, color: [f32; 4]) -> CmdResult {
|
||||
let cx = center.x as f64;
|
||||
let cy = center.z as f64;
|
||||
let cz = center.y as f64;
|
||||
let r = radius as f64;
|
||||
|
||||
// Build a half-circle arc wire from north pole to south pole (XZ plane).
|
||||
let north = builder::vertex(Point3::new(cx, cy, cz + r));
|
||||
let south = builder::vertex(Point3::new(cx, cy, cz - r));
|
||||
let east = Point3::new(cx + r, cy, cz);
|
||||
let arc = builder::circle_arc(&north, &south, east);
|
||||
// rsweep the wire around the Z axis for a full revolution → Shell.
|
||||
let wire: truck_modeling::Wire = std::iter::once(arc).collect();
|
||||
let axis_pt = Point3::new(cx, cy, cz);
|
||||
let axis = TruckVec3::new(0.0, 0.0, 1.0);
|
||||
let shell = builder::rsweep(&wire, axis_pt, axis, Rad(std::f64::consts::TAU));
|
||||
CmdResult::CommitSolid3D {
|
||||
mesh_fn: Box::new(move |name| shell_to_mesh(&shell, color, &name)),
|
||||
}
|
||||
}
|
||||
|
||||
// ── CYLINDER command ───────────────────────────────────────────────────────
|
||||
|
||||
pub struct CylinderCommand {
|
||||
step: CylStep,
|
||||
center: Vec3,
|
||||
radius: f32,
|
||||
color: [f32; 4],
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum CylStep {
|
||||
Center,
|
||||
Radius,
|
||||
Height,
|
||||
}
|
||||
|
||||
impl CylinderCommand {
|
||||
pub fn new(color: [f32; 4]) -> Self {
|
||||
Self {
|
||||
step: CylStep::Center,
|
||||
center: Vec3::ZERO,
|
||||
radius: 1.0,
|
||||
color,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for CylinderCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"CYLINDER"
|
||||
}
|
||||
fn prompt(&self) -> String {
|
||||
match self.step {
|
||||
CylStep::Center => "CYLINDER Center of base:".into(),
|
||||
CylStep::Radius => "CYLINDER Radius:".into(),
|
||||
CylStep::Height => "CYLINDER Height:".into(),
|
||||
}
|
||||
}
|
||||
fn on_point(&mut self, pt: Vec3) -> CmdResult {
|
||||
match self.step {
|
||||
CylStep::Center => {
|
||||
self.center = pt;
|
||||
self.step = CylStep::Radius;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
CylStep::Radius => {
|
||||
self.radius = (pt - self.center).length().max(1e-4);
|
||||
self.step = CylStep::Height;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
CylStep::Height => commit_cylinder(
|
||||
self.center,
|
||||
self.radius,
|
||||
(pt.y - self.center.y).abs().max(1e-4),
|
||||
self.color,
|
||||
),
|
||||
}
|
||||
}
|
||||
fn wants_text_input(&self) -> bool {
|
||||
matches!(self.step, CylStep::Radius | CylStep::Height)
|
||||
}
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let v = text.trim().parse::<f32>().ok().filter(|&v| v > 1e-6)?;
|
||||
match self.step {
|
||||
CylStep::Radius => {
|
||||
self.radius = v;
|
||||
self.step = CylStep::Height;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
CylStep::Height => Some(commit_cylinder(self.center, self.radius, v, self.color)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
||||
fn commit_cylinder(center: Vec3, radius: f32, height: f32, color: [f32; 4]) -> CmdResult {
|
||||
let cx = center.x as f64;
|
||||
let cy = center.z as f64;
|
||||
let cz = center.y as f64;
|
||||
let r = radius as f64;
|
||||
let h = height as f64;
|
||||
|
||||
// Build circle face at z=cz, sweep upward.
|
||||
let right = builder::vertex(Point3::new(cx + r, cy, cz));
|
||||
let left = builder::vertex(Point3::new(cx - r, cy, cz));
|
||||
let top_t = Point3::new(cx, cy + r, cz);
|
||||
let bot_t = Point3::new(cx, cy - r, cz);
|
||||
let upper = builder::circle_arc(&right, &left, top_t);
|
||||
let lower = builder::circle_arc(&left, &right, bot_t);
|
||||
let wire: truck_modeling::Wire = [upper, lower].into_iter().collect();
|
||||
let face = match builder::try_attach_plane(&[wire]) {
|
||||
Ok(f) => f,
|
||||
Err(_) => return CmdResult::Cancel,
|
||||
};
|
||||
let solid = builder::tsweep(&face, TruckVec3::new(0.0, 0.0, h));
|
||||
CmdResult::CommitSolid3D {
|
||||
mesh_fn: Box::new(move |name| solid_to_mesh(&solid, color, &name)),
|
||||
}
|
||||
}
|
||||
// BOX / SPHERE / CYLINDER (and CONE / WEDGE / TORUS) now live in the Model tab
|
||||
// (`modules::model::primitive_cmd`), which builds them as truck B-reps cached
|
||||
// for the Design-group boolean tools. EXTRUDE / REVOLVE / SWEEP / LOFT remain
|
||||
// here as 2D→3D operations.
|
||||
|
||||
// ── EXTRUDE command ────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -661,10 +329,7 @@ pub fn empty_solid3d() -> EntityType {
|
|||
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["BOX"] }); // BoxCommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["CYLINDER"] }); // CylinderCommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["EXT", "EXTRUDE"] }); // ExtrudeCommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["LOFT"] }); // LoftCommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["REV", "REVOLVE"] }); // RevolveCommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["SPHERE"] }); // SphereCommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["SWEEP"] }); // SweepCommand
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ pub trait CadModule: Send + Sync {
|
|||
pub mod annotate;
|
||||
pub mod home;
|
||||
pub mod insert;
|
||||
pub mod model;
|
||||
pub mod layout;
|
||||
pub mod manage;
|
||||
pub mod view;
|
||||
|
|
|
|||
27
src/modules/model/boolean_cmd.rs
Normal file
27
src/modules/model/boolean_cmd.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Boolean operations on 3D solids (Design group). The actual CSG runs in
|
||||
// `App::solid_boolean` (src/app/model_ops.rs) using truck-shapeops on the
|
||||
// session-cached truck B-reps; this module just names the operations.
|
||||
|
||||
/// Which boolean a Design-group tool performs on the two selected solids.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum BoolOp {
|
||||
Union,
|
||||
Subtract,
|
||||
Intersect,
|
||||
}
|
||||
|
||||
impl BoolOp {
|
||||
pub fn from_id(id: &str) -> Option<BoolOp> {
|
||||
Some(match id {
|
||||
"UNION" => BoolOp::Union,
|
||||
"SUBTRACT" => BoolOp::Subtract,
|
||||
"INTERSECT" => BoolOp::Intersect,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &["UNION", "SUBTRACT", "INTERSECT"]
|
||||
});
|
||||
63
src/modules/model/mod.rs
Normal file
63
src/modules/model/mod.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
// Model module — 3D solid modelling.
|
||||
//
|
||||
// Model group : create primitive solids (box, cylinder, cone, sphere, …)
|
||||
// as ACIS Solid3D entities via acadrust's primitive builders.
|
||||
// Design group : combine solids with truck boolean operations
|
||||
// (union / subtract / intersect).
|
||||
|
||||
pub mod boolean_cmd;
|
||||
pub mod primitive_cmd;
|
||||
|
||||
use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef};
|
||||
|
||||
pub struct ModelModule;
|
||||
|
||||
/// Helper to declare a ribbon tool that fires a named command.
|
||||
fn tool(id: &'static str, label: &'static str, glyph: &'static str) -> ToolDef {
|
||||
ToolDef {
|
||||
id,
|
||||
label,
|
||||
icon: IconKind::Glyph(glyph),
|
||||
event: ModuleEvent::Command(id.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
impl CadModule for ModelModule {
|
||||
fn id(&self) -> &'static str {
|
||||
"model"
|
||||
}
|
||||
fn title(&self) -> &'static str {
|
||||
"Model"
|
||||
}
|
||||
|
||||
fn ribbon_groups(&self) -> Vec<RibbonGroup> {
|
||||
vec![
|
||||
RibbonGroup {
|
||||
title: "Model",
|
||||
tools: vec![
|
||||
RibbonItem::LargeTool(tool("BOX", "Box", "▦")),
|
||||
RibbonItem::LargeTool(tool("CYLINDER", "Cylinder", "⬭")),
|
||||
RibbonItem::LargeTool(tool("CONE", "Cone", "△")),
|
||||
RibbonItem::LargeTool(tool("SPHERE", "Sphere", "◯")),
|
||||
RibbonItem::Dropdown {
|
||||
id: "MODEL_MORE",
|
||||
icon: IconKind::Glyph("◰"),
|
||||
items: vec![
|
||||
("WEDGE", "Wedge", IconKind::Glyph("◣")),
|
||||
("TORUS", "Torus", IconKind::Glyph("◎")),
|
||||
],
|
||||
default: "WEDGE",
|
||||
},
|
||||
],
|
||||
},
|
||||
RibbonGroup {
|
||||
title: "Design",
|
||||
tools: vec![
|
||||
RibbonItem::LargeTool(tool("UNION", "Union", "⊕")),
|
||||
RibbonItem::LargeTool(tool("SUBTRACT", "Subtract", "⊖")),
|
||||
RibbonItem::LargeTool(tool("INTERSECT", "Intersect", "⊗")),
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
324
src/modules/model/primitive_cmd.rs
Normal file
324
src/modules/model/primitive_cmd.rs
Normal file
|
|
@ -0,0 +1,324 @@
|
|||
// 3D primitive creation — BOX / CYLINDER / CONE / SPHERE / WEDGE / PYRAMID /
|
||||
// TORUS. Each is placed CAD-style with a few clicks (planar footprint first,
|
||||
// then a height value), then built as a real ACIS `Solid3D` via acadrust's
|
||||
// `acis::primitives` builders. `Scene::add_entity` tessellates the SAT B-rep
|
||||
// into the 3D mesh pipeline, so the solid renders, selects, and saves to DXF.
|
||||
//
|
||||
// A matching truck `Solid` is cached on the scene (see model/mod.rs) when the
|
||||
// entity is committed, so the Design-group boolean tools can combine it.
|
||||
|
||||
use acadrust::entities::Solid3D;
|
||||
use acadrust::{primitives, EntityType};
|
||||
use glam::Vec3;
|
||||
use truck_modeling::Solid;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::scene::model_solid;
|
||||
use crate::scene::wire_model::WireModel;
|
||||
|
||||
/// Which primitive a `PrimitiveCommand` builds.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Shape {
|
||||
Box,
|
||||
Wedge,
|
||||
Cylinder,
|
||||
Cone,
|
||||
Sphere,
|
||||
Torus,
|
||||
}
|
||||
|
||||
impl Shape {
|
||||
fn from_id(id: &str) -> Option<Shape> {
|
||||
Some(match id {
|
||||
"BOX" => Shape::Box,
|
||||
"WEDGE" => Shape::Wedge,
|
||||
"CYLINDER" => Shape::Cylinder,
|
||||
"CONE" => Shape::Cone,
|
||||
"SPHERE" => Shape::Sphere,
|
||||
"TORUS" => Shape::Torus,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
fn name(self) -> &'static str {
|
||||
match self {
|
||||
Shape::Box => "BOX",
|
||||
Shape::Wedge => "WEDGE",
|
||||
Shape::Cylinder => "CYLINDER",
|
||||
Shape::Cone => "CONE",
|
||||
Shape::Sphere => "SPHERE",
|
||||
Shape::Torus => "TORUS",
|
||||
}
|
||||
}
|
||||
/// True for footprints picked as a centre + radius (round shapes); false
|
||||
/// for corner-to-corner footprints (box/wedge).
|
||||
fn radial(self) -> bool {
|
||||
!matches!(self, Shape::Box | Shape::Wedge)
|
||||
}
|
||||
/// Whether a height value is collected after the footprint.
|
||||
fn needs_height(self) -> bool {
|
||||
!matches!(self, Shape::Sphere | Shape::Torus)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PrimitiveCommand {
|
||||
shape: Shape,
|
||||
/// Footprint points collected so far (local/world XY, z = 0).
|
||||
pts: Vec<Vec3>,
|
||||
/// True once the footprint is set and we are collecting the height.
|
||||
height_step: bool,
|
||||
}
|
||||
|
||||
impl PrimitiveCommand {
|
||||
pub fn new(id: &str) -> Self {
|
||||
Self {
|
||||
shape: Shape::from_id(id).unwrap_or(Shape::Box),
|
||||
pts: Vec::new(),
|
||||
height_step: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of footprint points the shape needs before the height step.
|
||||
fn footprint_pts(&self) -> usize {
|
||||
match self.shape {
|
||||
Shape::Torus => 3, // centre, major-radius, minor-radius
|
||||
_ => 2, // corner/corner or centre/radius
|
||||
}
|
||||
}
|
||||
|
||||
/// A reasonable default height when the user just presses Enter.
|
||||
fn default_height(&self) -> f64 {
|
||||
match self.shape {
|
||||
Shape::Box | Shape::Wedge => {
|
||||
let d = self.pts[1] - self.pts[0];
|
||||
(d.x.abs().max(d.y.abs())) as f64
|
||||
}
|
||||
_ => (self.pts[1] - self.pts[0]).length() as f64,
|
||||
}
|
||||
.max(1.0)
|
||||
}
|
||||
|
||||
/// Build both the acadrust `Solid3D` (ACIS, for persistence) and the truck
|
||||
/// `Solid` B-rep (rendering + booleans) from the footprint + `height`.
|
||||
fn build(&self, height: f64) -> Option<(EntityType, Solid)> {
|
||||
let (doc, solid) = match self.shape {
|
||||
Shape::Box | Shape::Wedge => {
|
||||
let (a, b) = (self.pts[0], self.pts[1]);
|
||||
let length = (b.x - a.x).abs() as f64;
|
||||
let width = (b.y - a.y).abs() as f64;
|
||||
if length < 1e-6 || width < 1e-6 || height < 1e-6 {
|
||||
return None;
|
||||
}
|
||||
if self.shape == Shape::Box {
|
||||
let center = [
|
||||
(a.x + b.x) as f64 / 2.0,
|
||||
(a.y + b.y) as f64 / 2.0,
|
||||
height / 2.0,
|
||||
];
|
||||
(
|
||||
primitives::build_box(center, length, width, height),
|
||||
model_solid::box_solid(center, length, width, height),
|
||||
)
|
||||
} else {
|
||||
let origin = [a.x.min(b.x) as f64, a.y.min(b.y) as f64, 0.0];
|
||||
(
|
||||
primitives::build_wedge(origin, length, width, height),
|
||||
model_solid::wedge_solid(origin, length, width, height),
|
||||
)
|
||||
}
|
||||
}
|
||||
Shape::Cylinder | Shape::Cone => {
|
||||
let c = self.pts[0];
|
||||
let r = (self.pts[1] - c).length() as f64;
|
||||
if r < 1e-6 || height < 1e-6 {
|
||||
return None;
|
||||
}
|
||||
let center = [c.x as f64, c.y as f64, 0.0];
|
||||
if self.shape == Shape::Cylinder {
|
||||
(
|
||||
primitives::build_cylinder(center, r, height),
|
||||
model_solid::cylinder_solid(center, r, height),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
primitives::build_cone(center, r, height),
|
||||
model_solid::cone_solid(center, r, height),
|
||||
)
|
||||
}
|
||||
}
|
||||
Shape::Sphere => {
|
||||
let c = self.pts[0];
|
||||
let r = (self.pts[1] - c).length() as f64;
|
||||
if r < 1e-6 {
|
||||
return None;
|
||||
}
|
||||
let center = [c.x as f64, c.y as f64, 0.0];
|
||||
(
|
||||
primitives::build_sphere(center, r),
|
||||
model_solid::sphere_solid(center, r),
|
||||
)
|
||||
}
|
||||
Shape::Torus => {
|
||||
let c = self.pts[0];
|
||||
let major = (self.pts[1] - c).length() as f64;
|
||||
let minor = (self.pts[2] - self.pts[1]).length() as f64;
|
||||
if major < 1e-6 || minor < 1e-6 {
|
||||
return None;
|
||||
}
|
||||
let center = [c.x as f64, c.y as f64, 0.0];
|
||||
(
|
||||
primitives::build_torus(center, major, minor),
|
||||
model_solid::torus_solid(center, major, minor),
|
||||
)
|
||||
}
|
||||
};
|
||||
let mut s3d = Solid3D::new();
|
||||
s3d.set_sat_document(&doc);
|
||||
// Edge wires make the solid click-pickable and draw a wireframe over
|
||||
// the shaded mesh.
|
||||
s3d.wires = model_solid::edge_wires(&solid);
|
||||
Some((EntityType::Solid3D(s3d), solid))
|
||||
}
|
||||
|
||||
fn commit(&self, height: f64) -> CmdResult {
|
||||
match self.build(height) {
|
||||
Some((entity, solid)) => CmdResult::CommitSolid {
|
||||
entity,
|
||||
solid: Box::new(solid),
|
||||
},
|
||||
None => CmdResult::Cancel,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for PrimitiveCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
self.shape.name()
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
let n = self.shape.name();
|
||||
if self.height_step {
|
||||
return format!("{n} Specify height <Enter for default>:");
|
||||
}
|
||||
match (self.shape.radial(), self.pts.len()) {
|
||||
(false, 0) => format!("{n} Specify first corner:"),
|
||||
(false, _) => format!("{n} Specify opposite corner:"),
|
||||
(true, 0) => format!("{n} Specify center point:"),
|
||||
(true, 1) => format!("{n} Specify radius:"),
|
||||
(true, _) => format!("{n} Specify tube radius:"),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: Vec3) -> CmdResult {
|
||||
if self.height_step {
|
||||
// A click in the ground plane has no Z; use its distance from the
|
||||
// footprint centre as the height magnitude.
|
||||
let h = (pt - self.pts[0]).length() as f64;
|
||||
return self.commit(h.max(1e-6));
|
||||
}
|
||||
self.pts.push(pt);
|
||||
if self.pts.len() < self.footprint_pts() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
// Footprint complete.
|
||||
if self.shape.needs_height() {
|
||||
self.height_step = true;
|
||||
CmdResult::NeedPoint
|
||||
} else {
|
||||
self.commit(0.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
if self.height_step {
|
||||
let h = self.default_height();
|
||||
return self.commit(h);
|
||||
}
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
self.height_step
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, raw: &str) -> Option<CmdResult> {
|
||||
if !self.height_step {
|
||||
return None;
|
||||
}
|
||||
let h: f64 = raw.trim().parse().ok().filter(|v| *v > 0.0)?;
|
||||
Some(self.commit(h))
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: Vec3) -> Option<WireModel> {
|
||||
if self.height_step || self.pts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut foot = self.pts.clone();
|
||||
foot.push(pt);
|
||||
Some(footprint_wire(self.shape, &foot))
|
||||
}
|
||||
}
|
||||
|
||||
// ── Footprint preview ───────────────────────────────────────────────────────
|
||||
|
||||
fn footprint_wire(shape: Shape, pts: &[Vec3]) -> WireModel {
|
||||
let mut points: Vec<[f32; 3]> = Vec::new();
|
||||
if shape.radial() {
|
||||
let c = pts[0];
|
||||
let r = (pts[1] - c).length();
|
||||
circle_points(&mut points, c, r);
|
||||
if shape == Shape::Torus && pts.len() >= 3 {
|
||||
// outer ring at major + minor for a quick torus hint
|
||||
let minor = (pts[2] - pts[1]).length();
|
||||
points.push([f32::NAN; 3]);
|
||||
circle_points(&mut points, c, r + minor);
|
||||
}
|
||||
} else {
|
||||
let (a, b) = (pts[0], pts[1]);
|
||||
points.extend_from_slice(&[
|
||||
[a.x, a.y, 0.0],
|
||||
[b.x, a.y, 0.0],
|
||||
[b.x, b.y, 0.0],
|
||||
[a.x, b.y, 0.0],
|
||||
[a.x, a.y, 0.0],
|
||||
]);
|
||||
}
|
||||
wire("primitive_preview", points)
|
||||
}
|
||||
|
||||
fn circle_points(out: &mut Vec<[f32; 3]>, c: Vec3, r: f32) {
|
||||
const SEG: usize = 48;
|
||||
for i in 0..=SEG {
|
||||
let t = i as f32 / SEG as f32 * std::f32::consts::TAU;
|
||||
out.push([c.x + r * t.cos(), c.y + r * t.sin(), 0.0]);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &["BOX", "WEDGE", "CYLINDER", "CONE", "SPHERE", "TORUS"]
|
||||
});
|
||||
|
||||
fn wire(name: &str, points: Vec<[f32; 3]>) -> WireModel {
|
||||
WireModel {
|
||||
name: name.into(),
|
||||
points,
|
||||
color: WireModel::CYAN,
|
||||
selected: false,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px: 1.0,
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
aci: 0,
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ use crate::modules::CadModule;
|
|||
pub fn all_modules() -> Vec<Box<dyn CadModule>> {
|
||||
vec![
|
||||
Box::new(super::home::HomeModule),
|
||||
Box::new(super::model::ModelModule),
|
||||
Box::new(super::insert::InsertModule),
|
||||
Box::new(super::annotate::AnnotateModule),
|
||||
Box::new(super::view::ViewModule),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ pub mod hatch_patterns;
|
|||
pub mod hit_test;
|
||||
pub mod image_model;
|
||||
pub mod mesh_model;
|
||||
pub mod model_solid;
|
||||
pub mod object;
|
||||
pub mod paper_canvas;
|
||||
pub mod pipeline;
|
||||
|
|
@ -614,6 +615,11 @@ pub struct Scene {
|
|||
pub hatches: HashMap<Handle, HatchModel>,
|
||||
/// GPU render data for solid meshes (truck Shell/Solid tessellation).
|
||||
pub meshes: HashMap<Handle, MeshLodSet>,
|
||||
/// Live truck B-reps for solids created this session by the Model tab,
|
||||
/// keyed by entity handle. Backs the Design-group boolean tools (a solid
|
||||
/// must be here to be combined). Not persisted — rebuilt only by creating
|
||||
/// or combining primitives in-session.
|
||||
pub model_solids: HashMap<Handle, truck_modeling::Solid>,
|
||||
/// GPU render data for raster images (RasterImage entities), keyed by handle.
|
||||
pub images: HashMap<Handle, ImageModel>,
|
||||
/// The viewport that is currently "entered" (MSPACE mode).
|
||||
|
|
@ -703,6 +709,7 @@ impl Scene {
|
|||
current_layout: "Model".to_string(),
|
||||
hatches: HashMap::new(),
|
||||
meshes: HashMap::new(),
|
||||
model_solids: HashMap::new(),
|
||||
images: HashMap::new(),
|
||||
active_viewport: None,
|
||||
bg_color: [0.11, 0.11, 0.11, 1.0],
|
||||
|
|
@ -833,6 +840,23 @@ impl Scene {
|
|||
}
|
||||
|
||||
/// Re-evaluate every cached mesh's color through `render_style` so a
|
||||
/// Register a Model-tab solid: cache its truck B-rep (for boolean ops) and
|
||||
/// tessellate it into the shaded mesh pipeline under `handle`. The solid is
|
||||
/// in the same offset-relative frame the mesh pipeline uses, so the mesh is
|
||||
/// stored as-is (Model-tab geometry is authored at world_offset 0).
|
||||
pub fn register_model_solid(&mut self, handle: Handle, solid: truck_modeling::Solid) {
|
||||
let color = self
|
||||
.document
|
||||
.get_entity(handle)
|
||||
.map(|e| self.render_style(e).0)
|
||||
.unwrap_or([0.8, 0.8, 0.85, 1.0]);
|
||||
if let Some(set) = crate::scene::model_solid::mesh_from_solid(&solid, color) {
|
||||
self.meshes.insert(handle, set);
|
||||
}
|
||||
self.model_solids.insert(handle, solid);
|
||||
self.bump_geometry();
|
||||
}
|
||||
|
||||
/// `BACKGROUND` change picks up the new `adapt_to_bg` result without
|
||||
/// re-tessellating ACIS geometry. Caller must bump `geometry_epoch`
|
||||
/// afterwards so the GPU re-uploads the now-updated colour data.
|
||||
|
|
@ -2711,6 +2735,7 @@ impl Scene {
|
|||
for h in &to_remove {
|
||||
self.hatches.remove(h);
|
||||
self.meshes.remove(h);
|
||||
self.model_solids.remove(h);
|
||||
self.document.remove_entity(*h);
|
||||
}
|
||||
|
||||
|
|
@ -4070,6 +4095,7 @@ impl Scene {
|
|||
self.selected.remove(&h);
|
||||
self.hatches.remove(&h);
|
||||
self.meshes.remove(&h);
|
||||
self.model_solids.remove(&h);
|
||||
}
|
||||
// Remove erased handles from all groups; delete groups that become empty.
|
||||
let group_dict_handle = self.document.header.acad_group_dict_handle;
|
||||
|
|
|
|||
216
src/scene/model_solid.rs
Normal file
216
src/scene/model_solid.rs
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
// truck B-rep construction for the Model tab's primitives, plus tessellation
|
||||
// into the renderer's `MeshLodSet`. Each builder follows truck's own example
|
||||
// recipes (tsweep / rsweep / cone / try_attach_plane), oriented Z-up with the
|
||||
// footprint on the z = `base_z` plane to match acadrust's `acis::primitives`.
|
||||
//
|
||||
// The resulting truck `Solid` is cached per entity handle on the Scene so the
|
||||
// Design-group boolean tools can run truck-shapeops on it.
|
||||
|
||||
use truck_modeling::{builder, Point3, Rad, Solid, Vector3, Wire};
|
||||
|
||||
use crate::scene::mesh_model::{MeshLodSet, MeshModel};
|
||||
|
||||
const FULL: f64 = 7.0; // > 2π → builder closes the revolution
|
||||
|
||||
/// Axis-aligned box from its center and full extents.
|
||||
pub fn box_solid(center: [f64; 3], length: f64, width: f64, height: f64) -> Solid {
|
||||
let min = Point3::new(
|
||||
center[0] - length / 2.0,
|
||||
center[1] - width / 2.0,
|
||||
center[2] - height / 2.0,
|
||||
);
|
||||
let v = builder::vertex(min);
|
||||
let e = builder::tsweep(&v, Vector3::new(length, 0.0, 0.0));
|
||||
let f = builder::tsweep(&e, Vector3::new(0.0, width, 0.0));
|
||||
builder::tsweep(&f, Vector3::new(0.0, 0.0, height))
|
||||
}
|
||||
|
||||
/// Right triangular prism (wedge): right-triangle cross-section in XZ,
|
||||
/// extruded along Y. `origin` is the min corner, ramp rising in +X/+Z.
|
||||
pub fn wedge_solid(origin: [f64; 3], length: f64, width: f64, height: f64) -> Solid {
|
||||
let o = Point3::new(origin[0], origin[1], origin[2]);
|
||||
let a = builder::vertex(o);
|
||||
let b = builder::vertex(Point3::new(o.x + length, o.y, o.z));
|
||||
let c = builder::vertex(Point3::new(o.x, o.y, o.z + height));
|
||||
let wire: Wire = vec![
|
||||
builder::line(&a, &b),
|
||||
builder::line(&b, &c),
|
||||
builder::line(&c, &a),
|
||||
]
|
||||
.into();
|
||||
let face = builder::try_attach_plane(&[wire]).expect("wedge profile");
|
||||
builder::tsweep(&face, Vector3::new(0.0, width, 0.0))
|
||||
}
|
||||
|
||||
/// Solid cylinder: disk on the z = base plane, extruded up by `height`.
|
||||
pub fn cylinder_solid(center: [f64; 3], radius: f64, height: f64) -> Solid {
|
||||
let v = builder::vertex(Point3::new(center[0] + radius, center[1], center[2]));
|
||||
let circle = builder::rsweep(
|
||||
&v,
|
||||
Point3::new(center[0], center[1], center[2]),
|
||||
Vector3::unit_z(),
|
||||
Rad(FULL),
|
||||
);
|
||||
let disk = builder::try_attach_plane(&[circle]).expect("cylinder cap");
|
||||
builder::tsweep(&disk, Vector3::new(0.0, 0.0, height))
|
||||
}
|
||||
|
||||
/// Solid cone: profile (apex → rim → base-center) revolved about the axis.
|
||||
pub fn cone_solid(center: [f64; 3], radius: f64, height: f64) -> Solid {
|
||||
let cx = center[0];
|
||||
let cy = center[1];
|
||||
let cz = center[2];
|
||||
let apex = builder::vertex(Point3::new(cx, cy, cz + height));
|
||||
let rim = builder::vertex(Point3::new(cx + radius, cy, cz));
|
||||
let base = builder::vertex(Point3::new(cx, cy, cz));
|
||||
let wire: Wire = vec![builder::line(&apex, &rim), builder::line(&rim, &base)].into();
|
||||
let shell = builder::cone(&wire, Vector3::unit_z(), Rad(FULL));
|
||||
Solid::new(vec![shell])
|
||||
}
|
||||
|
||||
/// Solid sphere: meridian semicircle revolved about the polar (Z) axis.
|
||||
pub fn sphere_solid(center: [f64; 3], radius: f64) -> Solid {
|
||||
let c = Point3::new(center[0], center[1], center[2]);
|
||||
let top = builder::vertex(Point3::new(c.x, c.y, c.z + radius));
|
||||
// Rotate the top point about the X axis by π → meridian from +Z to −Z.
|
||||
let meridian: Wire = builder::rsweep(&top, c, Vector3::unit_x(), Rad(std::f64::consts::PI));
|
||||
let shell = builder::cone(&meridian, Vector3::unit_z(), Rad(FULL));
|
||||
Solid::new(vec![shell])
|
||||
}
|
||||
|
||||
/// Solid torus in the z = base plane (tube revolved about the Z axis).
|
||||
pub fn torus_solid(center: [f64; 3], major: f64, minor: f64) -> Solid {
|
||||
let c = Point3::new(center[0], center[1], center[2]);
|
||||
let v = builder::vertex(Point3::new(c.x + major, c.y, c.z + minor));
|
||||
let tube = builder::rsweep(
|
||||
&v,
|
||||
Point3::new(c.x + major, c.y, c.z),
|
||||
Vector3::unit_y(),
|
||||
Rad(FULL),
|
||||
);
|
||||
let shell = builder::rsweep(&tube, c, Vector3::unit_z(), Rad(FULL));
|
||||
Solid::new(vec![shell])
|
||||
}
|
||||
|
||||
// ── Edge extraction (pick geometry + wireframe overlay) ─────────────────────
|
||||
|
||||
/// Tessellate the solid's B-rep edges into acadrust `Wire`s. Stored on the
|
||||
/// `Solid3D`/result entity so it is click-pickable (the renderer's wire
|
||||
/// fallback draws these as a wireframe over the shaded mesh, and hit-testing
|
||||
/// uses their points).
|
||||
pub fn edge_wires(solid: &Solid) -> Vec<acadrust::entities::Wire> {
|
||||
use crate::scene::truck_tess::{tessellate_edge, TruckTessResult};
|
||||
use acadrust::types::Vector3;
|
||||
let mut wires = Vec::new();
|
||||
for shell in solid.boundaries() {
|
||||
for edge in shell.edge_iter() {
|
||||
if let TruckTessResult::Lines(pts) = tessellate_edge(&edge, [0.0; 3]) {
|
||||
if pts.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
let pts3: Vec<Vector3> = pts
|
||||
.iter()
|
||||
.map(|p| Vector3::new(p[0] as f64, p[1] as f64, p[2] as f64))
|
||||
.collect();
|
||||
wires.push(acadrust::entities::Wire::from_points(pts3));
|
||||
}
|
||||
}
|
||||
}
|
||||
wires
|
||||
}
|
||||
|
||||
// ── Boolean operations (truck-shapeops) ─────────────────────────────────────
|
||||
|
||||
/// Which CSG to apply. Mirrors `model::boolean_cmd::BoolOp` but kept local so
|
||||
/// this scene module has no dependency on the UI module.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Bool {
|
||||
Union,
|
||||
Subtract,
|
||||
Intersect,
|
||||
}
|
||||
|
||||
const BOOL_TOL: f64 = 0.05;
|
||||
|
||||
/// Combine two solids. `Subtract` removes `b` from `a`. Returns `None` when the
|
||||
/// operation fails (e.g. the solids don't actually overlap).
|
||||
pub fn boolean(op: Bool, a: &Solid, b: &Solid) -> Option<Solid> {
|
||||
match op {
|
||||
Bool::Union => truck_shapeops::or(a, b, BOOL_TOL),
|
||||
Bool::Intersect => truck_shapeops::and(a, b, BOOL_TOL),
|
||||
Bool::Subtract => {
|
||||
let mut bn = b.clone();
|
||||
bn.not();
|
||||
truck_shapeops::and(a, &bn, BOOL_TOL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tessellation ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Tessellate a truck `Solid` into a single-LOD `MeshLodSet` (world-space,
|
||||
/// before world_offset is applied by the caller).
|
||||
pub fn mesh_from_solid(solid: &Solid, color: [f32; 4]) -> Option<MeshLodSet> {
|
||||
use crate::scene::truck_tess::{tessellate_solid, TruckTessResult};
|
||||
match tessellate_solid(solid, [0.0; 3]) {
|
||||
TruckTessResult::Mesh {
|
||||
verts,
|
||||
normals,
|
||||
indices,
|
||||
} if !indices.is_empty() => {
|
||||
let mesh = MeshModel {
|
||||
name: String::new(),
|
||||
verts,
|
||||
normals,
|
||||
indices,
|
||||
color,
|
||||
selected: false,
|
||||
};
|
||||
Some(MeshLodSet::from_single(mesh))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn tri_count(s: &Solid) -> usize {
|
||||
mesh_from_solid(s, [0.7, 0.7, 0.7, 1.0])
|
||||
.map(|m| m.lods[0].indices.len() / 3)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_primitives_triangulate() {
|
||||
let c = [0.0, 0.0, 0.0];
|
||||
assert!(tri_count(&box_solid(c, 10.0, 10.0, 10.0)) >= 12, "box");
|
||||
assert!(tri_count(&wedge_solid(c, 10.0, 10.0, 10.0)) >= 6, "wedge");
|
||||
assert!(tri_count(&cylinder_solid(c, 5.0, 12.0)) > 20, "cylinder");
|
||||
assert!(tri_count(&cone_solid(c, 5.0, 12.0)) > 10, "cone");
|
||||
assert!(tri_count(&sphere_solid(c, 5.0)) > 50, "sphere");
|
||||
assert!(tri_count(&torus_solid(c, 8.0, 2.0)) > 50, "torus");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn booleans_produce_solids() {
|
||||
let a = box_solid([0.0, 0.0, 0.0], 10.0, 10.0, 10.0);
|
||||
let b = box_solid([5.0, 5.0, 5.0], 10.0, 10.0, 10.0);
|
||||
for (op, label) in [
|
||||
(Bool::Union, "union"),
|
||||
(Bool::Subtract, "subtract"),
|
||||
(Bool::Intersect, "intersect"),
|
||||
] {
|
||||
let r = boolean(op, &a, &b);
|
||||
let n = r.as_ref().map(tri_count).unwrap_or(0);
|
||||
eprintln!("{label}: tris={n}");
|
||||
assert!(r.is_some() && n > 0, "{label} produced nothing");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn box_exposes_edges() {
|
||||
assert!(edge_wires(&box_solid([0.0, 0.0, 0.0], 10.0, 10.0, 10.0)).len() >= 12);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue