diff --git a/Cargo.lock b/Cargo.lock index 068bd0eac..c7762b135 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,9 +43,6 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.9", - "truck-meshalgo", - "truck-modeling", - "truck-polymesh", "ttf-parser", "ureq", "wasm-bindgen", @@ -346,15 +343,6 @@ version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" -[[package]] -name = "approx" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278" -dependencies = [ - "num-traits", -] - [[package]] name = "arbitrary" version = "1.4.2" @@ -402,12 +390,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "array-macro" -version = "2.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "220a2c618ab466efe41d0eace94dfeff1c35e3aa47891bdb95e1c0fefffd3c99" - [[package]] name = "arrayref" version = "0.3.9" @@ -832,15 +814,6 @@ dependencies = [ "piper", ] -[[package]] -name = "branches" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f11502672c5570f77f6bdf573332483f8475bab6a7fda00f1fae8ddb5a6245c0" -dependencies = [ - "rustc_version", -] - [[package]] name = "brotli-decompressor" version = "5.0.3" @@ -932,7 +905,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git#699e346fe67d51d7516abf1d27babf0259849713" +source = "git+https://github.com/HakanSeven12/cadkernel.git#afdce2a7d8237b87811e6a192b9378bdb1df783e" dependencies = [ "cavalier_contours", ] @@ -1012,17 +985,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" -[[package]] -name = "cgmath" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a98d30140e3296250832bbaaff83b27dcd6fa3cc70fb6f1f3e5c9c0023b5317" -dependencies = [ - "approx", - "num-traits", - "serde", -] - [[package]] name = "cipher" version = "0.4.4" @@ -1149,12 +1111,6 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - [[package]] name = "core-foundation" version = "0.9.4" @@ -1412,19 +1368,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "derive_more" -version = "0.99.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.119", -] - [[package]] name = "digest" version = "0.10.7" @@ -2037,10 +1980,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi", - "wasm-bindgen", ] [[package]] @@ -2725,15 +2666,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.14.0" @@ -2877,17 +2809,6 @@ dependencies = [ "mutate_once", ] -[[package]] -name = "katexit" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccfb0b7ce7938f84a5ecbdca5d0a991e46bc9d6d078934ad5e92c5270fe547db" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "khronos-egl" version = "6.0.0" @@ -3058,7 +2979,7 @@ dependencies = [ "md-5", "nom 8.0.0", "nom_locate", - "rand 0.9.5", + "rand", "rangemap", "sha2 0.10.9", "stringprep", @@ -3137,17 +3058,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "matext4cgmath" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6541e181de37f70f0aceb25441823a3d0efa9cc1d23f475a0d3926678949178" -dependencies = [ - "cgmath", - "katexit", - "num-complex", -] - [[package]] name = "maybe-rayon" version = "0.1.1" @@ -4428,35 +4338,14 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" -[[package]] -name = "rand" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - [[package]] name = "rand" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", + "rand_chacha", + "rand_core", ] [[package]] @@ -4466,16 +4355,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", + "rand_core", ] [[package]] @@ -4526,8 +4406,8 @@ dependencies = [ "num-traits", "paste", "profiling", - "rand 0.9.5", - "rand_chacha 0.9.0", + "rand", + "rand_chacha", "simd_helpers", "thiserror 2.0.18", "v_frame", @@ -4596,15 +4476,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "rclite" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e09665c494a9de2bd230e1aedc0c2fb20481a51f28f7db84d0c9bcfe2fe537b2" -dependencies = [ - "branches", -] - [[package]] name = "read-fonts" version = "0.35.0" @@ -4813,12 +4684,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "robust" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e27ee8bb91ca0adcf0ecb116293afa12d393f9c2b9b9cd54d33e8078fe19839" - [[package]] name = "roxmltree" version = "0.20.0" @@ -5363,18 +5228,6 @@ dependencies = [ "x11rb", ] -[[package]] -name = "spade" -version = "2.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9699399fd9349b00b184f5635b074f9ec93afffef30c853f8c875b32c0f8c7fa" -dependencies = [ - "hashbrown 0.16.1", - "num-traits", - "robust", - "smallvec", -] - [[package]] name = "spirv" version = "0.4.0+sdk-1.4.341.0" @@ -5785,122 +5638,6 @@ dependencies = [ "petgraph", ] -[[package]] -name = "truck-base" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c279de9e92e5dc20a188deb0bb9a4bd421a6a185e57e03e19025e84a36c5a05" -dependencies = [ - "cgmath", - "matext4cgmath", - "rustc-hash 2.1.3", - "serde", -] - -[[package]] -name = "truck-derivers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "421ff1c5a303aed64e295d9b30e8424ee7a2fa251617a59ae4acfd078496009f" -dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "truck-geometry" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d9d85f203fdbc2d206a38b8e7c4417a05a2eb589be8b81ecfe9f98faafc425d" -dependencies = [ - "serde", - "thiserror 1.0.69", - "truck-base", - "truck-geotrait", -] - -[[package]] -name = "truck-geotrait" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a31fe98d5a0589ad20576260e214af23dea21a8d125da81ad3bbcb666f411f" -dependencies = [ - "getrandom 0.2.17", - "rand 0.8.7", - "thiserror 1.0.69", - "truck-base", - "truck-derivers", -] - -[[package]] -name = "truck-meshalgo" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e18f325738e822c79bb1a48897ec789cbf50cbeeb0841105295f9da85bdf3613" -dependencies = [ - "array-macro", - "derive_more", - "itertools 0.13.0", - "rayon", - "rustc-hash 2.1.3", - "spade", - "truck-base", - "truck-geometry", - "truck-polymesh", - "truck-topology", -] - -[[package]] -name = "truck-modeling" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8082c2dfac8c2732a014d81807fb5d24dbac0d72ee4de2a705b7fbc05fa05b18" -dependencies = [ - "derive_more", - "rustc-hash 2.1.3", - "serde", - "thiserror 1.0.69", - "truck-base", - "truck-geometry", - "truck-geotrait", - "truck-polymesh", - "truck-topology", -] - -[[package]] -name = "truck-polymesh" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8f22def310af4a89c37beb4839bbf702b302d2c112ea29553759f0e16622fbf" -dependencies = [ - "array-macro", - "bytemuck", - "itertools 0.13.0", - "rustc-hash 2.1.3", - "serde", - "thiserror 1.0.69", - "truck-base", - "truck-geotrait", -] - -[[package]] -name = "truck-topology" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf62a1610405c5e39ab5eb6948f326fc1641689734a6b8207a80792133fe0e78" -dependencies = [ - "parking_lot", - "rayon", - "rclite", - "rustc-hash 2.1.3", - "serde", - "thiserror 1.0.69", - "truck-base", - "truck-geotrait", -] - [[package]] name = "ttf-parser" version = "0.25.1" diff --git a/Cargo.toml b/Cargo.toml index 89aa8f538..f331b842d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ winresource = "0.1" [features] default = ["solid3d"] -solid3d = ["dep:truck-meshalgo", "dep:lzma-sys"] +solid3d = ["dep:lzma-sys"] [dependencies] serde_json = "1" @@ -29,9 +29,6 @@ iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aa iced_aw = { git = "https://github.com/HakanSeven12/iced_aw.git", branch = "agent/fix-iced-fonts", features = ["menu", "context_menu", "color_picker", "drop_down"] } bytemuck = { version = "1.25", features = ["derive"] } glam = { version = "0.33", features = ["bytemuck"] } -truck-modeling = "0.6" -truck-meshalgo = { version = "0.4", default-features = false, features = ["tessellation"], optional = true } -truck-polymesh = "0.6" rfd = "0.17" clap = { version = "4", features = ["derive"] } env_logger = "0.11" diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 7fb701f00..c40de15e5 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -2728,18 +2728,14 @@ impl OpenCADStudio { self.tabs[i].scene.clear_preview_wire(); self.restore_pre_cmd_tangent(); } - // ── SWEEP ────────────────────────────────────────────────────── + // ── SWEEP ───────────────────────────────────────────────────── CmdResult::SweepEntity { profile_handle, path_handle, color, } => { - use crate::entities::traits::EntityTypeOps; use crate::modules::insert::solid3d_cmds::empty_solid3d; - use crate::scene::convert::acad_to_truck::TruckObject; - use crate::scene::convert::truck_tess; - use truck_modeling::builder; - use truck_modeling::Vector3 as TruckVec3; + use crate::scene::model::sweep_model; let profile_ent = self.tabs[i] .scene @@ -2747,154 +2743,24 @@ impl OpenCADStudio { .get_entity(profile_handle) .cloned(); let path_ent = self.tabs[i].scene.document.get_entity(path_handle).cloned(); + let result = profile_ent + .zip(path_ent) + .and_then(|(profile, path)| sweep_model::swept(&profile, &path, color)); - let result = profile_ent.zip(path_ent).and_then(|(prof_e, path_e)| { - let prof_truck = prof_e.to_truck_entity(&self.tabs[i].scene.document)?; - let path_truck = path_e.to_truck_entity(&self.tabs[i].scene.document)?; - - // Profile must be a wire (closed or open). - let profile_wire: truck_modeling::Wire = match prof_truck.object { - TruckObject::Contour(w) => w, - TruckObject::Curve(e) => std::iter::once(e).collect(), - _ => return None, - }; - - // Path determines the sweep operation. - let mesh = match path_truck.object { - // Linear path: translate profile along the line direction. - TruckObject::Curve(edge) => { - let p_start = edge.front().point(); - let p_end = edge.back().point(); - let dir = TruckVec3::new( - p_end.x - p_start.x, - p_end.y - p_start.y, - p_end.z - p_start.z, - ); - // Try to build a face from the profile; if it's a closed - // wire we get a Solid, otherwise a Shell. - if let Ok(face) = builder::try_attach_plane(&[profile_wire.clone()]) { - let solid = builder::tsweep(&face, dir); - match truck_tess::tessellate_solid(&solid) { - truck_tess::TruckTessResult::Mesh { - verts, - verts_low, - normals, - indices, - } => Some(crate::scene::model::mesh_model::MeshModel { - name: String::new(), - verts, - verts_low, - normals, - indices, - triangle_material_handles: Vec::new(), - triangle_colors: Vec::new(), - color, - selected: false, - }), - _ => None, - } - } else { - let shell = builder::tsweep(&profile_wire, dir); - match truck_tess::tessellate_shell(&shell) { - truck_tess::TruckTessResult::Mesh { - verts, - verts_low, - normals, - indices, - } => Some(crate::scene::model::mesh_model::MeshModel { - name: String::new(), - verts, - verts_low, - normals, - indices, - triangle_material_handles: Vec::new(), - triangle_colors: Vec::new(), - color, - selected: false, - }), - _ => None, - } - } - } - - // Contour path (polyline): sweep along the polyline using the - // first edge's direction as approximation (multi-segment sweep - // requires NURBS deformation — not supported here). - TruckObject::Contour(path_wire) => { - // Use start→end of the whole wire as translation vector. - let p_start = path_wire.front_vertex()?.point(); - let p_end = path_wire.back_vertex()?.point(); - let dir = TruckVec3::new( - p_end.x - p_start.x, - p_end.y - p_start.y, - p_end.z - p_start.z, - ); - if let Ok(face) = builder::try_attach_plane(&[profile_wire.clone()]) { - let solid = builder::tsweep(&face, dir); - match truck_tess::tessellate_solid(&solid) { - truck_tess::TruckTessResult::Mesh { - verts, - verts_low, - normals, - indices, - } => Some(crate::scene::model::mesh_model::MeshModel { - name: String::new(), - verts, - verts_low, - normals, - indices, - triangle_material_handles: Vec::new(), - triangle_colors: Vec::new(), - color, - selected: false, - }), - _ => None, - } - } else { - let shell = builder::tsweep(&profile_wire, dir); - match truck_tess::tessellate_shell(&shell) { - truck_tess::TruckTessResult::Mesh { - verts, - verts_low, - normals, - indices, - } => Some(crate::scene::model::mesh_model::MeshModel { - name: String::new(), - verts, - verts_low, - normals, - indices, - triangle_material_handles: Vec::new(), - triangle_colors: Vec::new(), - color, - selected: false, - }), - _ => None, - } - } - } - - _ => None, - }; - mesh - }); - - if let Some(mut mesh) = result { + if let Some(mut set) = result { let pending = self.begin_undo(i, "SWEEP", 1, true); - let new_entity = empty_solid3d(); - let new_handle = self.tabs[i].scene.add_entity(new_entity); - mesh.name = format!("{}", new_handle.value()); - self.tabs[i] - .scene - .meshes - .insert(new_handle, crate::scene::MeshLodSet::from_single(mesh)); + let new_handle = self.tabs[i].scene.add_entity(empty_solid3d()); + for mesh in &mut set.lods { + mesh.name = format!("{}", new_handle.value()); + } + self.tabs[i].scene.meshes.insert(new_handle, set); self.tabs[i].dirty = true; self.command_line.push_output(crate::t!("SWEEP: solid created.").as_ref()); if let Some(pd) = pending { self.commit_undo_delta(i, pd); } } else { - self.command_line.push_error(crate::t!("SWEEP: could not sweep profile along path. Use a closed 2D profile and a Line or Polyline path.").as_ref()); + self.command_line.push_error(crate::t!("SWEEP: could not sweep the profile along the path.").as_ref()); } self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; @@ -2902,96 +2768,29 @@ impl OpenCADStudio { self.restore_pre_cmd_tangent(); } - // ── LOFT ─────────────────────────────────────────────────────── + // ── LOFT ────────────────────────────────────────────────────── CmdResult::LoftEntities { handles, color } => { - use crate::entities::traits::EntityTypeOps; use crate::modules::insert::solid3d_cmds::empty_solid3d; - use crate::scene::convert::acad_to_truck::TruckObject; - use crate::scene::convert::truck_tess; - use truck_modeling::builder; + use crate::scene::model::sweep_model; - // Collect wires from each profile. - let mut wires: Vec = Vec::new(); - for h in &handles { - if let Some(ent) = self.tabs[i].scene.document.get_entity(*h).cloned() { - if let Some(te) = ent.to_truck_entity(&self.tabs[i].scene.document) { - let wire = match te.object { - TruckObject::Contour(w) => Some(w), - TruckObject::Curve(e) => Some(std::iter::once(e).collect()), - _ => None, - }; - if let Some(w) = wire { - wires.push(w); - } - } - } - } - - let result: Option = (|| { - if wires.len() < 2 { - return None; - } - - // Build ruled shells between consecutive profile pairs. - let mut all_faces: Vec = Vec::new(); - - for pair in wires.windows(2) { - let shell = builder::try_wire_homotopy(&pair[0], &pair[1]).ok()?; - for face in shell.into_iter() { - all_faces.push(face); - } - } - - // Cap the first and last profiles if they are closed. - if let Ok(cap) = builder::try_attach_plane(&[wires.first()?.clone()]) { - all_faces.push(cap); - } - if let Ok(cap) = builder::try_attach_plane(&[wires.last()?.clone()]) { - all_faces.push(cap); - } - - let shell = truck_modeling::Shell::from(all_faces); - match truck_tess::tessellate_shell(&shell) { - truck_tess::TruckTessResult::Mesh { - verts, - verts_low, - normals, - indices, - } => Some(crate::scene::model::mesh_model::MeshModel { - name: String::new(), - verts, - verts_low, - normals, - indices, - triangle_material_handles: Vec::new(), - triangle_colors: Vec::new(), - color, - selected: false, - }), - _ => None, - } - })( - ); - - if let Some(mut mesh) = result { + let profiles: Vec = handles + .iter() + .filter_map(|handle| self.tabs[i].scene.document.get_entity(*handle).cloned()) + .collect(); + if let Some(mut set) = sweep_model::lofted(&profiles, color) { let pending = self.begin_undo(i, "LOFT", 1, true); - let new_entity = empty_solid3d(); - let new_handle = self.tabs[i].scene.add_entity(new_entity); - mesh.name = format!("{}", new_handle.value()); - self.tabs[i] - .scene - .meshes - .insert(new_handle, crate::scene::MeshLodSet::from_single(mesh)); + let new_handle = self.tabs[i].scene.add_entity(empty_solid3d()); + for mesh in &mut set.lods { + mesh.name = format!("{}", new_handle.value()); + } + self.tabs[i].scene.meshes.insert(new_handle, set); self.tabs[i].dirty = true; - self.command_line.push_output(crate::tf!( - "LOFT: solid created from {} profiles.", - handles.len() - ).as_ref()); + self.command_line.push_output(crate::t!("LOFT: solid created.").as_ref()); if let Some(pd) = pending { self.commit_undo_delta(i, pd); } } else { - self.command_line.push_error(crate::t!("LOFT: could not loft profiles. Ensure sections have the same edge count and are compatible.").as_ref()); + self.command_line.push_error(crate::t!("LOFT: select at least two closed profiles.").as_ref()); } self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; diff --git a/src/app/commands/display.rs b/src/app/commands/display.rs index f6f23ef93..27463534a 100644 --- a/src/app/commands/display.rs +++ b/src/app/commands/display.rs @@ -411,7 +411,7 @@ impl OpenCADStudio { } // BOX / SPHERE / CYLINDER / CONE / WEDGE / TORUS are handled by the - // Model-tab primitive command above (with truck boolean caching). + // Model-tab primitive command above (with the kernel boolean caching). // ── EXTRUDE ──────────────────────────────────────────────────── // PRESSPULL on a closed boundary creates a solid by extruding it to a diff --git a/src/app/model_ops.rs b/src/app/model_ops.rs index c94c94d52..7600cc721 100644 --- a/src/app/model_ops.rs +++ b/src/app/model_ops.rs @@ -32,7 +32,7 @@ impl super::OpenCADStudio { /// selected solids whose B-reps are in the session cache. pub(super) fn solid_boolean(&mut self, op: BoolOp) -> Task { let i = self.active_tab; - // Selected entities that have a cached truck B-rep. + // Selected entities that have a cached B-rep. let handles: Vec = self.tabs[i] .scene .selected @@ -63,7 +63,7 @@ impl super::OpenCADStudio { 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. + // mesh + cached B-rep; its edge wires make it pickable. let mut s3d = Solid3D::new(); s3d.wires = solid_model::edge_wires(&result); let handle = self.add_solid_model(EntityType::Solid3D(s3d), result); @@ -79,7 +79,7 @@ impl super::OpenCADStudio { /// 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 truck-shapeops path as the boolean tools. + /// the same boolean path the Design-group tools use. pub(super) fn solid_slice(&mut self, axis: usize, value: f64, keep_low: bool) -> Task { let i = self.active_tab; let handles: Vec = self.tabs[i] @@ -191,7 +191,7 @@ impl super::OpenCADStudio { /// 3DROTATE — rotate the one selected solid about the X/Y/Z axis (0/1/2) /// through its centre by `angle_deg` degrees. Rotation preserves the solid's - /// orientation, so it reuses the cached truck B-rep directly. + /// orientation, so it reuses the cached B-rep directly. pub(super) fn solid_rotate3d(&mut self, axis: usize, angle_deg: f64) -> Task { let i = self.active_tab; let handles: Vec = self.tabs[i] diff --git a/src/app/update/file.rs b/src/app/update/file.rs index a8a85e3dc..ce60d66b0 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -220,14 +220,14 @@ fn plot_scene_content( } impl OpenCADStudio { - /// Before a save, give every cached truck solid that still has no ACIS + /// Before a save, give every cached solid that still has no ACIS /// geometry (EXTRUDE/REVOLVE/SWEEP/LOFT/boolean results) an exact modeler - /// body derived from its truck B-rep, so the written DWG/DXF carries real + /// body derived from its B-rep, so the written DWG/DXF carries real /// 3-D geometry other CAD apps can open instead of an empty data stream. /// Curved solids that the exact planar path can't yet express are left /// untouched (handled by the NURBS path). #[cfg(feature = "solid3d")] - fn sync_truck_solids_to_acis(&mut self, i: usize) { + fn sync_solid_models_to_acis(&mut self, i: usize) { use acadrust::EntityType; let scene = &mut self.tabs[i].scene; let targets: Vec = scene @@ -257,7 +257,7 @@ impl OpenCADStudio { } #[cfg(not(feature = "solid3d"))] - fn sync_truck_solids_to_acis(&mut self, _i: usize) {} + fn sync_solid_models_to_acis(&mut self, _i: usize) {} /// Snapshot the persisted UI preferences from live state. pub(in crate::app) fn current_settings(&self) -> crate::app::settings::UserSettings { @@ -1340,7 +1340,7 @@ pub(super) fn on_open_file(&mut self) -> Task { self.stamp_header_sysvars(i); self.tabs[i].scene.document.header.user_real1 = self.tabs[i].scene.annotation_scale as f64; - self.sync_truck_solids_to_acis(i); + self.sync_solid_models_to_acis(i); } #[cfg(target_arch = "wasm32")] @@ -2032,7 +2032,7 @@ pub(super) fn on_open_file(&mut self) -> Task { let close = self.close_save_dialog_window(); sync_annotation_scale_header(&mut self.tabs[i].scene); self.stamp_header_sysvars(i); - self.sync_truck_solids_to_acis(i); + self.sync_solid_models_to_acis(i); self.stamp_thumbnail(i, version); let mut recent_task = Task::none(); let saved = match crate::io::save_to_bytes( diff --git a/src/command.rs b/src/command.rs index e24dc1c97..b4bac5236 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1154,7 +1154,7 @@ pub enum CmdResult { /// 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 + /// persistence) plus its B-rep (cached for boolean ops + shaded /// rendering). Ends the command. CommitSolid { entity: EntityType, diff --git a/src/entities/arc.rs b/src/entities/arc.rs index f55aa8e46..8dffa04b2 100644 --- a/src/entities/arc.rs +++ b/src/entities/arc.rs @@ -1,20 +1,19 @@ use acadrust::entities::Arc; use crate::t; -use truck_modeling::{builder, Point3}; use crate::command::EntityTransform; use crate::entities::common::{ center_grip, edit_angle_prop as edit_angle, edit_prop as edit, parse_f64, ro_prop as ro, square_grip, }; -use crate::entities::traits::TruckConvertible; -use crate::scene::convert::acad_to_truck::{extrusion_wall_tris, TruckEntity, TruckObject}; +use crate::entities::traits::RenderConvertible; +use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; use crate::scene::model::wire_model::TangentGeom; const TAU: f64 = std::f64::consts::TAU; -fn to_truck(arc: &Arc) -> TruckEntity { +fn to_render(arc: &Arc) -> RenderEntity { let cx = arc.center.x; let cy = arc.center.y; let cz = arc.center.z; @@ -32,11 +31,11 @@ fn to_truck(arc: &Arc) -> TruckEntity { // Arc points in WCS: centre_wcs + r*cos(a)*Ax + r*sin(a)*Ay let arc_pt = |a: f64| { let (c, s) = (a.cos(), a.sin()); - Point3::new( + [ cwx + r * c * ax.0 + r * s * ay.0, cwy + r * c * ax.1 + r * s * ay.1, cwz + r * c * ax.2 + r * s * ay.2, - ) + ] }; // Centre, ends, arc-length midpoint and the quadrants the sweep actually @@ -58,7 +57,7 @@ fn to_truck(arc: &Arc) -> TruckEntity { let base: Vec<[f64; 3]> = (0..=n) .map(|i| { let p = arc_pt(start_a + (end_a - start_a) * (i as f64 / n as f64)); - [p.x, p.y, p.z] + [p[0], p[1], p[2]] }) .collect(); let mut pts: Vec<[f64; 3]> = Vec::with_capacity((n + 1) * 2 + 8); @@ -68,16 +67,15 @@ fn to_truck(arc: &Arc) -> TruckEntity { pts.push([x + t * nx, y + t * ny, z + t * nz]); } pts.push([f64::NAN; 3]); - let ps = arc_pt(sa); - pts.push([ps.x, ps.y, ps.z]); - pts.push([ps.x + t * nx, ps.y + t * ny, ps.z + t * nz]); - pts.push([f64::NAN; 3]); - let pe = arc_pt(ea); - pts.push([pe.x, pe.y, pe.z]); - pts.push([pe.x + t * nx, pe.y + t * ny, pe.z + t * nz]); - return TruckEntity { + for end in [arc_pt(sa), arc_pt(ea)] { + pts.push(end); + pts.push([end[0] + t * nx, end[1] + t * ny, end[2] + t * nz]); + pts.push([f64::NAN; 3]); + } + pts.pop(); + return RenderEntity { pick_tris: extrusion_wall_tris(&base, [t * nx, t * ny, t * nz]), - object: TruckObject::Lines(pts), + object: RenderObject::Lines(pts), snap_pts: snap.snap_pts.clone(), tangent_geoms: vec![tangent], key_vertices: vec![], @@ -85,20 +83,13 @@ fn to_truck(arc: &Arc) -> TruckEntity { }; } - // Kept as a truck edge rather than a point list: EXTRUDE, REVOLVE and - // SWEEP take their profile and path from `Curve` / `Contour` and have no - // arm for `Lines`, so handing them one would quietly stop an arc being - // usable as either. - let p_start = arc_pt(sa); - let p_end = arc_pt(ea); - let ccw_end = if ea >= sa { ea } else { ea + TAU }; - let p_mid = arc_pt(sa + (ccw_end - sa) * 0.5); - let v_start = builder::vertex(p_start); - let v_end = builder::vertex(p_end); - let edge = builder::circle_arc(&v_start, &v_end, p_mid); - TruckEntity { + // Sampled through the arc's own curve, which is the one definition of + // its geometry. EXTRUDE, REVOLVE and SWEEP read that definition directly + // rather than this point list, so nothing is lost by drawing from it. + let points = crate::entities::curve::curve_points(&crate::entities::curve::arc_curve(arc)); + RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Curve(edge), + object: RenderObject::Lines(points), snap_pts: snap.snap_pts.clone(), tangent_geoms: vec![tangent], key_vertices: vec![], @@ -291,9 +282,9 @@ fn apply_grip(arc: &mut Arc, grip_id: usize, apply: GripApply) { arc.center.z += d.z; } (0, GripApply::Absolute(p)) => { - arc.center.x = p.x; - arc.center.y = p.y; - arc.center.z = p.z; + arc.center.x = p[0]; + arc.center.y = p[1]; + arc.center.z = p[2]; } (1..=3, GripApply::Absolute(p)) => { let original = arc.clone(); @@ -320,9 +311,9 @@ fn apply_transform(arc: &mut Arc, t: &EntityTransform) { }); } -impl TruckConvertible for Arc { - fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { - Some(to_truck(self)) +impl RenderConvertible for Arc { + fn to_render(&self, _document: &acadrust::CadDocument) -> Option { + Some(to_render(self)) } } diff --git a/src/entities/attribute.rs b/src/entities/attribute.rs index b0910983d..d74c8b1a3 100644 --- a/src/entities/attribute.rs +++ b/src/entities/attribute.rs @@ -10,8 +10,8 @@ use crate::entities::text_support::{ layout_mtext, resolve_dxf_special_chars, resolve_text_style, text_local_bounds, MTextRenderOpts, MTextVAnchor, ResolvedTextStyle, }; -use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; -use crate::scene::convert::acad_to_truck::{GlyphRun, TextStroke, TruckEntity, TruckObject}; +use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible}; +use crate::scene::convert::acad_to_render::{GlyphRun, TextStroke, RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::model::wire_model::SnapHint; use crate::scene::text::lff; @@ -20,7 +20,7 @@ use crate::t; // ── Shared helpers ──────────────────────────────────────────────────────────── -/// Bundle of the fields both attribute kinds carry. Lets the truck builder +/// Bundle of the fields both attribute kinds carry. Lets the render builder /// stay generic over ATTDEF vs ATTRIB. struct AttrTextInputs<'a> { value: &'a str, @@ -101,7 +101,7 @@ fn mtext_flag_str(f: MTextFlag) -> &'static str { /// Render text strokes for an attribute, honouring alignment, oblique angle, /// width factor, generation flags (backward / upside-down), text-style /// resolution, and basic multiline splitting on `\n` / `\\P`. -fn build_attr_truck(input: AttrTextInputs<'_>, document: &acadrust::CadDocument) -> TruckEntity { +fn build_attr_render(input: AttrTextInputs<'_>, document: &acadrust::CadDocument) -> RenderEntity { let normal = (input.normal.x, input.normal.y, input.normal.z); let (wsx, wsy, wsz) = transform::ocs_point_to_wcs( ( @@ -218,9 +218,9 @@ fn build_attr_truck(input: AttrTextInputs<'_>, document: &acadrust::CadDocument) }); let _ = input.line_count; let _ = input.is_multiline; - return TruckEntity { + return RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Text(layout.strokes), + object: RenderObject::Text(layout.strokes), snap_pts: vec![(snap_pt, SnapHint::Insertion)], tangent_geoms: vec![], key_vertices: vec![], @@ -321,9 +321,9 @@ fn build_attr_truck(input: AttrTextInputs<'_>, document: &acadrust::CadDocument) } let _ = input.line_count; // round-trip only — recomputed above - TruckEntity { + RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Text(strokes_all), + object: RenderObject::Text(strokes_all), snap_pts: vec![(snap_pt, SnapHint::Insertion)], tangent_geoms: vec![], key_vertices: vec![], @@ -333,8 +333,8 @@ fn build_attr_truck(input: AttrTextInputs<'_>, document: &acadrust::CadDocument) // ── AttributeDefinition ─────────────────────────────────────────────────────── -impl TruckConvertible for AttributeDefinition { - fn to_truck(&self, document: &acadrust::CadDocument) -> Option { +impl RenderConvertible for AttributeDefinition { + fn to_render(&self, document: &acadrust::CadDocument) -> Option { // An attribute definition previews its tag when it has no default // value, so the placeholder is visible where a block will prompt for // input. (Passed as a non-empty value, so it renders as-is.) @@ -343,7 +343,7 @@ impl TruckConvertible for AttributeDefinition { } else { self.default_value.clone() }; - Some(build_attr_truck( + Some(build_attr_render( AttrTextInputs { value: &display_value, insertion_point: self.insertion_point, @@ -622,9 +622,9 @@ impl Transformable for AttributeDefinition { // ── AttributeEntity ─────────────────────────────────────────────────────────── -impl TruckConvertible for AttributeEntity { - fn to_truck(&self, document: &acadrust::CadDocument) -> Option { - Some(build_attr_truck( +impl RenderConvertible for AttributeEntity { + fn to_render(&self, document: &acadrust::CadDocument) -> Option { + Some(build_attr_render( AttrTextInputs { value: &self.value, insertion_point: self.insertion_point, diff --git a/src/entities/circle.rs b/src/entities/circle.rs index cfc65bcb1..6e292770b 100644 --- a/src/entities/circle.rs +++ b/src/entities/circle.rs @@ -5,12 +5,12 @@ use crate::command::EntityTransform; use crate::entities::common::{ center_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip, }; -use crate::entities::traits::TruckConvertible; -use crate::scene::convert::acad_to_truck::{extrusion_wall_tris, TruckEntity, TruckObject}; +use crate::entities::traits::RenderConvertible; +use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; use crate::scene::model::wire_model::TangentGeom; -fn to_truck(circle: &Circle) -> TruckEntity { +fn to_render(circle: &Circle) -> RenderEntity { let cx = circle.center.x; let cy = circle.center.y; let cz = circle.center.z; @@ -64,9 +64,9 @@ fn to_truck(circle: &Circle) -> TruckEntity { pts.push([f64::NAN; 3]); } } - return TruckEntity { + return RenderEntity { pick_tris: extrusion_wall_tris(&base, [t * nx, t * ny, t * nz]), - object: TruckObject::Lines(pts), + object: RenderObject::Lines(pts), snap_pts, tangent_geoms: vec![tangent], key_vertices: vec![], @@ -74,7 +74,7 @@ fn to_truck(circle: &Circle) -> TruckEntity { }; } - // Sampled from the entity's own curve rather than as a truck + // Sampled from the entity's own curve rather than as a // `circle_arc` (arc-through-three-points). At large WCS coordinates // (e.g. −1.2M UTM) the three-point fit cancels catastrophically — the // circle comes back with a ~3% radius wobble and uneven segment lengths, @@ -84,9 +84,9 @@ fn to_truck(circle: &Circle) -> TruckEntity { // double-single the shader reconstructs. let pts = crate::entities::curve::curve_points(&curve); - TruckEntity { + RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Lines(pts), + object: RenderObject::Lines(pts), snap_pts, tangent_geoms: vec![tangent], key_vertices: vec![], @@ -175,9 +175,9 @@ fn apply_transform(circle: &mut Circle, t: &EntityTransform) { }); } -impl TruckConvertible for Circle { - fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { - Some(to_truck(self)) +impl RenderConvertible for Circle { + fn to_render(&self, _document: &acadrust::CadDocument) -> Option { + Some(to_render(self)) } } diff --git a/src/entities/common.rs b/src/entities/common.rs index 573c75319..1a7339c31 100644 --- a/src/entities/common.rs +++ b/src/entities/common.rs @@ -590,7 +590,7 @@ pub fn parse_angle_deg(value: &str) -> Option { pub use acadrust::kernel::geom2d::BulgeArc; /// Triangulate the solid bands a `wide_fills` returns into the flat WCS f64 -/// triangle list `TruckEntity::pick_tris` carries, so a wide polyline is +/// triangle list `RenderEntity::pick_tris` carries, so a wide polyline is /// selectable across the band it draws and not just along its centreline. /// /// `origin` and `fills` are that function's own pair: 2-D offsets from the @@ -616,7 +616,7 @@ pub(crate) fn wide_band_tris(origin: [f64; 2], fills: &[Vec<[f32; 2]>]) -> Vec<[ /// thickness (code 39): a vertical wall between every band-boundary point and /// its `thickness`-along-`normal` copy, plus triangulated bottom and top caps. /// Returns `(fill_tris, edge_lines)` as flat WCS f64 lists — the caller wraps -/// them in a `TruckEntity` (object = `Lines(edge_lines)`, `fill_tris`, and +/// them in a `RenderEntity` (object = `Lines(edge_lines)`, `fill_tris`, and /// `pick_tris = fill_tris`). Shared by LwPolyline and Polyline2D so both wide /// polyline kinds extrude the same solid instead of just their centre-line. /// diff --git a/src/entities/curve.rs b/src/entities/curve.rs index 1d2e1f1be..2b7ddd92f 100644 --- a/src/entities/curve.rs +++ b/src/entities/curve.rs @@ -344,7 +344,7 @@ pub fn entity_curve_xy(entity: &EntityType) -> Option { /// pair; casting local coordinates on the way in throws away precision the /// shader was built to reconstruct. pub fn curve_points(curve: &PlanarCurve) -> Vec<[f64; 3]> { - curve.tessellate_within(crate::scene::convert::truck_tess::current_curve_tol()) + curve.tessellate_within(crate::scene::convert::curve_tol::current_curve_tol()) } /// The snap candidates an entity's curve offers, in the two channels the diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index 845f920a8..2dea32cbd 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -1468,7 +1468,7 @@ fn tessellate_dimension_inner( line_weight_px: f32, anno_scale: f32, // LOD hints — when present, synthesised dim text routes through the - // top-level LOD ladder (baseline / greek / full) instead of the truck + // top-level LOD ladder (baseline / greek / full) instead of the render // path so far-out drawings collapse to a colored rect or baseline. selected_set: &rustc_hash::FxHashSet, active_viewport: Option, @@ -1478,7 +1478,7 @@ fn tessellate_dimension_inner( ) -> Vec { let name = handle.value().to_string(); // (Baked-block fast path moved up into scene::tessellate_entity so the - // recursive call goes through the LOD ladder, not the truck path.) + // recursive call goes through the LOD ladder, not the kernel path.) let style_name = &dim.base().style_name; let source_style = document.dim_styles.iter().find(|s| { diff --git a/src/entities/ellipse.rs b/src/entities/ellipse.rs index 8363d0da8..aca22680d 100644 --- a/src/entities/ellipse.rs +++ b/src/entities/ellipse.rs @@ -1,5 +1,4 @@ use acadrust::entities::Ellipse; -use truck_modeling::{builder, BSplineCurve, Curve, Edge, KnotVec, Point3, Wire}; use crate::entities::curve::CurveSnap; use crate::t; @@ -8,11 +7,11 @@ use crate::command::EntityTransform; use crate::entities::common::{ center_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip, }; -use crate::entities::traits::TruckConvertible; -use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; +use crate::entities::traits::RenderConvertible; +use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; -fn to_truck(ell: &Ellipse) -> TruckEntity { +fn to_render(ell: &Ellipse) -> RenderEntity { // ELLIPSE is one of the few WCS entities in DXF: `center` (code 10) and // `major_axis` (code 11) are world coordinates already — unlike ARC / // CIRCLE, whose centers are OCS. The converter knows that; this used to @@ -35,9 +34,9 @@ fn to_truck(ell: &Ellipse) -> TruckEntity { // SWEEP read their profile out of `Contour` / `Curve` and have no arm for // a bare point list, so an ellipse handed over as `Lines` would stop // being usable as either. - let empty = |snap: CurveSnap| TruckEntity { + let empty = |snap: CurveSnap| RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Lines(Vec::new()), + object: RenderObject::Lines(Vec::new()), snap_pts: snap.snap_pts, tangent_geoms: vec![], key_vertices: vec![], @@ -46,42 +45,14 @@ fn to_truck(ell: &Ellipse) -> TruckEntity { let Some(planar) = curve else { return empty(snap); }; - let points: Vec = crate::entities::curve::curve_points(&planar) - .into_iter() - .map(|p| Point3::new(p[0], p[1], p[2])) - .collect(); + let points = crate::entities::curve::curve_points(&planar); if points.len() < 2 { return empty(snap); } - // Closure decided on the points rather than on the stored parameters. A - // file that writes an end parameter a hair short of a full turn still - // draws a closed ellipse, and an open edge whose two vertices sit on top - // of each other is not something a plane can be attached to. - let ends_meet = { - let (first, last) = (points[0], *points.last().expect("checked non-empty")); - let span = first.x.abs().max(first.y.abs()).max(first.z.abs()).max(1.0); - let gap = first - last; - (gap.x * gap.x + gap.y * gap.y + gap.z * gap.z).sqrt() <= 1e-9 * span - }; - let object = if planar.is_closed() || ends_meet { - // Two half-edges rather than one closed edge: a single edge whose - // two vertices coincide is not a wire truck will attach a plane to. - let split = points.len() / 2; - let upper = points[..=split].to_vec(); - let lower = points[split..].to_vec(); - let v_pos = builder::vertex(points[0]); - let v_neg = builder::vertex(points[split]); - let edge_upper = Edge::new(&v_pos, &v_neg, polyline_curve(upper)); - let edge_lower = Edge::new(&v_neg, &v_pos, polyline_curve(lower)); - TruckObject::Contour([edge_upper, edge_lower].into_iter().collect::()) - } else { - let v_start = builder::vertex(points[0]); - let v_end = builder::vertex(*points.last().expect("checked non-empty")); - TruckObject::Curve(Edge::new(&v_start, &v_end, polyline_curve(points))) - }; + let object = RenderObject::Lines(points); - TruckEntity { + RenderEntity { pick_tris: Vec::new(), object, snap_pts: snap.snap_pts, @@ -91,15 +62,11 @@ fn to_truck(ell: &Ellipse) -> TruckEntity { } } -/// A truck curve that passes through every one of `points` in order. +/// A curve through every one of `points` in order. /// /// Degree one, so the control polygon *is* the curve — the same thing the /// hand-built sampling produced, without restating how a B-spline is put /// together at each site. -fn polyline_curve(points: Vec) -> Curve { - let spans = points.len().saturating_sub(1); - Curve::BSplineCurve(BSplineCurve::new(KnotVec::uniform_knot(1, spans), points)) -} fn grips(ell: &Ellipse) -> Vec { let ctr = glam::DVec3::new(ell.center.x, ell.center.y, ell.center.z); @@ -302,9 +269,9 @@ fn apply_transform(ell: &mut Ellipse, t: &EntityTransform) { }); } -impl TruckConvertible for Ellipse { - fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { - Some(to_truck(self)) +impl RenderConvertible for Ellipse { + fn to_render(&self, _document: &acadrust::CadDocument) -> Option { + Some(to_render(self)) } } diff --git a/src/entities/extended.rs b/src/entities/extended.rs index 80cb8aca5..29b5ad113 100644 --- a/src/entities/extended.rs +++ b/src/entities/extended.rs @@ -9,8 +9,8 @@ use crate::command::EntityTransform; use crate::entities::common::{ center_grip, edit_angle_prop, edit_prop, parse_f64, ro_prop, square_grip, }; -use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; -use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; +use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible}; +use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject}; use crate::scene::model::object::{ GripApply, GripDef, PropSection, PropValue, Property, }; @@ -373,7 +373,7 @@ fn camera_lines(document: &acadrust::CadDocument, view_handle: Handle) -> Vec<[f points } -fn to_truck(entity: &ExtendedEntity, document: &acadrust::CadDocument) -> Option { +fn to_render(entity: &ExtendedEntity, document: &acadrust::CadDocument) -> Option { let (points, snaps, keys): ( Vec<[f64; 3]>, Vec<(glam::DVec3, SnapHint)>, @@ -455,8 +455,8 @@ fn to_truck(entity: &ExtendedEntity, document: &acadrust::CadDocument) -> Option if points.len() < 2 { return None; } - Some(TruckEntity { - object: TruckObject::Lines(points), + Some(RenderEntity { + object: RenderObject::Lines(points), snap_pts: snaps, tangent_geoms: Vec::new(), key_vertices: keys, @@ -1520,9 +1520,9 @@ fn apply_transform(entity: &mut ExtendedEntity, requested: &EntityTransform) { } } -impl TruckConvertible for ExtendedEntity { - fn to_truck(&self, document: &acadrust::CadDocument) -> Option { - to_truck(self, document) +impl RenderConvertible for ExtendedEntity { + fn to_render(&self, document: &acadrust::CadDocument) -> Option { + to_render(self, document) } } diff --git a/src/entities/helix.rs b/src/entities/helix.rs index 13ef7f152..2e5bf7d1c 100644 --- a/src/entities/helix.rs +++ b/src/entities/helix.rs @@ -11,8 +11,8 @@ use crate::t; use crate::command::EntityTransform; use crate::entities::common::ro_prop as ro; -use crate::entities::traits::{Grippable, Transformable, TruckConvertible}; -use crate::scene::convert::acad_to_truck::TruckEntity; +use crate::entities::traits::{Grippable, Transformable, RenderConvertible}; +use crate::scene::convert::acad_to_render::RenderEntity; use crate::scene::model::object::{GripApply, GripDef, PropSection}; /// Perpendicular distance from `p` to the helix axis (line through @@ -103,9 +103,9 @@ fn apply_transform(helix: &mut Helix, t: &EntityTransform) { Transformable::apply_transform(&mut helix.spline, t); } -impl TruckConvertible for Helix { - fn to_truck(&self, document: &acadrust::CadDocument) -> Option { - self.spline.to_truck(document) +impl RenderConvertible for Helix { + fn to_render(&self, document: &acadrust::CadDocument) -> Option { + self.spline.to_render(document) } } diff --git a/src/entities/leader.rs b/src/entities/leader.rs index 450ec9796..4fb4409b2 100644 --- a/src/entities/leader.rs +++ b/src/entities/leader.rs @@ -6,13 +6,13 @@ use crate::command::EntityTransform; use crate::entities::common::{ center_grip, edit_prop as edit, ro_prop as ro, square_grip, stepper_prop as stepper, }; -use crate::entities::traits::TruckConvertible; -use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; +use crate::entities::traits::RenderConvertible; +use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::model::wire_model::TangentGeom; use crate::t; -// ── TruckConvertible (used for snap/grip key-vertices) ───────────────────── +// ── RenderConvertible (used for snap/grip key-vertices) ───────────────────── /// Whether a leader draws a horizontal hookline. DWG stores no "hookline /// exists" flag (only the direction bit), so the stored flag alone would drop @@ -44,7 +44,7 @@ fn draws_hookline(leader: &Leader) -> bool { cos < (15.0f64).to_radians().cos() } -fn to_truck(leader: &Leader) -> TruckEntity { +fn to_render(leader: &Leader) -> RenderEntity { let verts = &leader.vertices; let nan = [f64::NAN; 3]; let p3 = |v: &acadrust::types::Vector3| -> [f64; 3] { [v.x, v.y, v.z] }; @@ -126,9 +126,9 @@ fn to_truck(leader: &Leader) -> TruckEntity { ]); } - TruckEntity { + RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Lines(points), + object: RenderObject::Lines(points), snap_pts: vec![], tangent_geoms: tangents, key_vertices: key_verts, @@ -459,12 +459,12 @@ fn apply_transform(leader: &mut Leader, t: &EntityTransform) { // ── Trait impls ──────────────────────────────────────────────────────────── -impl TruckConvertible for Leader { - fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { +impl RenderConvertible for Leader { + fn to_render(&self, _document: &acadrust::CadDocument) -> Option { if self.vertices.is_empty() { return None; } - Some(to_truck(self)) + Some(to_render(self)) } } diff --git a/src/entities/light.rs b/src/entities/light.rs index 7467bd84f..ee69a9549 100644 --- a/src/entities/light.rs +++ b/src/entities/light.rs @@ -6,7 +6,7 @@ //! same way AutoCAD shows a light symbol. //! //! The glyph is sized in **screen space** (a roughly constant pixel size across -//! zoom) via [`relative_truck`], falling back to a small fixed world size when +//! zoom) via [`relative_render`], falling back to a small fixed world size when //! no world-per-pixel factor is available (e.g. snapshot tessellation). use acadrust::entities::Light; @@ -17,8 +17,8 @@ use crate::command::EntityTransform; use crate::entities::common::{ center_grip, edit_angle_prop, edit_prop as edit, ro_prop, square_grip, }; -use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; -use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; +use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible}; +use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::model::wire_model::SnapHint; @@ -165,7 +165,7 @@ fn light_wire(l: &Light, r: f64) -> Vec<[f64; 3]> { out } -fn build(l: &Light, r: f64) -> TruckEntity { +fn build(l: &Light, r: f64) -> RenderEntity { let p = pos(l); let snap = glam::DVec3::new(p[0], p[1], p[2]); let mut snap_pts = vec![(snap, SnapHint::Node)]; @@ -175,9 +175,9 @@ fn build(l: &Light, r: f64) -> TruckEntity { snap_pts.push((glam::DVec3::new(t[0], t[1], t[2]), SnapHint::Node)); key.push(t); } - TruckEntity { + RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Lines(light_wire(l, r)), + object: RenderObject::Lines(light_wire(l, r)), snap_pts, tangent_geoms: vec![], key_vertices: key, @@ -187,12 +187,12 @@ fn build(l: &Light, r: f64) -> TruckEntity { /// Viewport-aware glyph: constant on-screen size from the world-per-pixel /// factor. Returns `None` for a non-light entity or when no factor is -/// available (the caller then falls back to the fixed-world [`to_truck`]). -pub fn relative_truck( +/// available (the caller then falls back to the fixed-world [`to_render`]). +pub fn relative_render( entity: &EntityType, _document: &acadrust::CadDocument, wpp: Option, -) -> Option { +) -> Option { let EntityType::Light(l) = entity else { return None; }; @@ -200,8 +200,8 @@ pub fn relative_truck( Some(build(l, w * GLYPH_PX)) } -impl TruckConvertible for Light { - fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { +impl RenderConvertible for Light { + fn to_render(&self, _document: &acadrust::CadDocument) -> Option { Some(build(self, FIXED_WORLD)) } } diff --git a/src/entities/line.rs b/src/entities/line.rs index 039e7b00c..cab1605c5 100644 --- a/src/entities/line.rs +++ b/src/entities/line.rs @@ -1,17 +1,16 @@ use acadrust::{entities::Line, Entity}; use crate::t; -use truck_modeling::{builder, Point3}; use crate::command::EntityTransform; use crate::entities::common::{ center_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip, }; -use crate::entities::traits::TruckConvertible; -use crate::scene::convert::acad_to_truck::{extrusion_wall_tris, TruckEntity, TruckObject}; +use crate::entities::traits::RenderConvertible; +use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; use crate::scene::model::wire_model::TangentGeom; -fn to_truck(line: &Line) -> TruckEntity { +fn to_render(line: &Line) -> RenderEntity { // LINE endpoints are stored in WCS — unlike the planar OCS entities // (ARC/CIRCLE/LWPOLYLINE/TEXT), the extrusion normal on a LINE only // orients its thickness sweep. Remapping the endpoints through the @@ -20,9 +19,7 @@ fn to_truck(line: &Line) -> TruckEntity { let normal = (line.normal.x, line.normal.y, line.normal.z); let (sx, sy, sz) = (line.start.x, line.start.y, line.start.z); let (ex, ey, ez) = (line.end.x, line.end.y, line.end.z); - let p0 = Point3::new(sx, sy, sz); - let p1 = Point3::new(ex, ey, ez); - let kv: Vec<[f64; 3]> = vec![[p0.x, p0.y, p0.z], [p1.x, p1.y, p1.z]]; + let kv: Vec<[f64; 3]> = vec![[sx, sy, sz], [ex, ey, ez]]; let tangent = TangentGeom::Line { p1: [kv[0][0] as f32, kv[0][1] as f32, kv[0][2] as f32], p2: [kv[1][0] as f32, kv[1][1] as f32, kv[1][2] as f32], @@ -46,9 +43,9 @@ fn to_truck(line: &Line) -> TruckEntity { kv[1], p1t, ]; - return TruckEntity { + return RenderEntity { pick_tris: extrusion_wall_tris(&kv, [t * nx, t * ny, t * nz]), - object: TruckObject::Lines(pts), + object: RenderObject::Lines(pts), snap_pts: vec![], tangent_geoms: vec![tangent], key_vertices: kv, @@ -56,12 +53,9 @@ fn to_truck(line: &Line) -> TruckEntity { }; } - let v0 = builder::vertex(p0); - let v1 = builder::vertex(p1); - let edge = builder::line(&v0, &v1); - TruckEntity { + RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Curve(edge), + object: RenderObject::Lines(kv.clone()), snap_pts: vec![], tangent_geoms: vec![tangent], key_vertices: kv, @@ -167,9 +161,9 @@ fn apply_transform(line: &mut Line, t: &EntityTransform) { } } -impl TruckConvertible for Line { - fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { - Some(to_truck(self)) +impl RenderConvertible for Line { + fn to_render(&self, _document: &acadrust::CadDocument) -> Option { + Some(to_render(self)) } } diff --git a/src/entities/lwpolyline.rs b/src/entities/lwpolyline.rs index a62620e17..f3c1e12ed 100644 --- a/src/entities/lwpolyline.rs +++ b/src/entities/lwpolyline.rs @@ -1,14 +1,13 @@ use acadrust::entities::{LwPolyline, LwVertex}; use crate::t; -use truck_modeling::{builder, Edge, Point3, Wire}; use crate::command::EntityTransform; use crate::entities::common::{ edit_prop as edit, parse_f64, rectangle_grip, ro_prop as ro, square_grip, stepper_prop as stepper, }; -use crate::entities::traits::TruckConvertible; -use crate::scene::convert::acad_to_truck::{extrusion_wall_tris, TruckEntity, TruckObject}; +use crate::entities::traits::RenderConvertible; +use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::model::wire_model::TangentGeom; @@ -179,7 +178,7 @@ fn thick_segments( normal: (f64, f64, f64), key_verts: Vec<[f64; 3]>, tangents: Vec, -) -> TruckEntity { +) -> RenderEntity { let (nx, ny, nz) = normal; let t = thickness; let off = |p: [f64; 3]| -> [f64; 3] { [p[0] + t * nx, p[1] + t * ny, p[2] + t * nz] }; @@ -210,9 +209,9 @@ fn thick_segments( pts.push(off(last)); } } - TruckEntity { + RenderEntity { pick_tris: extrusion_wall_tris(path_pts, [t * nx, t * ny, t * nz]), - object: TruckObject::Lines(pts), + object: RenderObject::Lines(pts), snap_pts: vec![], tangent_geoms: tangents, key_vertices: key_verts, @@ -233,14 +232,14 @@ fn thick_wide_band( normal: (f64, f64, f64), key_verts: Vec<[f64; 3]>, tangents: Vec, -) -> TruckEntity { +) -> RenderEntity { let (origin, fills) = wide_fills(pl); let (fill_tris, lines) = crate::entities::common::thick_band_tube(origin, &fills, thickness, normal, to_wcs); - TruckEntity { + RenderEntity { pick_tris: fill_tris.clone(), - object: TruckObject::Lines(lines), + object: RenderObject::Lines(lines), snap_pts: vec![], tangent_geoms: tangents, key_vertices: key_verts, @@ -248,12 +247,12 @@ fn thick_wide_band( } } -fn to_truck(pline: &LwPolyline) -> TruckEntity { +fn to_render(pline: &LwPolyline) -> RenderEntity { let verts = &pline.vertices; if verts.is_empty() { - return TruckEntity { + return RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Point(builder::vertex(Point3::new(0.0, 0.0, 0.0))), + object: RenderObject::Lines(Vec::new()), snap_pts: vec![], tangent_geoms: vec![], key_vertices: vec![], @@ -265,17 +264,16 @@ fn to_truck(pline: &LwPolyline) -> TruckEntity { let normal = (pline.normal.x, pline.normal.y, pline.normal.z); let count = verts.len(); let seg_count = if pline.is_closed { count } else { count - 1 }; - let mut edges: Vec = Vec::new(); let mut tangents: Vec = Vec::new(); let mut key_verts: Vec<[f64; 3]> = Vec::new(); - // Convert OCS (x, y, elevation) to WCS Point3. + // Convert OCS (x, y, elevation) to a WCS point. let to_wcs = |x: f64, y: f64| -> (f64, f64, f64) { crate::scene::view::transform::ocs_point_to_wcs((x, y, elev), normal) }; - let to_pt = |v: &LwVertex| -> Point3 { + let to_pt = |v: &LwVertex| -> [f64; 3] { let (wx, wy, wz) = to_wcs(v.location.x, v.location.y); - Point3::new(wx, wy, wz) + [wx, wy, wz] }; if pline.thickness.abs() > 1e-10 { @@ -345,13 +343,13 @@ fn to_truck(pline: &LwPolyline) -> TruckEntity { let p0 = to_pt(v0); let p1 = to_pt(v1); if i == 0 { - kv.push([p0.x, p0.y, p0.z]); + kv.push([p0[0], p0[1], p0[2]]); } - kv.push([p1.x, p1.y, p1.z]); + kv.push([p1[0], p1[1], p1[2]]); if v0.bulge.abs() < 1e-9 { tgs.push(TangentGeom::Line { - p1: [p0.x as f32, p0.y as f32, p0.z as f32], - p2: [p1.x as f32, p1.y as f32, p1.z as f32], + p1: [p0[0] as f32, p0[1] as f32, p0[2] as f32], + p2: [p1[0] as f32, p1[1] as f32, p1[2] as f32], }); } else if let Some(arc) = crate::entities::common::BulgeArc::from_bulge( [v0.location.x, v0.location.y], @@ -368,9 +366,9 @@ fn to_truck(pline: &LwPolyline) -> TruckEntity { let (pts, widths) = crate::entities::common::tapered_band_points(&band_verts, pline.is_closed, &to_wcs); let (fill_origin, fills) = wide_fills(pline); - return TruckEntity { + return RenderEntity { pick_tris: crate::entities::common::wide_band_tris(fill_origin, &fills), - object: TruckObject::TaperedLines(pts, widths), + object: RenderObject::TaperedLines(pts, widths), snap_pts: vec![], tangent_geoms: tgs, key_vertices: kv, @@ -426,9 +424,9 @@ fn to_truck(pline: &LwPolyline) -> TruckEntity { } } let (fill_origin, fills) = wide_fills(pline); - return TruckEntity { + return RenderEntity { pick_tris: crate::entities::common::wide_band_tris(fill_origin, &fills), - object: TruckObject::SegmentedLines(pts), + object: RenderObject::SegmentedLines(pts), snap_pts: vec![], tangent_geoms: tgs, key_vertices: kv, @@ -444,24 +442,15 @@ fn to_truck(pline: &LwPolyline) -> TruckEntity { let bulge = v0.bulge; if bulge.abs() < 1e-9 { - let tv0 = builder::vertex(p0); - let tv1 = builder::vertex(p1); - edges.push(builder::line(&tv0, &tv1)); tangents.push(TangentGeom::Line { - p1: [p0.x as f32, p0.y as f32, p0.z as f32], - p2: [p1.x as f32, p1.y as f32, p1.z as f32], + p1: [p0[0] as f32, p0[1] as f32, p0[2] as f32], + p2: [p1[0] as f32, p1[1] as f32, p1[2] as f32], }); } else if let Some(arc) = crate::entities::common::BulgeArc::from_bulge( [v0.location.x, v0.location.y], [v1.location.x, v1.location.y], bulge as f64, ) { - let mid_s = arc.sample(0.5); - let (mid_wx, mid_wy, mid_wz) = to_wcs(mid_s[0], mid_s[1]); - let p_mid = Point3::new(mid_wx, mid_wy, mid_wz); - let tv0 = builder::vertex(p0); - let tv1 = builder::vertex(p1); - edges.push(builder::circle_arc(&tv0, &tv1, p_mid)); let (wcx, wcy, wcz) = to_wcs(arc.center[0], arc.center[1]); tangents.push(TangentGeom::Circle { center: [wcx as f32, wcy as f32, wcz as f32], @@ -470,15 +459,21 @@ fn to_truck(pline: &LwPolyline) -> TruckEntity { } if i == 0 { - key_verts.push([p0.x, p0.y, p0.z]); + key_verts.push([p0[0], p0[1], p0[2]]); } - key_verts.push([p1.x, p1.y, p1.z]); + key_verts.push([p1[0], p1[1], p1[2]]); } let (fill_origin, fills) = wide_fills(pline); - TruckEntity { + RenderEntity { pick_tris: crate::entities::common::wide_band_tris(fill_origin, &fills), - object: TruckObject::Contour(edges.into_iter().collect::()), + // Sampled through the polyline's own curve, so a bulge stays an + // arc rather than becoming the chord across it. + object: RenderObject::Lines( + crate::entities::curve::lwpolyline_curve(pline) + .map(|planar| crate::entities::curve::curve_points(&planar)) + .unwrap_or_default(), + ), snap_pts: vec![], tangent_geoms: tangents, key_vertices: key_verts, @@ -805,9 +800,9 @@ fn apply_transform(pline: &mut LwPolyline, t: &EntityTransform) { }); } -impl TruckConvertible for LwPolyline { - fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { - Some(to_truck(self)) +impl RenderConvertible for LwPolyline { + fn to_render(&self, _document: &acadrust::CadDocument) -> Option { + Some(to_render(self)) } } diff --git a/src/entities/mesh.rs b/src/entities/mesh.rs index 656c3bd34..726087caa 100644 --- a/src/entities/mesh.rs +++ b/src/entities/mesh.rs @@ -4,8 +4,8 @@ use crate::t; use crate::command::EntityTransform; use crate::entities::common::{parse_f64, ro_prop as ro, square_grip}; -use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; -use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; +use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible}; +use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::model::wire_model::SnapHint; @@ -151,8 +151,8 @@ fn v3f32(v: &acadrust::types::Vector3) -> [f32; 3] { [v.x as f32, v.y as f32, v.z as f32] } -impl TruckConvertible for Face3D { - fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { +impl RenderConvertible for Face3D { + fn to_render(&self, _document: &acadrust::CadDocument) -> Option { let p0 = v3(&self.first_corner); let p1 = v3(&self.second_corner); let p2 = v3(&self.third_corner); @@ -195,9 +195,9 @@ impl TruckConvertible for Face3D { pts = vec![[cx - s, cy, cz], [cx + s, cy, cz]]; } - Some(TruckEntity { + Some(RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Lines(pts), + object: RenderObject::Lines(pts), snap_pts: vec![ (Vec3::from(p0f).as_dvec3(), SnapHint::Node), (Vec3::from(p1f).as_dvec3(), SnapHint::Node), @@ -312,17 +312,17 @@ impl Transformable for Face3D { // ── PolygonMesh (N×M grid) ──────────────────────────────────────────────────── -impl TruckConvertible for PolygonMesh { - fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { +impl RenderConvertible for PolygonMesh { + fn to_render(&self, _document: &acadrust::CadDocument) -> Option { let m = self.m_vertex_count as usize; let n = self.n_vertex_count as usize; if m == 0 || n == 0 || self.vertices.len() < m * n { return None; } - Some(TruckEntity { + Some(RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Lines(Vec::new()), + object: RenderObject::Lines(Vec::new()), snap_pts: vec![], tangent_geoms: vec![], key_vertices: vec![], @@ -433,15 +433,15 @@ impl Transformable for PolygonMesh { // ── PolyfaceMesh (arbitrary faces with 1-based vertex indices) ──────────────── -impl TruckConvertible for PolyfaceMesh { - fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { +impl RenderConvertible for PolyfaceMesh { + fn to_render(&self, _document: &acadrust::CadDocument) -> Option { if self.vertices.is_empty() || self.faces.is_empty() { return None; } - Some(TruckEntity { + Some(RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Lines(Vec::new()), + object: RenderObject::Lines(Vec::new()), snap_pts: vec![], tangent_geoms: vec![], key_vertices: vec![], @@ -1064,8 +1064,8 @@ pub(crate) fn tessellate_shaded_mesh( } } -impl TruckConvertible for Mesh { - fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { +impl RenderConvertible for Mesh { + fn to_render(&self, _document: &acadrust::CadDocument) -> Option { if self.vertices.is_empty() { return None; } @@ -1088,9 +1088,9 @@ impl TruckConvertible for Mesh { ) }; - Some(TruckEntity { + Some(RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Lines(Vec::new()), + object: RenderObject::Lines(Vec::new()), snap_pts, tangent_geoms: vec![], key_vertices, diff --git a/src/entities/mline.rs b/src/entities/mline.rs index 05c325e05..7c299e03a 100644 --- a/src/entities/mline.rs +++ b/src/entities/mline.rs @@ -3,8 +3,8 @@ use crate::t; use crate::command::EntityTransform; use crate::entities::common::{edit_prop as edit, ro_prop as ro, square_grip}; -use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; -use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; +use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible}; +use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::model::wire_model::SnapHint; @@ -204,8 +204,8 @@ pub fn mline_lines(m: &MLine, document: &acadrust::CadDocument) -> Vec Option { +impl RenderConvertible for MLine { + fn to_render(&self, document: &acadrust::CadDocument) -> Option { if self.vertices.is_empty() { return None; } @@ -239,9 +239,9 @@ impl TruckConvertible for MLine { }) .collect(); - Some(TruckEntity { + Some(RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Lines(pts), + object: RenderObject::Lines(pts), snap_pts, tangent_geoms: vec![], key_vertices: key_verts, diff --git a/src/entities/mtext.rs b/src/entities/mtext.rs index 0abf0adef..14796ac4f 100644 --- a/src/entities/mtext.rs +++ b/src/entities/mtext.rs @@ -7,8 +7,8 @@ use crate::entities::common::{ use crate::entities::text_support::{ layout_mtext, resolve_text_style, GlyphBox, MTextColumns, MTextRenderOpts, MTextVAnchor, }; -use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; -use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; +use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible}; +use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::model::wire_model::SnapHint; use crate::t; @@ -70,7 +70,7 @@ fn drawing_dir_str(d: &DrawingDirection) -> &'static str { } /// Per-visible-character world-space boxes for the MText editor's -/// click-to-select preview. Uses the exact same layout opts as `to_truck` +/// click-to-select preview. Uses the exact same layout opts as `to_render` /// so the boxes line up with the rendered glyphs. /// The MTEXT string to render: the live re-evaluated value when the entity /// hosts a dynamic field, otherwise its stored (cached) value. @@ -127,7 +127,7 @@ pub fn glyph_boxes(t: &MText, document: &acadrust::CadDocument) -> Vec layout.glyph_boxes } -fn to_truck(t: &MText, document: &acadrust::CadDocument) -> TruckEntity { +fn to_render(t: &MText, document: &acadrust::CadDocument) -> RenderEntity { let resolved_style = resolve_text_style(&t.style, document); let attach_h_anchor: f32 = match t.attachment_point { AttachmentPoint::TopCenter @@ -178,9 +178,9 @@ fn to_truck(t: &MText, document: &acadrust::CadDocument) -> TruckEntity { t.insertion_point.y, t.insertion_point.z, ); - TruckEntity { + RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Text(layout.strokes), + object: RenderObject::Text(layout.strokes), snap_pts: vec![(insertion, SnapHint::Insertion)], tangent_geoms: vec![], key_vertices: vec![], @@ -562,9 +562,9 @@ fn apply_transform(t: &mut MText, tr: &EntityTransform) { }); } -impl TruckConvertible for MText { - fn to_truck(&self, document: &acadrust::CadDocument) -> Option { - Some(to_truck(self, document)) +impl RenderConvertible for MText { + fn to_render(&self, document: &acadrust::CadDocument) -> Option { + Some(to_render(self, document)) } } diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs index 0d2a7d478..0632394c3 100644 --- a/src/entities/multileader.rs +++ b/src/entities/multileader.rs @@ -57,12 +57,12 @@ use crate::command::EntityTransform; use crate::entities::common::{ center_grip, edit_angle_prop as edit_angle, edit_prop as edit, num_prop as num_row, ro_prop as ro, square_grip, triangle_grip, }; -use crate::entities::traits::TruckConvertible; -use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; +use crate::entities::traits::RenderConvertible; +use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::model::wire_model::{SnapHint, TangentGeom}; -// ── TruckConvertible ──────────────────────────────────────────────────────── +// ── RenderConvertible ──────────────────────────────────────────────────────── /// Catmull-Rom spline tessellation through `ctrl` points, `segs_per_span` segments each. /// Operates in f64 so it can be applied to either WCS-direct coordinates (entity path) @@ -93,7 +93,7 @@ pub(crate) fn catmull_rom_pts(ctrl: &[[f64; 3]], segs_per_span: u32) -> Vec<[f64 out } -fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option { +fn to_render(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option { let nan = [f64::NAN; 3]; let p3 = |v: &acadrust::types::Vector3| -> [f64; 3] { [v.x, v.y, v.z] }; @@ -327,9 +327,9 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option Option { - to_truck(self, document) +impl RenderConvertible for MultiLeader { + fn to_render(&self, document: &acadrust::CadDocument) -> Option { + to_render(self, document) } } diff --git a/src/entities/ole2frame.rs b/src/entities/ole2frame.rs index 133651f1c..d1e8a43c0 100644 --- a/src/entities/ole2frame.rs +++ b/src/entities/ole2frame.rs @@ -2,12 +2,12 @@ use acadrust::entities::{Ole2Frame, OleObjectType}; use crate::command::EntityTransform; use crate::entities::common::{center_grip, edit_prop as edit, ro_prop as ro, square_grip}; -use crate::entities::traits::TruckConvertible; -use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; +use crate::entities::traits::RenderConvertible; +use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; use crate::scene::model::wire_model::SnapHint; -fn to_truck(ole: &Ole2Frame) -> TruckEntity { +fn to_render(ole: &Ole2Frame) -> RenderEntity { let x0 = ole.upper_left_corner.x; let y0 = ole.lower_right_corner.y; let x1 = ole.lower_right_corner.x; @@ -16,9 +16,9 @@ fn to_truck(ole: &Ole2Frame) -> TruckEntity { if (x1 - x0).abs() < 1e-6 && (y1 - y0).abs() < 1e-6 { let s = 0.5_f64; - return TruckEntity { + return RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Lines(vec![[-s, 0.0, z], [s, 0.0, z]]), + object: RenderObject::Lines(vec![[-s, 0.0, z], [s, 0.0, z]]), snap_pts: vec![], tangent_geoms: vec![], key_vertices: vec![], @@ -42,7 +42,7 @@ fn to_truck(ole: &Ole2Frame) -> TruckEntity { [x0, y0, z], ]; let center = glam::DVec3::new(cx, cy, z); - TruckEntity { + RenderEntity { // Interior pick surface: the frame selects on a click anywhere // inside, not just on its border. pick_tris: crate::entities::common::quad_pick_tris(&[ @@ -51,7 +51,7 @@ fn to_truck(ole: &Ole2Frame) -> TruckEntity { [x1, y1, z], [x0, y1, z], ]), - object: TruckObject::Lines(pts), + object: RenderObject::Lines(pts), snap_pts: vec![(center, SnapHint::Center)], tangent_geoms: vec![], key_vertices: vec![[x0, y0, z], [x1, y1, z]], @@ -168,9 +168,9 @@ fn apply_transform(ole: &mut Ole2Frame, t: &EntityTransform) { } } -impl TruckConvertible for Ole2Frame { - fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { - Some(to_truck(self)) +impl RenderConvertible for Ole2Frame { + fn to_render(&self, _document: &acadrust::CadDocument) -> Option { + Some(to_render(self)) } } diff --git a/src/entities/point.rs b/src/entities/point.rs index 0d7ac3f7b..e6fdd65a3 100644 --- a/src/entities/point.rs +++ b/src/entities/point.rs @@ -1,12 +1,11 @@ use acadrust::entities::Point; use acadrust::EntityType; use crate::t; -use truck_modeling::{builder, Point3}; use crate::command::EntityTransform; use crate::entities::common::{edit_prop as edit, parse_f64, square_grip}; -use crate::entities::traits::TruckConvertible; -use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; +use crate::entities::traits::RenderConvertible; +use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; use crate::scene::model::wire_model::SnapHint; @@ -17,7 +16,7 @@ use crate::scene::model::wire_model::SnapHint; const REL_REF_PX: f64 = 600.0; /// Resolve a positive (absolute) PDSIZE to a world size. Relative/zero PDSIZE -/// is handled by [`relative_truck`] when a world-per-pixel factor is available; +/// is handled by [`relative_render`] when a world-per-pixel factor is available; /// without one it falls back to a small fixed world size. fn pdsize_world(pdsize: f64) -> f64 { if pdsize > 0.0 { @@ -27,21 +26,20 @@ fn pdsize_world(pdsize: f64) -> f64 { } } -/// Build the truck entity for a point given the glyph half-size `s` in world -/// units. Shared by the header-driven path ([`to_truck`]) and the -/// viewport-aware relative path ([`relative_truck`]). -fn point_truck(pt: &Point, pdmode: i16, s: f64) -> TruckEntity { +/// Build the render entity for a point given the glyph half-size `s` in world +/// units. Shared by the header-driven path ([`to_render`]) and the +/// viewport-aware relative path ([`relative_render`]). +fn point_render(pt: &Point, pdmode: i16, s: f64) -> RenderEntity { // POINT location is stored in WCS (the extrusion normal only orients the // glyph/thickness) — remapping it through the arbitrary-axis OCS moved // mirrored points (normal 0,0,-1) to the wrong side of the drawing. let (wx, wy, wz) = (pt.location.x, pt.location.y, pt.location.z); let snap = glam::DVec3::new(wx, wy, wz); if pdmode == 0 { - // Default: a single vertex (driver handles the dot pixel). - let p = Point3::new(wx, wy, wz); - return TruckEntity { + // Default: a single position (the driver sizes the dot in pixels). + return RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Point(builder::vertex(p)), + object: RenderObject::Dot([wx, wy, wz]), snap_pts: vec![(snap, SnapHint::Node)], tangent_geoms: vec![], key_vertices: vec![], @@ -51,18 +49,18 @@ fn point_truck(pt: &Point, pdmode: i16, s: f64) -> TruckEntity { let pts = point_glyph(wx, wy, wz, pdmode, s); if pts.is_empty() { // PDMODE 1 = nothing — emit an empty Lines wire so picking still works. - return TruckEntity { + return RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Lines(vec![]), + object: RenderObject::Lines(vec![]), snap_pts: vec![(snap, SnapHint::Node)], tangent_geoms: vec![], key_vertices: vec![[wx, wy, wz]], fill_tris: vec![], }; } - TruckEntity { + RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Lines(pts), + object: RenderObject::Lines(pts), snap_pts: vec![(snap, SnapHint::Node)], tangent_geoms: vec![], key_vertices: vec![[wx, wy, wz]], @@ -75,11 +73,11 @@ fn point_truck(pt: &Point, pdmode: i16, s: f64) -> TruckEntity { /// across zoom. Returns `None` for an absolute PDSIZE, the size-independent /// default dot (PDMODE 0), or when no `wpp` is available — the caller then uses /// the normal header-driven path. -pub fn relative_truck( +pub fn relative_render( entity: &EntityType, document: &acadrust::CadDocument, wpp: Option, -) -> Option { +) -> Option { let EntityType::Point(pt) = entity else { return None; }; @@ -89,7 +87,7 @@ pub fn relative_truck( return None; } let wpp = wpp.filter(|w| *w > 0.0)?; - Some(point_truck(pt, pdmode, relative_world_size(pdsize, wpp) * 0.5)) + Some(point_render(pt, pdmode, relative_world_size(pdsize, wpp) * 0.5)) } /// Full on-screen glyph size (world units) for a relative (≤ 0) PDSIZE at the @@ -175,10 +173,10 @@ fn point_glyph(cx: f64, cy: f64, z: f64, pdmode: i16, s_half: f64) -> Vec<[f64; pts } -fn to_truck(pt: &Point, document: &acadrust::CadDocument) -> TruckEntity { +fn to_render(pt: &Point, document: &acadrust::CadDocument) -> RenderEntity { let pdmode = document.header.point_display_mode; let s = pdsize_world(document.header.point_display_size) * 0.5; - point_truck(pt, pdmode, s) + point_render(pt, pdmode, s) } fn grips(pt: &Point) -> Vec { @@ -235,9 +233,9 @@ fn apply_transform(pt: &mut Point, t: &EntityTransform) { }); } -impl TruckConvertible for Point { - fn to_truck(&self, document: &acadrust::CadDocument) -> Option { - Some(to_truck(self, document)) +impl RenderConvertible for Point { + fn to_render(&self, document: &acadrust::CadDocument) -> Option { + Some(to_render(self, document)) } } diff --git a/src/entities/polyline.rs b/src/entities/polyline.rs index bc8a8c588..87d912e40 100644 --- a/src/entities/polyline.rs +++ b/src/entities/polyline.rs @@ -1,19 +1,18 @@ use acadrust::entities::{Polyline, Polyline2D, Polyline3D}; use crate::t; -use truck_modeling::{builder, Edge, Point3, Wire}; use crate::command::EntityTransform; use crate::entities::common::{ edit_prop as edit, parse_f64, ro_prop as ro, square_grip, stepper_prop as stepper, }; -use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; -use crate::scene::convert::acad_to_truck::{extrusion_wall_tris, TruckEntity, TruckObject}; +use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible}; +use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::model::wire_model::TangentGeom; // ── Polyline (old-style 3D heavy polyline) ──────────────────────────────────── -fn tessellate_polyline(pl: &Polyline) -> TruckEntity { +fn tessellate_polyline(pl: &Polyline) -> RenderEntity { let pts: Vec<[f64; 3]> = pl .vertices .iter() @@ -26,9 +25,9 @@ fn tessellate_polyline(pl: &Polyline) -> TruckEntity { } let key_verts = pts.clone(); - TruckEntity { + RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Lines(points), + object: RenderObject::Lines(points), snap_pts: vec![], tangent_geoms: vec![], key_vertices: key_verts, @@ -36,8 +35,8 @@ fn tessellate_polyline(pl: &Polyline) -> TruckEntity { } } -impl TruckConvertible for Polyline { - fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { +impl RenderConvertible for Polyline { + fn to_render(&self, _document: &acadrust::CadDocument) -> Option { Some(tessellate_polyline(self)) } } @@ -196,13 +195,13 @@ pub fn drawn_vertices2d( (kept.len() >= 2).then_some(kept) } -fn tessellate_polyline2d(pl: &Polyline2D) -> TruckEntity { +fn tessellate_polyline2d(pl: &Polyline2D) -> RenderEntity { let filtered = drawn_vertices2d(pl); let verts: &[acadrust::entities::Vertex2D] = filtered.as_deref().unwrap_or(&pl.vertices); if verts.is_empty() { - return TruckEntity { + return RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Lines(vec![]), + object: RenderObject::Lines(vec![]), snap_pts: vec![], tangent_geoms: vec![], key_vertices: vec![], @@ -214,16 +213,15 @@ fn tessellate_polyline2d(pl: &Polyline2D) -> TruckEntity { let normal = (pl.normal.x, pl.normal.y, pl.normal.z); let count = verts.len(); let seg_count = if pl.is_closed() { count } else { count - 1 }; - let mut edges: Vec = Vec::new(); let mut tangents: Vec = Vec::new(); let mut key_verts: Vec<[f64; 3]> = Vec::new(); let to_wcs = |x: f64, y: f64| -> (f64, f64, f64) { crate::scene::view::transform::ocs_point_to_wcs((x, y, elev), normal) }; - let to_pt = |v: &acadrust::entities::Vertex2D| -> Point3 { + let to_pt = |v: &acadrust::entities::Vertex2D| -> [f64; 3] { let (wx, wy, wz) = to_wcs(v.location.x, v.location.y); - Point3::new(wx, wy, wz) + [wx, wy, wz] }; if pl.thickness.abs() > 1e-10 { @@ -282,9 +280,9 @@ fn tessellate_polyline2d(pl: &Polyline2D) -> TruckEntity { let (origin, fills) = wide_fills(pl); let (fill_tris, lines) = crate::entities::common::thick_band_tube(origin, &fills, t, normal, &to_wcs); - return TruckEntity { + return RenderEntity { pick_tris: fill_tris.clone(), - object: TruckObject::Lines(lines), + object: RenderObject::Lines(lines), snap_pts: vec![], tangent_geoms: tgs, key_vertices: kv, @@ -307,9 +305,9 @@ fn tessellate_polyline2d(pl: &Polyline2D) -> TruckEntity { } } } - return TruckEntity { + return RenderEntity { pick_tris: extrusion_wall_tris(&path, [t * nx, t * ny, t * nz]), - object: TruckObject::Lines(pts), + object: RenderObject::Lines(pts), snap_pts: vec![], tangent_geoms: tgs, key_vertices: kv, @@ -325,24 +323,15 @@ fn tessellate_polyline2d(pl: &Polyline2D) -> TruckEntity { let bulge = v0.bulge; if bulge.abs() < 1e-9 { - let tv0 = builder::vertex(p0); - let tv1 = builder::vertex(p1); - edges.push(builder::line(&tv0, &tv1)); tangents.push(TangentGeom::Line { - p1: [p0.x as f32, p0.y as f32, p0.z as f32], - p2: [p1.x as f32, p1.y as f32, p1.z as f32], + p1: [p0[0] as f32, p0[1] as f32, p0[2] as f32], + p2: [p1[0] as f32, p1[1] as f32, p1[2] as f32], }); } else if let Some(arc) = crate::entities::common::BulgeArc::from_bulge( [v0.location.x, v0.location.y], [v1.location.x, v1.location.y], bulge, ) { - let mid_s = arc.sample(0.5); - let (mid_wx, mid_wy, mid_wz) = to_wcs(mid_s[0], mid_s[1]); - let p_mid = Point3::new(mid_wx, mid_wy, mid_wz); - let tv0 = builder::vertex(p0); - let tv1 = builder::vertex(p1); - edges.push(builder::circle_arc(&tv0, &tv1, p_mid)); let (wcx, wcy, wcz) = to_wcs(arc.center[0], arc.center[1]); tangents.push(TangentGeom::Circle { center: [wcx as f32, wcy as f32, wcz as f32], @@ -351,9 +340,9 @@ fn tessellate_polyline2d(pl: &Polyline2D) -> TruckEntity { } if i == 0 { - key_verts.push([p0.x, p0.y, p0.z]); + key_verts.push([p0[0], p0[1], p0[2]]); } - key_verts.push([p1.x, p1.y, p1.z]); + key_verts.push([p1[0], p1[1], p1[2]]); } let (fill_origin, fills) = wide_fills(pl); @@ -366,11 +355,15 @@ fn tessellate_polyline2d(pl: &Polyline2D) -> TruckEntity { pl.is_closed(), &to_wcs, ); - TruckObject::TaperedLines(pts, widths) + RenderObject::TaperedLines(pts, widths) } - None => TruckObject::Contour(edges.into_iter().collect::()), + None => RenderObject::Lines( + crate::entities::curve::polyline2d_curve(pl) + .map(|planar| crate::entities::curve::curve_points(&planar)) + .unwrap_or_default(), + ), }; - TruckEntity { + RenderEntity { pick_tris: crate::entities::common::wide_band_tris(fill_origin, &fills), object, snap_pts: vec![], @@ -416,8 +409,8 @@ fn tapered_band_verts_2d( } } -impl TruckConvertible for Polyline2D { - fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { +impl RenderConvertible for Polyline2D { + fn to_render(&self, _document: &acadrust::CadDocument) -> Option { Some(tessellate_polyline2d(self)) } } @@ -699,7 +692,7 @@ impl Transformable for Polyline2D { // ── Polyline3D ──────────────────────────────────────────────────────────────── -fn tessellate_polyline3d(pl: &Polyline3D) -> TruckEntity { +fn tessellate_polyline3d(pl: &Polyline3D) -> RenderEntity { let to_pt = |v: &acadrust::entities::Vertex3DPolyline| -> [f64; 3] { [v.position.x, v.position.y, v.position.z] }; @@ -724,9 +717,9 @@ fn tessellate_polyline3d(pl: &Polyline3D) -> TruckEntity { points.push(wire_pts[0]); } - TruckEntity { + RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Lines(points), + object: RenderObject::Lines(points), snap_pts: vec![], tangent_geoms: vec![], key_vertices: key_verts, @@ -734,8 +727,8 @@ fn tessellate_polyline3d(pl: &Polyline3D) -> TruckEntity { } } -impl TruckConvertible for Polyline3D { - fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { +impl RenderConvertible for Polyline3D { + fn to_render(&self, _document: &acadrust::CadDocument) -> Option { Some(tessellate_polyline3d(self)) } } diff --git a/src/entities/raster_image.rs b/src/entities/raster_image.rs index e0e4997ce..be4437fb2 100644 --- a/src/entities/raster_image.rs +++ b/src/entities/raster_image.rs @@ -4,8 +4,8 @@ use crate::t; use crate::command::EntityTransform; use crate::entities::common::{center_grip, edit_prop as edit, ro_prop as ro, square_grip}; use crate::entities::text_support::{resolve_text_style, text_local_bounds}; -use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; -use crate::scene::convert::acad_to_truck::{GlyphRun, TextStroke, TruckEntity, TruckObject}; +use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible}; +use crate::scene::convert::acad_to_render::{GlyphRun, TextStroke, RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::text::lff; @@ -67,8 +67,8 @@ fn reflect_vec3(vx: &mut f64, vy: &mut f64, ax: f64, ay: f64, len2: f64) { // ── RasterImage ─────────────────────────────────────────────────────────────── -impl TruckConvertible for RasterImage { - fn to_truck(&self, document: &acadrust::CadDocument) -> Option { +impl RenderConvertible for RasterImage { + fn to_render(&self, document: &acadrust::CadDocument) -> Option { let corners = image_corners( &self.insertion_point, &self.u_vector, @@ -141,11 +141,11 @@ impl TruckConvertible for RasterImage { let resolvable = path.is_empty() || crate::scene::model::image_model::resolve_image(path).is_some(); if resolvable { - return Some(TruckEntity { + return Some(RenderEntity { // Interior pick surface: the image selects on a click anywhere // inside its frame, not just on the border. pick_tris: crate::entities::common::quad_pick_tris(&corners), - object: TruckObject::Lines(pts), + object: RenderObject::Lines(pts), snap_pts: vec![], tangent_geoms: vec![], key_vertices: corners.to_vec(), @@ -230,9 +230,9 @@ impl TruckConvertible for RasterImage { }); } - Some(TruckEntity { + Some(RenderEntity { pick_tris: crate::entities::common::quad_pick_tris(&corners), - object: TruckObject::Text(groups), + object: RenderObject::Text(groups), snap_pts: vec![], tangent_geoms: vec![], key_vertices: corners.to_vec(), @@ -416,8 +416,8 @@ impl Transformable for RasterImage { // ── Wipeout ─────────────────────────────────────────────────────────────────── -impl TruckConvertible for Wipeout { - fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { +impl RenderConvertible for Wipeout { + fn to_render(&self, _document: &acadrust::CadDocument) -> Option { let corners = image_corners( &self.insertion_point, &self.u_vector, @@ -464,11 +464,11 @@ impl TruckConvertible for Wipeout { image_wire(corners, false) }; - Some(TruckEntity { + Some(RenderEntity { // Interior pick surface — a wipeout reads as a solid patch, so a // click anywhere on it should select it. pick_tris: crate::entities::common::quad_pick_tris(&corners), - object: TruckObject::Lines(pts), + object: RenderObject::Lines(pts), snap_pts: vec![], tangent_geoms: vec![], key_vertices: corners.to_vec(), @@ -491,7 +491,7 @@ impl Grippable for Wipeout { let ox = self.insertion_point.x; let oy = self.insertion_point.y; let oz = self.insertion_point.z; - // Same image-pixel-space → WCS mapping as `to_truck` so grips + // Same image-pixel-space → WCS mapping as `to_render` so grips // sit exactly on the rendered polygon vertices. self.clip_boundary_vertices .iter() diff --git a/src/entities/ray.rs b/src/entities/ray.rs index d17c25325..ca31dc92f 100644 --- a/src/entities/ray.rs +++ b/src/entities/ray.rs @@ -3,8 +3,8 @@ use crate::t; use crate::command::EntityTransform; use crate::entities::common::{center_grip, edit_prop as edit, square_grip}; -use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; -use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; +use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible}; +use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; /// Display length used when rendering semi-infinite / infinite lines. @@ -12,8 +12,8 @@ const DISPLAY_EXTENT: f64 = 1_000_000.0; // ── Ray (semi-infinite line) ────────────────────────────────────────────────── -impl TruckConvertible for Ray { - fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { +impl RenderConvertible for Ray { + fn to_render(&self, _document: &acadrust::CadDocument) -> Option { let bp = self.base_point; let dir = self.direction; // Normalize direction to avoid f32 overflow when DXF stores @@ -29,9 +29,9 @@ impl TruckConvertible for Ray { bp.z + nz * DISPLAY_EXTENT, ]; let start: [f64; 3] = [bp.x, bp.y, bp.z]; - Some(TruckEntity { + Some(RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Lines(vec![start, far]), + object: RenderObject::Lines(vec![start, far]), snap_pts: vec![], tangent_geoms: vec![], key_vertices: vec![start], @@ -151,8 +151,8 @@ impl Transformable for Ray { // ── XLine (construction line, infinite) ────────────────────────────────────── -impl TruckConvertible for XLine { - fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { +impl RenderConvertible for XLine { + fn to_render(&self, _document: &acadrust::CadDocument) -> Option { let bp = self.base_point; let dir = self.direction; let len = (dir.x * dir.x + dir.y * dir.y + dir.z * dir.z).sqrt(); @@ -170,9 +170,9 @@ impl TruckConvertible for XLine { bp.y - ny * DISPLAY_EXTENT, bp.z - nz * DISPLAY_EXTENT, ]; - Some(TruckEntity { + Some(RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Lines(vec![far_neg, far_pos]), + object: RenderObject::Lines(vec![far_neg, far_pos]), snap_pts: vec![], tangent_geoms: vec![], key_vertices: vec![[bp.x, bp.y, bp.z]], diff --git a/src/entities/shape.rs b/src/entities/shape.rs index df5e0f4e7..7b4b1b8f1 100644 --- a/src/entities/shape.rs +++ b/src/entities/shape.rs @@ -10,8 +10,8 @@ use crate::t; use crate::command::EntityTransform; use crate::entities::common::{edit_angle_prop as edit_angle, edit_prop as edit, ro_prop as ro, square_grip}; -use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; -use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; +use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible}; +use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection, Property, PropValue}; use crate::scene::view::transform; use crate::scene::model::wire_model::SnapHint; @@ -54,7 +54,7 @@ fn shape_marker( out } -// ── TruckConvertible ────────────────────────────────────────────────────────── +// ── RenderConvertible ────────────────────────────────────────────────────────── /// The shape's SHX polylines, resolved through its STYLE's shape file: the /// style by handle (name fallback), the file with the shared path fallbacks @@ -129,8 +129,8 @@ fn shx_polylines( None } -impl TruckConvertible for Shape { - fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { +impl RenderConvertible for Shape { + fn to_render(&self, _document: &acadrust::CadDocument) -> Option { // SHAPE insertion point is OCS (planar entity) — map through the // arbitrary axis so a mirrored shape's marker lands where it renders. let (ox, oy, oz) = crate::scene::view::transform::ocs_point_to_wcs( @@ -182,9 +182,9 @@ impl TruckConvertible for Shape { } } if !pts.is_empty() { - return Some(TruckEntity { + return Some(RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Lines(pts), + object: RenderObject::Lines(pts), snap_pts: vec![(snap_pt, SnapHint::Insertion)], tangent_geoms: vec![], key_vertices: vec![[ox, oy, oz]], @@ -203,9 +203,9 @@ impl TruckConvertible for Shape { self.oblique_angle, ); - Some(TruckEntity { + Some(RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Lines(pts), + object: RenderObject::Lines(pts), snap_pts: vec![(snap_pt, SnapHint::Insertion)], tangent_geoms: vec![], key_vertices: vec![[ox, oy, oz]], diff --git a/src/entities/solid.rs b/src/entities/solid.rs index 38b56cf82..89505d3cf 100644 --- a/src/entities/solid.rs +++ b/src/entities/solid.rs @@ -1,6 +1,6 @@ // SOLID entity — 2D filled quadrilateral (or triangle when p3 == p4). // -// Wireframe: the 4 perimeter edges as TruckObject::Lines. +// Wireframe: the 4 perimeter edges as RenderObject::Lines. // Filled: two triangles on `fill_tris`, preserving the entity's full WCS // plane both at top level and through block expansion. The scene // keeps a separate 2-D HatchModel only for plot projection; screen @@ -12,8 +12,8 @@ use crate::t; use crate::command::EntityTransform; use crate::entities::common::{edit_prop as edit, ro_prop as ro, square_grip}; -use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; -use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; +use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible}; +use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; use crate::scene::model::wire_model::SnapHint; @@ -72,8 +72,8 @@ pub(crate) fn perimeter_indices(corners: &[[f64; 3]; 4]) -> [usize; 4] { } } -impl TruckConvertible for Solid { - fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { +impl RenderConvertible for Solid { + fn to_render(&self, _document: &acadrust::CadDocument) -> Option { // SOLID corners are OCS. Map them to WCS, then resolve either the DXF // Z-order or legacy perimeter order before building edges and fill. let corners = wcs_corners(self); @@ -104,9 +104,9 @@ impl TruckConvertible for Solid { // two points coincide and the second triangle degenerates harmlessly. let fill_tris = vec![p0, p1, p2, p0, p2, p3]; - Some(TruckEntity { + Some(RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Lines(pts), + object: RenderObject::Lines(pts), snap_pts: snap, tangent_geoms: vec![], key_vertices: corners.to_vec(), diff --git a/src/entities/spline.rs b/src/entities/spline.rs index a72824162..fe95b27c5 100644 --- a/src/entities/spline.rs +++ b/src/entities/spline.rs @@ -1,17 +1,14 @@ use acadrust::entities::Spline; use crate::t; -use truck_modeling::{ - base::{BoundedCurve, ParameterDivision1D, ParametricCurve, Vector4}, - builder, BSplineCurve, Curve, Edge, KnotVec, NurbsCurve, Point3, Wire, -}; +use acadrust::kernel::space::NurbsCurve3; use crate::command::EntityTransform; use crate::entities::common::{edit_prop as edit, parse_f64, ro_prop as ro, square_grip}; -use crate::entities::traits::TruckConvertible; -use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; +use crate::entities::traits::RenderConvertible; +use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; -fn to_truck(spl: &Spline) -> TruckEntity { +fn to_render(spl: &Spline) -> RenderEntity { let n = spl.control_points.len(); if n < 2 { // A fit-point spline (DWG scenario 2 / R2013+) stores only the points @@ -57,18 +54,18 @@ fn to_truck(spl: &Spline) -> TruckEntity { } else { spl.fit_points.iter().map(|p| [p.x, p.y, p.z]).collect() }; - return TruckEntity { + return RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Lines(pts), + object: RenderObject::Lines(pts), snap_pts, tangent_geoms: vec![], key_vertices, fill_tris: vec![], }; } - return TruckEntity { + return RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Point(builder::vertex(Point3::new(0.0, 0.0, 0.0))), + object: RenderObject::Lines(Vec::new()), snap_pts: vec![], tangent_geoms: vec![], key_vertices: vec![], @@ -76,42 +73,6 @@ fn to_truck(spl: &Spline) -> TruckEntity { }; } - let knot_vec = if !spl.knots.is_empty() { - KnotVec::from(spl.knots.clone()) - } else { - KnotVec::uniform_knot(spl.degree as usize, n - 1) - }; - - // Use rational NURBS when weights are provided (circles/conics stored as NURBS). - let use_nurbs = !spl.weights.is_empty() && spl.weights.len() == n; - let (p_start, p_end, curve) = if use_nurbs { - let homo_pts: Vec = spl - .control_points - .iter() - .zip(spl.weights.iter()) - .map(|(p, &w)| { - let w = if w.abs() < 1e-12 { 1.0 } else { w }; - Vector4::new(p.x * w, p.y * w, p.z * w, w) - }) - .collect(); - let nurbs = NurbsCurve::new(BSplineCurve::new(knot_vec, homo_pts)); - let (t0, t1) = nurbs.range_tuple(); - (nurbs.subs(t0), nurbs.subs(t1), Curve::NurbsCurve(nurbs)) - } else { - let ctrl_pts: Vec = spl - .control_points - .iter() - .map(|p| Point3::new(p.x, p.y, p.z)) - .collect(); - let bspline = BSplineCurve::new(knot_vec, ctrl_pts); - let (t0, t1) = bspline.range_tuple(); - ( - bspline.subs(t0), - bspline.subs(t1), - Curve::BSplineCurve(bspline), - ) - }; - // Snap sources from the spline's own curve. A spline is not a chain of // straight segments, so nothing goes in `key_vertices`: consecutive // entries there are joined and their midpoints offered, and the chord @@ -149,30 +110,27 @@ fn to_truck(spl: &Spline) -> TruckEntity { } }; + // Sampled through the spline's own curve — the same definition EXTRUDE + // and REVOLVE read — and closed back onto its start when the flags say + // it is periodic and the two ends do not already meet. let is_closed = spl.flags.closed || spl.flags.periodic; - let gap = { - let dx = (p_end.x - p_start.x) as f32; - let dy = (p_end.y - p_start.y) as f32; - let dz = (p_end.z - p_start.z) as f32; - (dx * dx + dy * dy + dz * dz).sqrt() - }; + let mut points = crate::entities::curve::spline_curve(spl) + .map(|planar| crate::entities::curve::curve_points(&planar)) + .unwrap_or_default(); + if is_closed { + if let (Some(first), Some(last)) = (points.first().copied(), points.last().copied()) { + let gap = ((last[0] - first[0]).powi(2) + + (last[1] - first[1]).powi(2) + + (last[2] - first[2]).powi(2)) + .sqrt(); + if gap > 1e-6 { + points.push(first); + } + } + } + let object = RenderObject::Lines(points); - let object = if is_closed && gap > 1e-6 { - let v_start = builder::vertex(p_start); - let v_end = builder::vertex(p_end); - let v_close = builder::vertex(p_start); - let main_edge = Edge::new(&v_start, &v_end, curve); - let close_edge = builder::line(&v_end, &v_close); - let wire: Wire = [main_edge, close_edge].into_iter().collect(); - TruckObject::Contour(wire) - } else { - let v_start = builder::vertex(p_start); - let v_end = builder::vertex(p_end); - let edge = Edge::new(&v_start, &v_end, curve); - TruckObject::Curve(edge) - }; - - TruckEntity { + RenderEntity { pick_tris: Vec::new(), object, snap_pts, @@ -199,12 +157,6 @@ pub(crate) fn measurement_polyline(spl: &Spline) -> Vec<[f64; 3]> { if degree == 0 || degree >= count { return spl.control_points.iter().map(|p| [p.x, p.y, p.z]).collect(); } - let knot_vec = if spl.knots.len() == count + degree + 1 { - KnotVec::from(spl.knots.clone()) - } else { - KnotVec::uniform_knot(degree, count - 1) - }; - let mut min = [f64::INFINITY; 3]; let mut max = [f64::NEG_INFINITY; 3]; for point in &spl.control_points { @@ -221,35 +173,23 @@ pub(crate) fn measurement_polyline(spl: &Spline) -> Vec<[f64; 3]> { .sqrt(); let tolerance = crate::scene::convert::tess_util::fill_chord_tol(diagonal.max(1.0)); - if spl.weights.len() == count { - let controls = spl - .control_points + // The kernel's space curve holds the rational and the polynomial case + // alike — weights absent means polynomial — and samples to a chord + // tolerance, refined where the curve bends rather than evenly. + let controls: Vec<[f64; 3]> = spl + .control_points + .iter() + .map(|point| [point.x, point.y, point.z]) + .collect(); + let weights = (spl.weights.len() == count).then(|| { + spl.weights .iter() - .zip(&spl.weights) - .map(|(point, &weight)| { - let weight = if weight.abs() < 1e-12 { 1.0 } else { weight }; - Vector4::new( - point.x * weight, - point.y * weight, - point.z * weight, - weight, - ) - }) - .collect::>(); - let curve = NurbsCurve::new(BSplineCurve::new(knot_vec, controls)); - let range = curve.range_tuple(); - let (_, points) = curve.parameter_division(range, tolerance); - points.into_iter().map(|point| [point.x, point.y, point.z]).collect() - } else { - let controls = spl - .control_points - .iter() - .map(|point| Point3::new(point.x, point.y, point.z)) - .collect::>(); - let curve = BSplineCurve::new(knot_vec, controls); - let range = curve.range_tuple(); - let (_, points) = curve.parameter_division(range, tolerance); - points.into_iter().map(|point| [point.x, point.y, point.z]).collect() + .map(|weight| if weight.abs() < 1e-12 { 1.0 } else { *weight }) + .collect() + }); + match NurbsCurve3::new(degree, controls, spl.knots.clone(), weights) { + Some(curve) => curve.tessellate_within(tolerance), + None => spl.control_points.iter().map(|p| [p.x, p.y, p.z]).collect(), } } @@ -651,9 +591,9 @@ fn apply_transform(spline: &mut Spline, t: &EntityTransform) { }); } -impl TruckConvertible for Spline { - fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { - Some(to_truck(self)) +impl RenderConvertible for Spline { + fn to_render(&self, _document: &acadrust::CadDocument) -> Option { + Some(to_render(self)) } } @@ -712,7 +652,7 @@ impl crate::entities::traits::Grippable for Spline { * 0.5; self.weights.insert(i1, w); } - // Clear knots so to_truck rebuilds a uniform knot vector + // Clear knots so to_render rebuilds a uniform knot vector // for the new CV count. self.knots.clear(); } diff --git a/src/entities/table.rs b/src/entities/table.rs index e899febb8..5535a264b 100644 --- a/src/entities/table.rs +++ b/src/entities/table.rs @@ -6,8 +6,8 @@ use crate::entities::common::{ro_prop as ro, square_grip}; use crate::entities::text_support::{ layout_mtext, MTextRenderOpts, MTextVAnchor, ResolvedTextStyle, }; -use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; -use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; +use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible}; +use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection, Property, PropValue}; use crate::scene::view::transform; use crate::scene::model::wire_model::SnapHint; @@ -703,8 +703,8 @@ pub(crate) fn block_cell_inserts( inserts } -impl TruckConvertible for Table { - fn to_truck(&self, document: &acadrust::CadDocument) -> Option { +impl RenderConvertible for Table { + fn to_render(&self, document: &acadrust::CadDocument) -> Option { if self.rows.is_empty() || self.columns.is_empty() { return None; } @@ -982,7 +982,7 @@ impl TruckConvertible for Table { }); // Flatten TextStroke groups into the table's Lines buffer. // Per-run inline `\C` / `\c` colour is dropped here because the - // table emits a single TruckObject::Lines for borders + text; + // table emits a single RenderObject::Lines for borders + text; // tracking it would require splitting the table into multiple // WireModels per cell colour. Borders + uniform-coloured runs // honour the entity's outer colour. @@ -1026,9 +1026,9 @@ impl TruckConvertible for Table { [x as f64 + base[0], y as f64 + base[1], z as f64 + base[2]] }) .collect(); - Some(TruckEntity { + Some(RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Lines(pts_f64), + object: RenderObject::Lines(pts_f64), snap_pts: vec![(glam::DVec3::new(self.insertion_point.x, self.insertion_point.y, self.insertion_point.z), SnapHint::Insertion)], tangent_geoms: vec![], key_vertices: vec![], diff --git a/src/entities/text.rs b/src/entities/text.rs index e8eed635e..00af2eea8 100644 --- a/src/entities/text.rs +++ b/src/entities/text.rs @@ -7,8 +7,8 @@ use crate::entities::common::{ use crate::entities::text_support::{ resolve_dxf_special_chars, resolve_text_style, text_local_bounds, }; -use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; -use crate::scene::convert::acad_to_truck::{GlyphRun, TextStroke, TruckEntity, TruckObject}; +use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible}; +use crate::scene::convert::acad_to_render::{GlyphRun, TextStroke, RenderEntity, RenderObject}; use crate::scene::text::lff; use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::model::wire_model::SnapHint; @@ -52,7 +52,7 @@ pub(crate) fn sync_text_alignment_point(t: &mut Text) { } /// Resolved placement of a TEXT run: the baseline-anchored run origin (WCS xy) -/// plus every parameter needed to lay the glyphs out. Shared by `to_truck` (the +/// plus every parameter needed to lay the glyphs out. Shared by `to_render` (the /// stroke path) and the SDF-quad text collector so both place text identically. pub struct TextPlacement { /// Run-local origin (glyph space `[0,0]` maps here), WCS xy, kept f64. @@ -98,7 +98,7 @@ pub(crate) fn acad_text_encode(value: &str) -> String { out } -fn to_truck(t: &Text, document: &acadrust::CadDocument) -> TruckEntity { +fn to_render(t: &Text, document: &acadrust::CadDocument) -> RenderEntity { let p = text_run_placement(t, document); let snap_pt = glam::DVec3::new(p.wcs_insertion[0], p.wcs_insertion[1], p.wcs_insertion[2]); // Parse `%%` codes via acadrust, re-encoded for the stroke tessellator. @@ -113,9 +113,9 @@ fn to_truck(t: &Text, document: &acadrust::CadDocument) -> TruckEntity { &p.font, &value, ); - TruckEntity { + RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Text(vec![TextStroke { + object: RenderObject::Text(vec![TextStroke { strokes, origin: p.origin, color: None, @@ -139,7 +139,7 @@ fn to_truck(t: &Text, document: &acadrust::CadDocument) -> TruckEntity { } /// Compute a TEXT entity's run placement (origin + layout params). Extracted -/// from `to_truck` verbatim so the stroke and SDF-quad paths agree exactly. +/// from `to_render` verbatim so the stroke and SDF-quad paths agree exactly. pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPlacement { let normal = (t.normal.x, t.normal.y, t.normal.z); let (wsx, wsy, wsz) = crate::scene::view::transform::ocs_point_to_wcs( @@ -493,9 +493,9 @@ fn apply_transform(t: &mut Text, tr: &EntityTransform) { }); } -impl TruckConvertible for Text { - fn to_truck(&self, document: &acadrust::CadDocument) -> Option { - Some(to_truck(self, document)) +impl RenderConvertible for Text { + fn to_render(&self, document: &acadrust::CadDocument) -> Option { + Some(to_render(self, document)) } } diff --git a/src/entities/text_support.rs b/src/entities/text_support.rs index f6eae80a1..0ddc32aa7 100644 --- a/src/entities/text_support.rs +++ b/src/entities/text_support.rs @@ -1,7 +1,7 @@ use acadrust::types::aci_table::aci_to_rgb; use acadrust::CadDocument; -use crate::scene::convert::acad_to_truck::{GlyphRun, TextStroke}; +use crate::scene::convert::acad_to_render::{GlyphRun, TextStroke}; use crate::scene::text::font_face::Face; use crate::scene::text::lff; diff --git a/src/entities/tolerance.rs b/src/entities/tolerance.rs index 2f759a985..6172b0aad 100644 --- a/src/entities/tolerance.rs +++ b/src/entities/tolerance.rs @@ -2,8 +2,8 @@ use acadrust::entities::Tolerance; use crate::command::EntityTransform; use crate::entities::common::{edit_prop as edit, ro_prop as ro, square_grip}; -use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; -use crate::scene::convert::acad_to_truck::{GlyphRun, TextStroke, TruckEntity, TruckObject}; +use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible}; +use crate::scene::convert::acad_to_render::{GlyphRun, TextStroke, RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; use crate::scene::model::wire_model::SnapHint; use crate::scene::text::lff; @@ -430,10 +430,10 @@ fn tessellate_tolerance( (box_out, cells) } -// ── TruckConvertible ────────────────────────────────────────────────────────── +// ── RenderConvertible ────────────────────────────────────────────────────────── -impl TruckConvertible for Tolerance { - fn to_truck(&self, document: &acadrust::CadDocument) -> Option { +impl RenderConvertible for Tolerance { + fn to_render(&self, document: &acadrust::CadDocument) -> Option { if self.text.is_empty() { return None; } @@ -493,9 +493,9 @@ impl TruckConvertible for Tolerance { return None; } - Some(TruckEntity { + Some(RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Text(groups), + object: RenderObject::Text(groups), snap_pts: vec![(snap_pt, SnapHint::Insertion)], tangent_geoms: vec![], key_vertices: vec![], @@ -845,10 +845,10 @@ mod tests { let mut tol = Tolerance::new(); tol.text = "ABC".into(); - let Some(TruckEntity { - object: TruckObject::Text(groups), + let Some(RenderEntity { + object: RenderObject::Text(groups), .. - }) = tol.to_truck(&doc) + }) = tol.to_render(&doc) else { panic!("tolerance produced no text"); }; diff --git a/src/entities/traits.rs b/src/entities/traits.rs index 963cc5db7..723bec01a 100644 --- a/src/entities/traits.rs +++ b/src/entities/traits.rs @@ -1,17 +1,17 @@ use acadrust::{CadDocument, EntityType}; use crate::command::EntityTransform; -use crate::scene::convert::acad_to_truck::TruckEntity; +use crate::scene::convert::acad_to_render::RenderEntity; use crate::scene::model::object::{ GripApply, GripDef, GripMenuAction, GripMenuItem, PropSection, }; use crate::scene::convert::tess_util::FallbackGeometry; -pub trait TruckConvertible { - fn to_truck(&self, document: &CadDocument) -> Option; +pub trait RenderConvertible { + fn to_render(&self, document: &CadDocument) -> Option; } -/// Fallback geometry for entities not routed through the truck topology +/// Fallback geometry for entities not routed through the render /// pipeline (Viewport, Insert, Hatch outline, Ole2Frame). Returns absolute /// WCS `f32` points + snap/key vertices the dispatcher wraps into a /// `WireModel`. @@ -162,7 +162,7 @@ pub fn entity_type_name(et: &EntityType) -> &str { } pub trait EntityTypeOps { - fn to_truck_entity(&self, document: &CadDocument) -> Option; + fn to_render_entity(&self, document: &CadDocument) -> Option; fn grips(&self) -> Vec; fn grip_menu(&self, grip_id: usize) -> Vec; fn geometry_properties(&self, text_style_names: &[String]) -> Vec; @@ -284,9 +284,9 @@ macro_rules! impl_entity_basics_with_text_styles { } impl EntityTypeOps for EntityType { - fn to_truck_entity(&self, document: &CadDocument) -> Option { + fn to_render_entity(&self, document: &CadDocument) -> Option { dispatch!(self, - |e| TruckConvertible::to_truck(e, document), + |e| RenderConvertible::to_render(e, document), [ Point, Line, Circle, Arc, Ellipse, Spline, LwPolyline, Polyline, Polyline2D, Polyline3D, Ray, XLine, RasterImage, diff --git a/src/entities/underlay.rs b/src/entities/underlay.rs index dc78a758d..b7450d8c6 100644 --- a/src/entities/underlay.rs +++ b/src/entities/underlay.rs @@ -10,8 +10,8 @@ use glam::DVec3; use crate::command::EntityTransform; use crate::entities::common::{center_grip, edit_angle_prop as edit_angle, edit_prop as edit, ro_prop as ro, square_grip}; -use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; -use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; +use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible}; +use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::model::wire_model::SnapHint; @@ -37,7 +37,7 @@ fn cross_wire(origin: [f64; 3], size: f64) -> Vec<[f64; 3]> { ] } -// ── TruckConvertible ────────────────────────────────────────────────────────── +// ── RenderConvertible ────────────────────────────────────────────────────────── /// The underlay's page rectangle in world space (CCW from the insertion), /// when its definition resolves and the page rasterises: page inches × the @@ -71,8 +71,8 @@ fn page_quad( ]) } -impl TruckConvertible for Underlay { - fn to_truck(&self, _document: &acadrust::CadDocument) -> Option { +impl RenderConvertible for Underlay { + fn to_render(&self, _document: &acadrust::CadDocument) -> Option { let origin = v3(&self.insertion_point); let _origin_f32 = v3f32(&self.insertion_point); @@ -90,9 +90,9 @@ impl TruckConvertible for Underlay { // selects on a click anywhere inside, not just on its outline. let ring: Vec<[f64; 3]> = world_verts.iter().map(|v| [v.x, v.y, v.z]).collect(); let pick_tris = crate::entities::mesh::triangulate_planar(&ring); - Some(TruckEntity { + Some(RenderEntity { pick_tris, - object: TruckObject::Lines(pts), + object: RenderObject::Lines(pts), snap_pts: vec![(glam::DVec3::new(self.insertion_point.x, self.insertion_point.y, self.insertion_point.z), SnapHint::Node)], tangent_geoms: vec![], key_vertices: key, @@ -103,9 +103,9 @@ impl TruckConvertible for Underlay { // visible extent instead of a lone cross under the raster. let pts = vec![q[0], q[1], q[2], q[3], q[0]]; let pick_tris = crate::entities::mesh::triangulate_planar(&q.to_vec()); - Some(TruckEntity { + Some(RenderEntity { pick_tris, - object: TruckObject::Lines(pts), + object: RenderObject::Lines(pts), snap_pts: vec![(glam::DVec3::new(self.insertion_point.x, self.insertion_point.y, self.insertion_point.z), SnapHint::Node)], tangent_geoms: vec![], key_vertices: q.to_vec(), @@ -114,9 +114,9 @@ impl TruckConvertible for Underlay { } else { // No clip boundary: draw a cross at insertion point. let pts = cross_wire(origin, 1.0); - Some(TruckEntity { + Some(RenderEntity { pick_tris: Vec::new(), - object: TruckObject::Lines(pts), + object: RenderObject::Lines(pts), snap_pts: vec![(glam::DVec3::new(self.insertion_point.x, self.insertion_point.y, self.insertion_point.z), SnapHint::Node)], tangent_geoms: vec![], key_vertices: vec![origin], diff --git a/src/io/mod.rs b/src/io/mod.rs index 0b8263e00..693a04e3c 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -1896,7 +1896,7 @@ pub(crate) fn is_entity_corrupt(e: &EntityType) -> bool { !finite_vec3(&c.center) || !finite_coord(c.radius) // Reject zero- or near-zero circles: they tessellate into a - // degenerate truck curve that crashes parameter_division. + // degenerate curve the tessellator cannot sample. || c.radius.abs() < 1.0e-10 || c.radius.abs() > 1.0e10 } @@ -1909,12 +1909,12 @@ pub(crate) fn is_entity_corrupt(e: &EntityType) -> bool { || a.radius.abs() < 1.0e-10 || a.radius.abs() > 1.0e10 // Zero-sweep arc (start_angle == end_angle, modulo 2π) collapses - // to a single point in WCS — truck's circle_arc on three + // to a single point in WCS — a three-point circle fit on // coincident vertices recurses unboundedly in parameter_division. || (a.end_angle - a.start_angle).abs() < 1.0e-9 // Near-zero sweep is the same trap with a wider mouth: a tiny but // non-zero sweep (e.g. 1.6e-6 rad) still places start/mid/end - // within truck's coincidence tolerance, so parameter_division + // within the coincidence tolerance, so sampling // recurses and allocates until OOM. Gate on arc *length* // (radius × sweep), not sweep alone, so a legitimately large- // radius small-sweep arc (still a visible curve) survives while @@ -1924,7 +1924,7 @@ pub(crate) fn is_entity_corrupt(e: &EntityType) -> bool { // the floor above, a small sweep over a modest radius leaves // start/mid/end almost on one line (a 35-unit, 6.5e-7-rad arc has // arc length 2.3e-5 — past the gate — yet bows off its chord by - // only ~2e-12). truck's 3-point `circle_arc` fit then returns a + // only ~2e-12). A three-point circle fit then returns a // near-infinite radius and `parameter_division` subdivides without // bound. Gate on the sagitta (chord height = r·(1−cos(sweep/2))), // the true measure of how far the arc departs a straight line and @@ -1951,13 +1951,13 @@ pub(crate) fn is_entity_corrupt(e: &EntityType) -> bool { } E::Spline(s) => { // Parser desync emits exactly-100_000-control-point splines with a - // garbage knot vector. Building a truck NURBS/B-spline from one and + // garbage knot vector. Building a NURBS from one and // tessellating it runs `parameter_division` into an unbounded // allocation — single-threaded, 32 GB+ — long before the drawing // finishes loading. Reject the desync signature plus any spline - // truck can't build: non-finite control points, or a knot vector + // the kernel can't build: non-finite control points, or a knot vector // that's non-finite, non-monotonic, or the wrong length - // (truck requires `knots.len() == ctrl.len() + degree + 1`). + // (the kernel requires `knots.len() == ctrl.len() + degree + 1`). let n = s.control_points.len(); let degree_bad = s.degree < 1; let deg = s.degree.max(0) as usize; @@ -1966,7 +1966,7 @@ pub(crate) fn is_entity_corrupt(e: &EntityType) -> bool { || s.knots.windows(2).any(|w| w[1] < w[0]) || s.knots.len() != n + deg + 1); // Degenerate: every control point collapses onto (nearly) the same - // point, so the curve has zero length. truck's `circle_arc` / + // point, so the curve has zero length. A three-point fit // `parameter_division` never converges on it and the tessellation // hangs — a periodic 9-point spline pinned at the origin is the seen // case. Reject when the control-point extent is sub-precision. @@ -2203,7 +2203,7 @@ mod corrupt_guard_tests { // A near-zero-sweep arc: sweep 1.56e-6 rad on a 3.9e-3 radius. The angles // are individually finite and the radius is in range, so the old // (end-start) < 1e-9 check passed it through — but start/mid/end land - // within truck's coincidence tolerance and parameter_division allocates + // within the coincidence tolerance and sampling allocates // until OOM. The arc-length floor must reject it. #[test] fn rejects_near_degenerate_arc() { @@ -2218,7 +2218,7 @@ mod corrupt_guard_tests { // A 35-unit-radius arc sweeping 6.5e-7 rad has arc length 2.3e-5 — past the // arc-length floor — yet its start/mid/end bow off the chord by only ~2e-12, - // so truck's 3-point circle fit blows up and parameter_division hangs. The + // so a three-point circle fit blows up and sampling hangs. The // sagitta floor must reject it where the arc-length floor alone does not. #[test] fn rejects_near_collinear_arc() { @@ -2246,7 +2246,7 @@ mod corrupt_guard_tests { assert!(!is_entity_corrupt(&EntityType::Arc(a))); } - // Parser desync emits 100_000-control-point splines; building a truck + // Parser desync emits 100_000-control-point splines; building a kernel // NURBS from one and tessellating it OOMs. The control-point cap rejects it. #[test] fn rejects_desync_spline() { @@ -2256,7 +2256,7 @@ mod corrupt_guard_tests { } // A periodic spline whose control points all collapse onto (nearly) one - // point has zero length; truck's parameter_division never converges and the + // point has zero length; sampling never converges and the // tessellation hangs. The control-point extent floor must reject it. #[test] fn rejects_degenerate_point_spline() { diff --git a/src/modules/draw/draw/spline.rs b/src/modules/draw/draw/spline.rs index fbb3bbb58..c26c29fa1 100644 --- a/src/modules/draw/draw/spline.rs +++ b/src/modules/draw/draw/spline.rs @@ -11,8 +11,7 @@ use crate::command::{CadCommand, CmdResult}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; use glam::DVec3; -use truck_modeling::base::{BoundedCurve, ParametricCurve}; -use truck_modeling::{BSplineCurve, KnotVec, Point3}; +use acadrust::kernel::space::NurbsCurve3; #[allow(dead_code)] pub fn tool() -> ToolDef { @@ -87,19 +86,19 @@ fn sample_curve(pts: &[DVec3]) -> Vec<[f32; 3]> { .collect(); } let degree = 3_usize.min(n - 1); - let ctrl: Vec = pts - .iter() - .map(|p| Point3::new(p.x, p.y, p.z)) - .collect(); - let curve = BSplineCurve::new(KnotVec::from(uniform_knots(n, degree)), ctrl); - let (t0, t1) = curve.range_tuple(); + let ctrl: Vec<[f64; 3]> = pts.iter().map(|p| [p.x, p.y, p.z]).collect(); + let Some(curve) = NurbsCurve3::new(degree, ctrl, uniform_knots(n, degree), None) else { + return pts + .iter() + .map(|p| [p.x as f32, p.y as f32, p.z as f32]) + .collect(); + }; // Resolution scales with span count so long splines stay smooth. let steps = 24 * (n - 1); (0..=steps) .map(|i| { - let t = t0 + (t1 - t0) * (i as f64 / steps as f64); - let p = curve.subs(t); - [p.x as f32, p.y as f32, p.z as f32] + let p = curve.point_at(i as f64 / steps as f64); + [p[0] as f32, p[1] as f32, p[2] as f32] }) .collect() } diff --git a/src/modules/draw/modify/blend.rs b/src/modules/draw/modify/blend.rs index faadd4a3c..f4fa0a5f4 100644 --- a/src/modules/draw/modify/blend.rs +++ b/src/modules/draw/modify/blend.rs @@ -11,8 +11,7 @@ use acadrust::types::Vector3; use acadrust::{EntityType, Handle}; use glam::DVec3; use crate::t; -use truck_modeling::base::{BoundedCurve, ParametricCurve, Vector4}; -use truck_modeling::{BSplineCurve, KnotVec, NurbsCurve, Point3}; +use acadrust::kernel::space::NurbsCurve3; use crate::command::{CadCommand, CmdOption, CmdResult}; use crate::entities::common::BulgeArc; @@ -563,51 +562,34 @@ fn spline_control_endpoint(spline: &Spline, click: DVec3) -> Option = spline + .control_points + .iter() + .map(|point| [point.x, point.y, point.z]) + .collect(); + let weights = (spline.weights.len() == count).then(|| { + spline + .weights + .iter() + .map(|weight| if weight.abs() <= EPS { 1.0 } else { *weight }) + .collect() + }); + let curve = NurbsCurve3::new(degree, controls, spline.knots.clone(), weights)?; - if spline.weights.len() == count { - let homogeneous: Vec = spline - .control_points - .iter() - .zip(&spline.weights) - .map(|(point, &weight)| { - let weight = if weight.abs() <= EPS { 1.0 } else { weight }; - Vector4::new(point.x * weight, point.y * weight, point.z * weight, weight) - }) - .collect(); - let curve = NurbsCurve::new(BSplineCurve::new(knots, homogeneous)); - let (start, end) = curve.range_tuple(); - let at_start = click.distance_squared(point_to_dvec(curve.subs(start))) - <= click.distance_squared(point_to_dvec(curve.subs(end))); - let parameter = if at_start { start } else { end }; - curve_frame( - point_to_dvec(curve.subs(parameter)), - vector_to_dvec(curve.der(parameter)), - vector_to_dvec(curve.der2(parameter)), - at_start, - ) - } else { - let controls: Vec = spline - .control_points - .iter() - .map(|point| Point3::new(point.x, point.y, point.z)) - .collect(); - let curve = BSplineCurve::new(knots, controls); - let (start, end) = curve.range_tuple(); - let at_start = click.distance_squared(point_to_dvec(curve.subs(start))) - <= click.distance_squared(point_to_dvec(curve.subs(end))); - let parameter = if at_start { start } else { end }; - curve_frame( - point_to_dvec(curve.subs(parameter)), - vector_to_dvec(curve.der(parameter)), - vector_to_dvec(curve.der2(parameter)), - at_start, - ) - } + let (start, end) = curve.domain(); + let at_start = click.distance_squared(point_to_dvec(curve.point_at_knot(start))) + <= click.distance_squared(point_to_dvec(curve.point_at_knot(end))); + let parameter = if at_start { start } else { end }; + curve_frame( + point_to_dvec(curve.point_at_knot(parameter)), + point_to_dvec(curve.tangent_at_knot(parameter)), + point_to_dvec(curve.acceleration_at_knot(parameter)), + at_start, + ) } fn curve_frame( @@ -666,12 +648,8 @@ fn to_dvec(point: Vector3) -> DVec3 { DVec3::new(point.x, point.y, point.z) } -fn point_to_dvec(point: Point3) -> DVec3 { - DVec3::new(point.x, point.y, point.z) -} - -fn vector_to_dvec(vector: truck_modeling::Vector3) -> DVec3 { - DVec3::new(vector.x, vector.y, vector.z) +fn point_to_dvec(point: [f64; 3]) -> DVec3 { + DVec3::new(point[0], point[1], point[2]) } inventory::submit!(crate::command::CommandRegistration { diff --git a/src/modules/insert/solid3d_cmds.rs b/src/modules/insert/solid3d_cmds.rs index 273fdbc59..95c9a6191 100644 --- a/src/modules/insert/solid3d_cmds.rs +++ b/src/modules/insert/solid3d_cmds.rs @@ -1,7 +1,7 @@ // 2D→3D modelling commands — EXTRUDE, REVOLVE, SWEEP, LOFT. // // Each picks profile/path entities and emits a `CmdResult` whose handler -// builds a truck solid and inserts its MeshModel into scene.meshes under a +// builds a 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`). @@ -13,7 +13,7 @@ use crate::t; use crate::command::{CadCommand, CmdResult}; // 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 +// (`modules::model::primitive_cmd`), which builds them as B-reps cached // for the Design-group boolean tools. EXTRUDE / REVOLVE / SWEEP / LOFT remain // here as 2D→3D operations. diff --git a/src/modules/model/boolean_cmd.rs b/src/modules/model/boolean_cmd.rs index 49f6cd52b..94d926f18 100644 --- a/src/modules/model/boolean_cmd.rs +++ b/src/modules/model/boolean_cmd.rs @@ -1,6 +1,6 @@ // 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. +// `App::solid_boolean` (src/app/model_ops.rs) using the kernel on the +// session-cached 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)] diff --git a/src/modules/model/mod.rs b/src/modules/model/mod.rs index 66293cbd3..08f6f6f51 100644 --- a/src/modules/model/mod.rs +++ b/src/modules/model/mod.rs @@ -2,7 +2,7 @@ // // 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 +// Design group : combine solids with the kernel boolean operations // (union / subtract / intersect). pub mod boolean_cmd; diff --git a/src/modules/model/primitive_cmd.rs b/src/modules/model/primitive_cmd.rs index 9e72918c5..e2d0c6d5f 100644 --- a/src/modules/model/primitive_cmd.rs +++ b/src/modules/model/primitive_cmd.rs @@ -4,7 +4,7 @@ // `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 +// A matching the kernel `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; diff --git a/src/scene/convert/acad_to_truck.rs b/src/scene/convert/acad_to_render.rs similarity index 85% rename from src/scene/convert/acad_to_truck.rs rename to src/scene/convert/acad_to_render.rs index cfdf9c977..f7f1bdb9f 100644 --- a/src/scene/convert/acad_to_truck.rs +++ b/src/scene/convert/acad_to_render.rs @@ -1,7 +1,12 @@ -// acadrust -> truck topology conversion layer. +// acadrust entity -> what the renderer draws. +// +// Every entity becomes one of a handful of shapes: a run of points, a run of +// text strokes, a band of varying width, or a single dot. Curved entities are +// sampled through `entities::curve`, which is where each one's geometry is +// defined once — so a circle drawn here and a circle extruded by the Model +// tab come from the same definition and cannot disagree. use acadrust::{CadDocument, EntityType}; -use truck_modeling::{Edge, Solid, Vertex, Wire}; use crate::entities::traits::EntityTypeOps; use crate::scene::model::wire_model::{SnapHint, TangentGeom}; @@ -43,10 +48,10 @@ pub struct GlyphRun { } #[allow(dead_code)] -pub enum TruckObject { - Point(Vertex), - Curve(Edge), - Contour(Wire), +pub enum RenderObject { + /// A single position, drawn as a dot sized in pixels rather than in world + /// units — what a PDMODE 0 point is. + Dot([f64; 3]), Text(Vec), /// Pre-computed NaN-separated 3-D point list (leader lines, arrowheads, etc.). /// Points are stored in WCS as **f64** so the large world_offset can be @@ -61,11 +66,10 @@ pub enum TruckObject { /// shader interpolates the two endpoint widths of each segment so the band /// tapers smoothly. Points hold no NaN breaks (one continuous band). TaperedLines(Vec<[f64; 3]>, Vec), - Volume(Solid), } -pub struct TruckEntity { - pub object: TruckObject, +pub struct RenderEntity { + pub object: RenderObject, pub snap_pts: Vec<(glam::DVec3, SnapHint)>, pub tangent_geoms: Vec, /// Polyline vertex positions in WCS f64; converted to offset-relative f32 @@ -83,13 +87,13 @@ pub struct TruckEntity { pub pick_tris: Vec<[f64; 3]>, } -pub fn convert(entity: &EntityType, document: &CadDocument) -> Option { - entity.to_truck_entity(document) +pub fn convert(entity: &EntityType, document: &CadDocument) -> Option { + entity.to_render_entity(document) } /// Triangulate the wall swept by extruding `base` along `extrusion` — the /// geometry a DXF thickness (code 39) adds to an entity. Two triangles per -/// base segment, in `TruckEntity::pick_tris` order (flat WCS f64, 3 per tri). +/// base segment, in `RenderEntity::pick_tris` order (flat WCS f64, 3 per tri). /// /// `base` is a point chain in the same form a wire uses, so a NaN entry breaks /// the sweep exactly as it breaks a segment chain — a polyline with disjoint diff --git a/src/scene/convert/curve_tol.rs b/src/scene/convert/curve_tol.rs new file mode 100644 index 000000000..7e0313476 --- /dev/null +++ b/src/scene/convert/curve_tol.rs @@ -0,0 +1,52 @@ +// How finely a curve is sampled, for the whole of one render frame. +// +// Every curve in a drawing is tessellated to a chord height — how far the +// polyline drawn for it may sit from the curve itself — and the right value +// depends on the zoom. At a metre per pixel a millimetre of error is +// invisible; zoomed to a millimetre per pixel it is the whole picture. +// +// The Scene sets this once per frame from `world_per_pixel`, targeting about +// half a pixel. Every tessellation inside that frame reads the same atomic, +// including the ones running on rayon workers, which is why it is a global +// rather than a parameter: threading a tolerance through every entity +// converter's signature would touch every one of them to say the same thing. +// +// Zero means "no frame is being drawn" — a load, a snap, a hit test — and the +// floor is used instead. That is what `BlockCache::build` expects. + +use std::sync::atomic::{AtomicU64, Ordering}; + +/// The finest a curve is ever sampled, in world units, and the value used +/// when no frame has set one. +const CURVE_TOL: f64 = 0.005; + +static CURVE_TOL_BITS: AtomicU64 = AtomicU64::new(0); + +/// Sets the per-frame curve tolerance. `None` — or anything not finite and +/// positive — reverts to the floor. +pub fn set_curve_tol_override(tol: Option) { + let bits = match tol { + Some(value) if value > 0.0 && value.is_finite() => value.to_bits(), + _ => 0, + }; + CURVE_TOL_BITS.store(bits, Ordering::Relaxed); +} + +/// The tolerance to sample at, never below the floor — so zooming a long way +/// in cannot ask for a sampling finer than the baseline quality. +pub(crate) fn current_curve_tol() -> f64 { + let bits = CURVE_TOL_BITS.load(Ordering::Relaxed); + if bits == 0 { + CURVE_TOL + } else { + f64::from_bits(bits).max(CURVE_TOL) + } +} + +/// `Some(tol)` only while a frame's override is in force — that is, while +/// something is being drawn rather than loaded, snapped or hit-tested. Hatch +/// boundaries use it to decide whether zoom-adaptive sampling applies at all. +pub(crate) fn active_curve_tol() -> Option { + let bits = CURVE_TOL_BITS.load(Ordering::Relaxed); + (bits != 0).then(|| f64::from_bits(bits).max(CURVE_TOL)) +} diff --git a/src/scene/convert/mod.rs b/src/scene/convert/mod.rs index ea0ff3c1d..a0e7aa75b 100644 --- a/src/scene/convert/mod.rs +++ b/src/scene/convert/mod.rs @@ -1,9 +1,9 @@ -pub mod acad_to_truck; #[cfg(feature = "solid3d")] +pub mod acad_to_render; pub mod acis_export; pub mod acis_kernel; pub mod dgn_linestyle; -pub mod truck_tess; +pub mod curve_tol; pub mod tessellate; pub(crate) mod tess; pub mod proxy_graphics; diff --git a/src/scene/convert/solid3d_tess.rs b/src/scene/convert/solid3d_tess.rs index df75b6be0..29b132309 100644 --- a/src/scene/convert/solid3d_tess.rs +++ b/src/scene/convert/solid3d_tess.rs @@ -39,9 +39,9 @@ use crate::scene::model::mesh_model::{MeshLodSet, MeshModel}; /// fraction of the curve radius sets their sampling density (~0.002 ⇒ ~50 /// segments per full circle). pub(crate) const EDGE_CHORD_FRAC: f64 = 0.002; -/// Truck's own triangulation chord tolerance for the cone faces still routed +/// The triangulation chord tolerance for the cone faces still routed /// through its kernel, as a fraction of the surface radius. Matches -/// [`LodConfig::HIGH`]: truck emits a single mesh rather than an LOD ladder, so +/// [`LodConfig::HIGH`]: the kernel emits a single mesh rather than an LOD ladder, so /// it has to be the detailed one. A coarse fraction here turns a wide pipe into /// a hexagonal prism whose flats sink far inside the true radius, tearing the /// wall away from the planar faces and caps that meet it. @@ -286,22 +286,22 @@ fn tessellate_acis( facet_res: f64, isolines: usize, ) -> Option { - let truck = crate::scene::convert::acis_kernel::tessellate_sat( + let lifted = crate::scene::convert::acis_kernel::tessellate_sat( sat, name.clone(), color, facet_res, ); - let manual = if truck.as_ref().is_some_and(|set| set.complete) { + let manual = if lifted.as_ref().is_some_and(|set| set.complete) { None } else { tessellate_sat_lods(sat, name.clone(), color, facet_res) }; - let mut set = match (truck, manual) { + let mut set = match (lifted, manual) { (Some(set), _) if set.complete => set, (_, Some(set)) if set.complete => set, - (Some(truck), Some(manual)) => { - let truck_tris = truck + (Some(lifted), Some(manual)) => { + let lifted_tris = lifted .lods .first() .map(|mesh| mesh.indices.len()) @@ -311,10 +311,10 @@ fn tessellate_acis( .first() .map(|mesh| mesh.indices.len()) .unwrap_or(0); - if manual_tris > truck_tris { + if manual_tris > lifted_tris { manual } else { - truck + lifted } } (Some(set), None) | (None, Some(set)) => set, diff --git a/src/scene/convert/spline_tess.rs b/src/scene/convert/spline_tess.rs index eae3f568f..8a4e6ac88 100644 --- a/src/scene/convert/spline_tess.rs +++ b/src/scene/convert/spline_tess.rs @@ -3,7 +3,7 @@ // Lofted / swept / revolved surfaces store their geometry as an ACIS // `nubs` (non-uniform B-spline) block inside the `spline-surface` record. // Rather than evaluate the basis functions by hand, we parse the control net -// and knot vectors out of the SAT tokens, hand them to truck's +// and knot vectors out of the SAT tokens, hand them to the kernel's // `BSplineSurface` (the same NURBS kernel the Model tab already builds on), // and sample its parametric grid into triangles. @@ -12,46 +12,14 @@ use acadrust::entities::acis::{ SatCoedge, SatDocument, SatFace, SatLoop, SatPCurve, SatRecord, SatSplineSurface, SatToken, }; use rustc_hash::FxHashSet; -use truck_modeling::base::{Vector3, Vector4}; -use truck_modeling::{ - BSplineSurface, KnotVec, NurbsSurface, ParametricSurface, ParametricSurface3D, Point3, -}; +use acadrust::kernel::space::NurbsSurface3; use crate::scene::convert::solid3d_tess::LodConfig; -/// A `spline-surface` block is either a non-rational `nubs` (control points are -/// plain xyz) or a rational `nurbs` (control points carry a weight). They share -/// the parametric-surface interface, so tessellation treats them uniformly. -enum SplineSurf { - Bs(BSplineSurface), - Nurbs(NurbsSurface), -} - -impl SplineSurf { - fn parameter_range( - &self, - ) -> ( - (std::ops::Bound, std::ops::Bound), - (std::ops::Bound, std::ops::Bound), - ) { - match self { - SplineSurf::Bs(s) => s.parameter_range(), - SplineSurf::Nurbs(s) => s.parameter_range(), - } - } - fn subs(&self, u: f64, v: f64) -> Point3 { - match self { - SplineSurf::Bs(s) => s.subs(u, v), - SplineSurf::Nurbs(s) => s.subs(u, v), - } - } - fn normal(&self, u: f64, v: f64) -> Vector3 { - match self { - SplineSurf::Bs(s) => s.normal(u, v), - SplineSurf::Nurbs(s) => s.normal(u, v), - } - } -} +// A `spline-surface` block is either a non-rational `nubs`, whose control +// points are plain xyz, or a rational `nurbs`, whose carry a weight. The +// kernel's surface holds both — weights absent means polynomial — so there is +// nothing here to tell apart. /// Tessellate one `spline-surface` face by sampling its B-spline surface. /// Appends triangles to the shared mesh buffers; a no-op when the surface @@ -74,9 +42,7 @@ pub fn tess_spline_face( // Sample over the knot domain and clip cells against ACIS pcurves when the // face carries a parametric trim. This preserves holes and non-rectangular // spline faces instead of always filling the complete UV rectangle. - let (u_range, v_range) = surface.parameter_range(); - let (u0, u1) = range_bounds(u_range); - let (v0, v1) = range_bounds(v_range); + let ((u0, u1), (v0, v1)) = surface.domain(); if !(u1 > u0) || !(v1 > v0) { return false; } @@ -95,13 +61,17 @@ pub fn tess_spline_face( let v = v0 + (v1 - v0) * (j as f64 / sv as f64); for i in 0..=su { let u = u0 + (u1 - u0) * (i as f64 / su as f64); - let p = surface.subs(u, v); - let mut n = surface.normal(u, v); + let p = surface.point_at_knot(u, v); + // A pole, or a row of coincident control points, has no plane to + // be perpendicular to. Up is as good an answer as any there and + // better than an invented one, since the patch has no area at + // that point to shade. + let mut n = surface.normal_at_knot(u, v).unwrap_or([0.0, 0.0, 1.0]); if reversed { - n = -n; + n = [-n[0], -n[1], -n[2]]; } - verts.push([p.x, p.y, p.z]); - normals.push([n.x as f32, n.y as f32, n.z as f32]); + verts.push(p); + normals.push([n[0] as f32, n[1] as f32, n[2] as f32]); } } @@ -270,23 +240,9 @@ fn point_in_polygon(point: (f64, f64), polygon: &[(f64, f64)]) -> bool { inside } -/// Extract the inclusive `[start, end]` bounds from a truck parameter range. -fn range_bounds(r: (std::ops::Bound, std::ops::Bound)) -> (f64, f64) { - use std::ops::Bound::*; - let lo = match r.0 { - Included(v) | Excluded(v) => v, - Unbounded => 0.0, - }; - let hi = match r.1 { - Included(v) | Excluded(v) => v, - Unbounded => 1.0, - }; - (lo, hi) -} - /// Parse the `nubs` control net + knot vectors out of a `spline-surface` -/// record's token stream into a truck `BSplineSurface`. -fn build_spline_surface(sat: &SatDocument, rec: &SatRecord) -> Option { +/// record's token stream into a kernel surface. +fn build_spline_surface(sat: &SatDocument, rec: &SatRecord) -> Option { if let Some(surface) = build_decoded_spline_surface(sat, rec) { return Some(surface); } @@ -356,98 +312,68 @@ fn build_spline_surface(sat: &SatDocument, rec: &SatRecord) -> Option = Vec::with_capacity(total); - for _ in 0..total { - let x = read_float(toks, &mut p)?; - let y = read_float(toks, &mut p)?; - let z = read_float(toks, &mut p)?; - let w = read_float(toks, &mut p)?; - flat.push(Vector4::new(x * w, y * w, z * w, w)); - } - let mut ctrl = vec![Vec::with_capacity(n_ctrl_v); n_ctrl_u]; - for v in 0..n_ctrl_v { - for u in 0..n_ctrl_u { - ctrl[u].push(flat[v * n_ctrl_u + u]); - } - } - let bs = match BSplineSurface::try_new((uk, vk), ctrl) { - Ok(surface) => surface, - Err(error) => { - if std::env::var_os("OCS_TESS_DEBUG").is_some() { - eprintln!( - "acis_spline_parse[{}]: rational surface rejected degree={deg_u}x{deg_v} control={n_ctrl_u}x{n_ctrl_v}: {error:?}", - rec.index - ); - } - return None; - } - }; - Some(SplineSurf::Nurbs(NurbsSurface::new(bs))) - } else { - let mut flat: Vec = Vec::with_capacity(total); - for _ in 0..total { - let x = read_float(toks, &mut p)?; - let y = read_float(toks, &mut p)?; - let z = read_float(toks, &mut p)?; - flat.push(Point3::new(x, y, z)); - } - let mut ctrl = vec![Vec::with_capacity(n_ctrl_v); n_ctrl_u]; - for v in 0..n_ctrl_v { - for u in 0..n_ctrl_u { - ctrl[u].push(flat[v * n_ctrl_u + u]); - } - } - let bs = match BSplineSurface::try_new((uk, vk), ctrl) { - Ok(surface) => surface, - Err(error) => { - if std::env::var_os("OCS_TESS_DEBUG").is_some() { - eprintln!( - "acis_spline_parse[{}]: surface rejected degree={deg_u}x{deg_v} control={n_ctrl_u}x{n_ctrl_v}: {error:?}", - rec.index - ); - } - return None; - } - }; - Some(SplineSurf::Bs(bs)) + let mut flat: Vec<[f64; 3]> = Vec::with_capacity(total); + let mut flat_weights: Vec = Vec::with_capacity(total); + for _ in 0..total { + let x = read_float(toks, &mut p)?; + let y = read_float(toks, &mut p)?; + let z = read_float(toks, &mut p)?; + // A rational net stores the weight alongside each point, and the + // point itself unweighted — the kernel carries the two separately and + // does the homogeneous multiply where it belongs. + flat_weights.push(if rational { read_float(toks, &mut p)? } else { 1.0 }); + flat.push([x, y, z]); } + let mut net = vec![Vec::with_capacity(n_ctrl_v); n_ctrl_u]; + let mut weights = vec![Vec::with_capacity(n_ctrl_v); n_ctrl_u]; + for v in 0..n_ctrl_v { + for u in 0..n_ctrl_u { + net[u].push(flat[v * n_ctrl_u + u]); + weights[u].push(flat_weights[v * n_ctrl_u + u]); + } + } + let surface = NurbsSurface3::new( + deg_u, + deg_v, + net, + u_knots, + v_knots, + rational.then_some(weights), + ); + if surface.is_none() && std::env::var_os("OCS_TESS_DEBUG").is_some() { + eprintln!( + "acis_spline_parse[{}]: surface rejected degree={deg_u}x{deg_v} control={n_ctrl_u}x{n_ctrl_v}", + rec.index + ); + } + surface } -fn build_decoded_spline_surface(sat: &SatDocument, rec: &SatRecord) -> Option { +fn build_decoded_spline_surface(sat: &SatDocument, rec: &SatRecord) -> Option { let spline = SatSplineSurface::from_record(rec)?; let decoded = spline.bspline(sat)?; - let uk = KnotVec::from(decoded.u_knots); - let vk = KnotVec::from(decoded.v_knots); - let mut ctrl = vec![Vec::with_capacity(decoded.control_count_v); decoded.control_count_u]; - if decoded.rational { - for v in 0..decoded.control_count_v { - for u in 0..decoded.control_count_u { - let point = decoded.control_points[v * decoded.control_count_u + u]; - ctrl[u].push(Vector4::new(point[0], point[1], point[2], point[3])); - } + // Row-major with u varying fastest, which is how ACIS writes it and the + // other way round from the net the kernel reads. + let mut net = vec![Vec::with_capacity(decoded.control_count_v); decoded.control_count_u]; + let mut weights = vec![Vec::with_capacity(decoded.control_count_v); decoded.control_count_u]; + for v in 0..decoded.control_count_v { + for u in 0..decoded.control_count_u { + let point = decoded.control_points[v * decoded.control_count_u + u]; + net[u].push([point[0], point[1], point[2]]); + weights[u].push(point[3]); } - let surface = BSplineSurface::try_new((uk, vk), ctrl).ok()?; - Some(SplineSurf::Nurbs(NurbsSurface::new(surface))) - } else { - let mut points = vec![Vec::with_capacity(decoded.control_count_v); decoded.control_count_u]; - for v in 0..decoded.control_count_v { - for u in 0..decoded.control_count_u { - let point = decoded.control_points[v * decoded.control_count_u + u]; - points[u].push(Point3::new(point[0], point[1], point[2])); - } - } - let surface = BSplineSurface::try_new((uk, vk), points).ok()?; - Some(SplineSurf::Bs(surface)) } + NurbsSurface3::new( + decoded.degree_u, + decoded.degree_v, + net, + decoded.u_knots, + decoded.v_knots, + decoded.rational.then_some(weights), + ) } /// Read `count` `(knot value, multiplicity)` pairs into an expanded raw knot diff --git a/src/scene/convert/tess_util.rs b/src/scene/convert/tess_util.rs index 01ca0c4af..e60d87fc0 100644 --- a/src/scene/convert/tess_util.rs +++ b/src/scene/convert/tess_util.rs @@ -11,7 +11,7 @@ use glam::Vec3; use crate::scene::model::wire_model::{SnapHint, TangentGeom, WireModel}; /// Output of the fallback per-entity geometry path used by entities not -/// covered by the truck topology pipeline (Viewport, Insert, Hatch +/// covered by the render conversion pipeline (Viewport, Insert, Hatch /// outline, Ole2Frame). Tuple form preserved to avoid touching every /// callsite when the dispatcher wraps these into a WireModel. /// @@ -91,13 +91,13 @@ pub fn fill_chord_tol(radius: f64) -> f64 { } /// Chord tolerance for the per-frame wire outline: pulls the active -/// `truck_tess::set_curve_tol_override` value (Scene sets it to +/// `curve_tol::set_curve_tol_override` value (Scene sets it to /// `world_per_pixel × 0.5` so curves stay at ~half-pixel chord error at /// the current zoom). When no override is active (snap / hit-test /// passes, load-time builds), falls back to [`fill_chord_tol`] so we /// never under-sample. pub fn wire_chord_tol(radius: f64) -> f64 { - match crate::scene::convert::truck_tess::active_curve_tol() { + match crate::scene::convert::curve_tol::active_curve_tol() { Some(t) => t.min(fill_chord_tol(radius)).max(1e-9), None => fill_chord_tol(radius), } diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 61785dbc7..7bf2dcc90 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -2,15 +2,15 @@ // // Flow: // EntityType -// ↓ acad_to_truck::convert() -// TruckEntity { object: TruckObject, snap_pts, tangent_geoms, key_vertices } -// ↓ truck_tess::tessellate_*() -// TruckTessResult::Lines → WireModel -// TruckTessResult::Point → WireModel (small cross) -// TruckTessResult::Mesh → MeshModel -// TruckObject::Text → one WireModel per glyph stroke (elevation from entity Z) +// ↓ acad_to_render::convert() +// RenderEntity { object: RenderObject, snap_pts, tangent_geoms, key_vertices } +// ↓ curve_tol::tessellate_*() +// RenderObject::Lines → WireModel +// RenderObject::Dot → WireModel (a dot sized in pixels) +// RenderObject::Text → WireModel (glyph strokes) + SDF quads +// RenderObject::Text → one WireModel per glyph stroke (elevation from entity Z) // -// Entities not handled by acad_to_truck (Viewport, Insert, Hatch, Ole2Frame) +// Entities not handled by acad_to_render (Viewport, Insert, Hatch, Ole2Frame) // are tessellated by the FallbackTess fallback_geometry() path. use crate::entities::leader::LeaderTess; @@ -18,10 +18,7 @@ use acadrust::types::Color as AcadColor; use acadrust::{CadDocument, EntityType, Handle}; use glam::Vec3; -use crate::scene::convert::acad_to_truck::{convert, TruckObject}; -use crate::scene::convert::truck_tess::{ - tessellate_edge, tessellate_vertex, tessellate_wire, TruckTessResult, -}; +use crate::scene::convert::acad_to_render::{convert, RenderObject}; use crate::scene::model::wire_model::{SnapHint, WireModel}; /// Split an f64 offset-relative coordinate into the double-single (high, low) @@ -190,7 +187,7 @@ pub fn tessellate( // MLINE emits one WireModel per style element so each parallel line keeps // its own colour and linetype — a red Continuous line under a yellow dashed // line reads as the two-tone multiline the style defines. Handled here, like - // Leader, because the single-colour truck `Lines` path can't carry + // Leader, because the single-colour the kernel `Lines` path can't carry // per-element colour. if let EntityType::MLine(m) = entity { let lines = crate::entities::mline::mline_lines(m, document); @@ -422,11 +419,11 @@ pub fn tessellate( return out; } - // ── Try the truck path first ─────────────────────────────────────────── + // ── Try the kernel path first ─────────────────────────────────────────── // Relative-PDSIZE points size their glyph from the current zoom so they // stay a roughly constant on-screen size; otherwise the header-driven path. - let te = crate::entities::point::relative_truck(entity, document, world_per_pixel) - .or_else(|| crate::entities::light::relative_truck(entity, document, world_per_pixel)) + let te = crate::entities::point::relative_render(entity, document, world_per_pixel) + .or_else(|| crate::entities::light::relative_render(entity, document, world_per_pixel)) .or_else(|| convert(entity, document)); if let Some(te) = te { match te.object { @@ -438,7 +435,7 @@ pub fn tessellate( // groups by override colour and emit one WireModel per bin so a // single MTEXT can hand back N colour-distinct wires when the // value mixes inline colours. - TruckObject::Text(stroke_groups) => { + RenderObject::Text(stroke_groups) => { let entity_zf = entity_z(entity) as f64; let elev_v = entity_zf; @@ -990,10 +987,22 @@ pub fn tessellate( } // ── Standard topology objects ───────────────────────────────── - TruckObject::Point(v) => { - let result = tessellate_vertex(&v); - match result { - TruckTessResult::Point([x, y, z], [xl, yl, zl]) => { + RenderObject::Dot(position) => { + { + { + // Split into a coarse float and a fine correction, so + // a point at survey coordinates keeps its last + // millimetres instead of losing them to f32. + let [x, y, z] = [ + position[0] as f32, + position[1] as f32, + position[2] as f32, + ]; + let [xl, yl, zl] = [ + (position[0] - x as f64) as f32, + (position[1] - y as f64) as f32, + (position[2] - z as f64) as f32, + ]; // A PDMODE=0 point is a single dot. Size its marker to // ~1 px so it reads as a dot rather than a large // world-space "+" in small drawings — otherwise the @@ -1048,97 +1057,12 @@ pub fn tessellate( fill_tris_low: Vec::new(), }]; } - _ => {} } } - TruckObject::Curve(e) => { - if let TruckTessResult::Lines(points, points_low) = - tessellate_edge(&e) - { - let snap_pts = te.snap_pts; - let key_vertices: Vec<[f64; 3]> = te - .key_vertices - .into_iter() - .map(|[x, y, z]| [x, y, z]) - .collect(); - return vec![WireModel { - taper_widths: Vec::new(), - world_width: 0.0, - depth_override: None, - fill_is_3d: false, - fill_is_2d_solid: false, - pick_tris: Vec::new(), - pick_tris_low: Vec::new(), - dash_from_start: false, - dash_align_end: None, - text_verts: Vec::new(), - name, - points, - points_low, - color, - selected, - pattern_length, - pattern, - line_weight_px, - snap_pts, - tangent_geoms: te.tangent_geoms, - aci: 0, - key_vertices, - aabb: WireModel::UNBOUNDED_AABB, - plinegen: true, - fill_tris: vec![], - fill_tris_low: Vec::new(), - }]; - } - } - TruckObject::Contour(w) => { - if let TruckTessResult::Lines(points, points_low) = - tessellate_wire(&w) - { - let snap_pts = te.snap_pts; - let key_vertices: Vec<[f64; 3]> = te - .key_vertices - .into_iter() - .map(|[x, y, z]| [x, y, z]) - .collect(); - // A wide polyline arrives here: the shader expands this - // centre-line to `world_width` so the band IS the wire (the - // linetype dashes it); the band also backs pick. - let (pick_tris, pick_tris_low) = points_to_ds(te.pick_tris); - return vec![WireModel { - taper_widths: Vec::new(), - world_width: polyline_band_width(entity), - depth_override: None, - fill_is_3d: false, - fill_is_2d_solid: false, - pick_tris, - pick_tris_low, - dash_from_start: false, - dash_align_end: None, - text_verts: Vec::new(), - name, - points, - points_low, - color, - selected, - pattern_length, - pattern, - line_weight_px, - snap_pts, - tangent_geoms: te.tangent_geoms, - aci: 0, - key_vertices, - aabb: WireModel::UNBOUNDED_AABB, - plinegen: true, - fill_tris: vec![], - fill_tris_low: Vec::new(), - }]; - } - } - TruckObject::Lines(points) => { + RenderObject::Lines(points) => { // Points are world-space f64 from entity converters (polyline, // leader, mesh, solid2d, etc.). Subtract world_offset in f64 // and split into double-single (high, low) f32 buffers — the @@ -1312,7 +1236,7 @@ pub fn tessellate( return out; } - TruckObject::SegmentedLines(points) => { + RenderObject::SegmentedLines(points) => { let (local_pts, local_pts_low) = points_to_ds(points); let snap_pts = te.snap_pts; let key_vertices: Vec<[f64; 3]> = te @@ -1353,7 +1277,7 @@ pub fn tessellate( }]; } - TruckObject::TaperedLines(points, widths) => { + RenderObject::TaperedLines(points, widths) => { // A wide polyline whose width varies: one continuous band wire // carrying a per-point width; the shader interpolates each // segment's two endpoint widths. `world_width` (the widest edge) @@ -1398,21 +1322,6 @@ pub fn tessellate( }]; } - TruckObject::Volume(_) => { - // Solid3D / Region / Body → mesh tessellation lives in - // `solid3d_tess`. As a wire fallback, render the pre-computed - // edge wires stored in the entity when present (e.g. from - // SOLVIEW output or when the SAT kernel cannot parse the - // ACIS data). - let wire_pts = solid_wire_fallback(entity); - let mut wm = WireModel::solid_f64(name, wire_pts, color, selected); - // Add insertion snap at point_of_reference. - if let Some(p) = crate::entities::solid3d::point_of_reference(entity) { - let sp = glam::DVec3::new(p.x, p.y, p.z); - wm.snap_pts.push((sp, SnapHint::Insertion)); - } - return vec![wm]; - } } } diff --git a/src/scene/convert/truck_tess.rs b/src/scene/convert/truck_tess.rs deleted file mode 100644 index d92a1cc1a..000000000 --- a/src/scene/convert/truck_tess.rs +++ /dev/null @@ -1,287 +0,0 @@ -// Unified tessellation for all truck topology levels. -// -// Vertex → TruckTessResult::Point -// Edge → TruckTessResult::Lines (curve sampled with tolerance) -// Wire → TruckTessResult::Lines (edges joined, no duplicate junctions) -// Face / Shell / Solid → TruckTessResult::Mesh (triangle mesh via truck-meshalgo) -// -// The caller (acad_to_truck / tessellate) decides which function to call based on -// the truck topology level of the object. - -#[cfg(feature = "solid3d")] -use truck_meshalgo::tessellation::{MeshableShape, MeshedShape}; -use truck_modeling::base::{BoundedCurve, ParameterDivision1D}; -use truck_modeling::{Edge, Shell, Solid, Vertex, Wire}; -#[cfg(feature = "solid3d")] -use truck_polymesh::PolygonMesh; - - -// Chord-height tolerance used for adaptive curve sampling (world units). -const CURVE_TOL: f64 = 0.005; -// Triangle mesh tolerance (world units). -#[allow(dead_code)] -pub const MESH_TOL: f64 = 0.01; - -// ── Zoom-adaptive curve tolerance override ──────────────────────────────── -// -// The Scene sets this once per render frame to a value derived from -// `world_per_pixel` (target ≈ 0.5 px chord height). All Edge tessellations -// inside the frame — including those running on rayon worker threads — read -// from the same atomic, so callers don't need to thread a tolerance -// parameter through every entity-converter signature. Zero means "use -// `CURVE_TOL`"; that is the default, and what BlockCache::build expects. -use std::sync::atomic::{AtomicU64, Ordering}; -static CURVE_TOL_BITS: AtomicU64 = AtomicU64::new(0); - -/// Set the per-frame curve tolerance. `None` (or any non-finite/<=0 value) -/// reverts to the default `CURVE_TOL`. -pub fn set_curve_tol_override(tol: Option) { - let bits = match tol { - Some(t) if t > 0.0 && t.is_finite() => t.to_bits(), - _ => 0, - }; - CURVE_TOL_BITS.store(bits, Ordering::Relaxed); -} - -/// Resolve the active curve tolerance. Clamped to the floor `CURVE_TOL` so -/// extreme zoom-in never under-samples below the existing baseline quality. -pub(crate) fn current_curve_tol() -> f64 { - let bits = CURVE_TOL_BITS.load(Ordering::Relaxed); - if bits == 0 { - CURVE_TOL - } else { - f64::from_bits(bits).max(CURVE_TOL) - } -} - -/// Returns `Some(tol)` only when a Scene-driven per-frame override is -/// active (i.e. we're tessellating inside a render frame, not at load -/// time or in a snap / hit-test pass). Hatch boundary outlines use this -/// to apply zoom-adaptive sampling. -pub(crate) fn active_curve_tol() -> Option { - let bits = CURVE_TOL_BITS.load(Ordering::Relaxed); - if bits == 0 { - None - } else { - Some(f64::from_bits(bits).max(CURVE_TOL)) - } -} - -// ── Public result type ──────────────────────────────────────────────────── - -/// Output of any truck topology tessellation. -#[allow(dead_code)] -pub enum TruckTessResult { - /// A single world-space point (from Vertex). Double-single (high, low). - Point([f32; 3], [f32; 3]), - /// An ordered sequence of points forming a polyline (from Edge or Wire), - /// returned as parallel double-single (high, low) f32 buffers so the GPU - /// keeps sub-unit precision at UTM-scale coordinates. - Lines(Vec<[f32; 3]>, Vec<[f32; 3]>), - /// A triangle mesh (from Face, Shell, or Solid). `verts` is the high half - /// of the double-single position; `verts_low` the paired residual. - Mesh { - verts: Vec<[f32; 3]>, - verts_low: Vec<[f32; 3]>, - normals: Vec<[f32; 3]>, - indices: Vec, - }, -} - -// ── Shared offset helper ────────────────────────────────────────────────── - -/// Convert a truck f64 point to local f32 by subtracting world_offset in f64 -/// before truncating. This preserves sub-unit decimal precision even when the -/// raw world coordinates are in the millions (e.g. Turkish UTM). -#[inline] -fn to_local(x: f64, y: f64, z: f64) -> [f32; 3] { - [ - x as f32, - y as f32, - z as f32, - ] -} - -/// Sub-f32 residual of `to_local`, computed in f64 — the renderer pairs this -/// with the high half so the double-single shader path reconstructs the f64 -/// source even when the high half is quantized to half a metre at UTM scale. -#[inline] -fn to_local_low(x: f64, y: f64, z: f64, hi: [f32; 3]) -> [f32; 3] { - [ - (x - hi[0] as f64) as f32, - (y - hi[1] as f64) as f32, - (z - hi[2] as f64) as f32, - ] -} - -// ── Vertex ──────────────────────────────────────────────────────────────── - -pub fn tessellate_vertex(v: &Vertex) -> TruckTessResult { - let p = v.point(); - let hi = to_local(p.x, p.y, p.z); - let lo = to_local_low(p.x, p.y, p.z, hi); - TruckTessResult::Point(hi, lo) -} - -// ── Edge ────────────────────────────────────────────────────────────────── - -pub fn tessellate_edge(e: &Edge) -> TruckTessResult { - // oriented_curve() respects the edge direction (inverts if needed). - let curve = e.oriented_curve(); - let (t0, t1) = curve.range_tuple(); - // parameter_division samples adaptively: fewer points on straight segments, - // more on tight curves, all within the active chord-height tolerance. - // The Scene scales this with zoom so far-out arcs aren't oversampled. - let (_, mut pts) = curve.parameter_division((t0, t1), current_curve_tol()); - // Keep the rendered endpoints bit-for-bit identical to the topology. - // Evaluating a curve again at t=0/1 can differ from its stored vertices by - // a few f64 ULPs (a straight line's end is computed as p0 + (p1-p0), for - // example). At extreme zoom that turns a shared CAD endpoint into a visible - // gap between independently tessellated entities (#561). - if let Some(first) = pts.first_mut() { - *first = e.front().point(); - } - if pts.len() > 1 { - *pts.last_mut().expect("length checked") = e.back().point(); - } - let mut high: Vec<[f32; 3]> = Vec::with_capacity(pts.len()); - let mut low: Vec<[f32; 3]> = Vec::with_capacity(pts.len()); - for p in &pts { - let hi = to_local(p.x, p.y, p.z); - let lo = to_local_low(p.x, p.y, p.z, hi); - high.push(hi); - low.push(lo); - } - TruckTessResult::Lines(high, low) -} - -// ── Wire ────────────────────────────────────────────────────────────────── - -pub fn tessellate_wire(w: &Wire) -> TruckTessResult { - let mut high: Vec<[f32; 3]> = Vec::new(); - let mut low: Vec<[f32; 3]> = Vec::new(); - for edge in w.edge_iter() { - if let TruckTessResult::Lines(eh, el) = tessellate_edge(edge) { - if high.is_empty() { - high = eh; - low = el; - } else { - // Skip the first point of each continuation edge to avoid - // duplicating the shared junction vertex. - high.extend_from_slice(&eh[1..]); - low.extend_from_slice(&el[1..]); - } - } - } - TruckTessResult::Lines(high, low) -} - -// ── Shell ───────────────────────────────────────────────────────────────── - -#[allow(dead_code)] -#[cfg(feature = "solid3d")] -pub fn tessellate_shell(s: &Shell) -> TruckTessResult { - let meshed = s.triangulation(MESH_TOL); - polygon_to_result(meshed.to_polygon()) -} - -/// Without `solid3d` (e.g. wasm) there is no mesher; a shell yields no mesh. -#[cfg(not(feature = "solid3d"))] -pub fn tessellate_shell(_s: &Shell) -> TruckTessResult { - TruckTessResult::Mesh { - verts: Vec::new(), - verts_low: Vec::new(), - normals: Vec::new(), - indices: Vec::new(), - } -} - -// ── Solid ───────────────────────────────────────────────────────────────── - -#[allow(dead_code)] -#[cfg(feature = "solid3d")] -pub fn tessellate_solid(s: &Solid) -> TruckTessResult { - let meshed = s.triangulation(MESH_TOL); - polygon_to_result(meshed.to_polygon()) -} - -/// Without `solid3d` (e.g. wasm) there is no mesher; a solid yields no mesh. -#[allow(dead_code)] -#[cfg(not(feature = "solid3d"))] -pub fn tessellate_solid(_s: &Solid) -> TruckTessResult { - TruckTessResult::Mesh { - verts: Vec::new(), - verts_low: Vec::new(), - normals: Vec::new(), - indices: Vec::new(), - } -} - -// ── Internal ───────────────────────────────────────────────────────────── - -#[cfg(feature = "solid3d")] -fn polygon_to_result(mesh: PolygonMesh) -> TruckTessResult { - let positions = mesh.positions(); - let mut verts: Vec<[f32; 3]> = Vec::with_capacity(positions.len()); - let mut verts_low: Vec<[f32; 3]> = Vec::with_capacity(positions.len()); - for p in positions.iter() { - let hi = to_local(p.x, p.y, p.z); - verts_low.push(to_local_low(p.x, p.y, p.z, hi)); - verts.push(hi); - } - - // Per-vertex normals: if the mesh has normals, map each triangle vertex's - // normal index back to the normals array. Fall back to empty if absent. - let mesh_normals = mesh.normals(); - - let indices: Vec = mesh - .tri_faces() - .iter() - .flat_map(|tri| [tri[0].pos as u32, tri[1].pos as u32, tri[2].pos as u32]) - .collect(); - - // Build a per-position normal by averaging normals of all faces that share it. - // If mesh has no normals, leave the Vec empty. - let normals: Vec<[f32; 3]> = if !mesh_normals.is_empty() { - let n = verts.len(); - let mut acc = vec![[0.0_f32; 3]; n]; - let mut cnt = vec![0u32; n]; - for tri in mesh.tri_faces() { - for sv in tri { - let pos_idx = sv.pos; - if let Some(nor_idx) = sv.nor { - if let Some(nv) = mesh_normals.get(nor_idx) { - acc[pos_idx][0] += nv.x as f32; - acc[pos_idx][1] += nv.y as f32; - acc[pos_idx][2] += nv.z as f32; - cnt[pos_idx] += 1; - } - } - } - } - acc.iter() - .zip(cnt.iter()) - .map(|(s, &c)| { - if c == 0 { - [0.0, 1.0, 0.0] - } else { - let inv = 1.0 / c as f32; - let nx = s[0] * inv; - let ny = s[1] * inv; - let nz = s[2] * inv; - let len = (nx * nx + ny * ny + nz * nz).sqrt().max(1e-9); - [nx / len, ny / len, nz / len] - } - }) - .collect() - } else { - vec![] - }; - - TruckTessResult::Mesh { - verts, - verts_low, - normals, - indices, - } -} diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 55b7fc0eb..e1f5bccd7 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -1515,21 +1515,11 @@ impl Scene { // where boundaries are often spline curves with // rational weights and short cubic segments). // - // Build a NurbsCurve when `rational`, otherwise a - // plain BSplineCurve, and sample adaptively via - // truck's `parameter_division` at the same chord - // tolerance the fill polygon uses for arcs. + // The kernel's plane NURBS holds the rational and + // the polynomial case alike — weights absent means + // polynomial — and tessellates to a chord tolerance, + // the same one the fill polygon uses for arcs. let degree = spline.degree.max(0) as usize; - let knot_vec = if !spline.knots.is_empty() { - KnotVec::from(spline.knots.clone()) - } else if spline.control_points.len() >= degree + 1 { - KnotVec::uniform_knot(degree, spline.control_points.len() - 1) - } else { - KnotVec::from(vec![]) - }; - let knot_ok = spline.control_points.len() >= 2 - && degree >= 1 - && knot_vec.len() == spline.control_points.len() + degree + 1; // Rough chord-tolerance: 0.1% of the control-poly // diagonal so adaptive sampling produces enough @@ -1550,39 +1540,30 @@ impl Scene { let mut epts: Vec<[f64; 2]> = Vec::new(); let mut sampled = false; - if knot_ok { - if spline.rational { - // NURBS: pack (x, y, 0, w) into Vector4. - let cps: Vec = spline - .control_points - .iter() - .map(|p| { - let w = if p.z.abs() > 1e-12 { p.z } else { 1.0 }; - Vector4::new(p.x * w, p.y * w, 0.0, w) - }) - .collect(); - let bspl = TruckBSpline::new(knot_vec.clone(), cps); - let curve = NurbsCurve::new(bspl); - let (t0, t1) = curve.range_tuple(); - let (_, pts) = curve.parameter_division((t0, t1), tol); - for p in pts { - epts.push(to_xy(p.x, p.y)); - } - sampled = true; - } else { - let cps: Vec = spline - .control_points - .iter() - .map(|p| Point3::new(p.x, p.y, 0.0)) - .collect(); - let bspl = TruckBSpline::new(knot_vec, cps); - let (t0, t1) = bspl.range_tuple(); - let (_, pts) = bspl.parameter_division((t0, t1), tol); - for p in pts { - epts.push(to_xy(p.x, p.y)); - } - sampled = true; + let points: Vec<[f64; 2]> = spline + .control_points + .iter() + .map(|p| [p.x, p.y]) + .collect(); + let weights = spline.rational.then(|| { + spline + .control_points + .iter() + .map(|p| if p.z.abs() > 1e-12 { p.z } else { 1.0 }) + .collect() + }); + if let Some(curve) = acadrust::kernel::geom2d::NurbsCurve::new( + degree, + points, + spline.knots.clone(), + weights, + ) { + for point in + acadrust::kernel::geom2d::Curve::Nurbs(curve).tessellate_within(tol) + { + epts.push(to_xy(point[0], point[1])); } + sampled = epts.len() >= 2; } if !sampled { // Fallback: prefer fit_points (which lie on the diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 947d9c93d..7a105a98f 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1,5 +1,5 @@ // Scene modules grouped by role: -// convert — DXF/ACIS entities → truck solids & tessellated geometry +// convert — DXF/ACIS entities → solids & tessellated geometry // text — LFF stroke + TrueType font engines and shaping // model — per-entity GPU render models (wire, hatch, mesh, image, object) // pick — hit-testing, selection, grips, spatial index, xclip @@ -147,11 +147,6 @@ use acadrust::objects::ObjectType; use acadrust::types::Vector2; use acadrust::{CadDocument, EntityType, Handle, TableEntry}; use glam; -use truck_modeling::{ - base::{BoundedCurve, ParameterDivision1D}, - BSplineCurve as TruckBSpline, KnotVec, NurbsCurve, Point3, Vector4, -}; - use iced::time::Duration; use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet}; use std::cell::RefCell; @@ -1767,7 +1762,7 @@ pub struct Scene { pub viewcube_ucs: glam::Mat4, /// GPU render data for hatch fills, keyed by the DXF entity Handle. pub hatches: HashMap, - /// GPU render data for solid meshes (truck Shell/Solid tessellation). + /// GPU render data for solid meshes (the kernel Shell/Solid tessellation). /// Top-level (layout-owned) solids only, stored in the offset-relative /// render frame and drawn flat. pub meshes: HashMap, @@ -8334,11 +8329,11 @@ impl Scene { struct CurveTolGuard; impl Drop for CurveTolGuard { fn drop(&mut self) { - crate::scene::convert::truck_tess::set_curve_tol_override(None); + crate::scene::convert::curve_tol::set_curve_tol_override(None); } } let _tol_guard = wpp.map(|w| { - crate::scene::convert::truck_tess::set_curve_tol_override(Some((w * 0.5) as f64)); + crate::scene::convert::curve_tol::set_curve_tol_override(Some((w * 0.5) as f64)); CurveTolGuard }); // Per-entity tessellation memo. Same classify/tessellate logic, two diff --git a/src/scene/model/mesh_model.rs b/src/scene/model/mesh_model.rs index c0678a20b..b3a6a520d 100644 --- a/src/scene/model/mesh_model.rs +++ b/src/scene/model/mesh_model.rs @@ -1,4 +1,4 @@ -// Triangle mesh model — produced by truck Shell/Solid tessellation. +// Triangle mesh model — produced by the kernel Shell/Solid tessellation. // // Stored alongside WireModels in the scene; rendered by the mesh pipeline // (wgpu TriangleList with depth test, flat normals). @@ -301,7 +301,7 @@ impl MeshLodSet { } /// Wrap a single MeshModel as a one-LOD set. Used by interactive - /// commands that only produce one tessellation (e.g. truck-based + /// commands that only produce one tessellation (e.g. the kernel-based /// BOX/CYLINDER creation). The LOD selector will pick slot 0 for /// every zoom level. pub fn from_single(mesh: MeshModel) -> Self { diff --git a/src/scene/model/sweep_model.rs b/src/scene/model/sweep_model.rs index 0e451d9ec..e9f3ddedb 100644 --- a/src/scene/model/sweep_model.rs +++ b/src/scene/model/sweep_model.rs @@ -14,7 +14,7 @@ use acadrust::kernel::brep::{self, Body}; use acadrust::kernel::geom2d::Curve; -use acadrust::kernel::space::{Plane, PlanarCurve}; +use acadrust::kernel::space::{PlanarCurve, Plane, Vec3}; use acadrust::EntityType; use crate::entities::curve::entity_curve; @@ -123,6 +123,228 @@ pub fn revolved( ) } +// ── SWEEP and LOFT ────────────────────────────────────────────────────────── +// +// Neither keeps a B-rep — both have only ever produced a mesh — so both are +// built from point lists rather than from topology. A profile becomes the +// points its own curve tessellates to, which is the same source EXTRUDE and +// REVOLVE read, so a circle stays round here too. + +use crate::entities::curve::curve_points; +use crate::scene::model::mesh_model::{MeshLodSet, MeshModel}; + +/// The points a profile entity traces, and whether it closes. +fn outline(entity: &EntityType) -> Option<(Vec<[f64; 3]>, bool)> { + let planar = entity_curve(entity)?; + let closed = planar.curve.is_closed(); + let mut points = curve_points(&planar); + // A closed curve tessellates back to its own start. Carrying the repeat + // would put a zero-width quad in every strip below. + if closed && points.len() > 1 { + let first = points[0]; + let last = points[points.len() - 1]; + if Vec3::from(first).distance(Vec3::from(last)) < 1e-9 { + points.pop(); + } + } + (points.len() >= 2).then_some((points, closed)) +} + +/// SWEEP: drag a profile along a path. +/// +/// The path contributes its direction and length, not its shape — which is +/// what SWEEP has always done here, and what makes it an extrusion along an +/// arbitrary vector rather than along a curve. +pub fn swept(profile: &EntityType, path: &EntityType, color: [f32; 4]) -> Option { + let (points, closed) = outline(profile)?; + let along = { + let track = curve_points(&entity_curve(path)?); + let (from, to) = (Vec3::from(*track.first()?), Vec3::from(*track.last()?)); + to - from + }; + if along.length() < 1e-12 { + return None; + } + let moved: Vec<[f64; 3]> = points + .iter() + .map(|point| (Vec3::from(*point) + along).to_array()) + .collect(); + let mut mesh = Ribbon::default(); + mesh.band(&points, &moved, closed); + if closed { + // An open profile sweeps into a sheet with nothing to cap. + mesh.cap(&points, true); + mesh.cap(&moved, false); + } + mesh.finish(color) +} + +/// LOFT: rule a surface through a run of profiles. +/// +/// Consecutive profiles are joined by a band each, and the two ends are +/// capped when they close. Profiles with different point counts are resampled +/// onto the finer of the two, so a circle lofted to a square does not twist. +pub fn lofted(profiles: &[EntityType], color: [f32; 4]) -> Option { + let sections: Vec<(Vec<[f64; 3]>, bool)> = profiles.iter().filter_map(outline).collect(); + if sections.len() < 2 { + return None; + } + let mut mesh = Ribbon::default(); + for pair in sections.windows(2) { + let count = pair[0].0.len().max(pair[1].0.len()); + let closed = pair[0].1 && pair[1].1; + let lower = resampled(&pair[0].0, count, pair[0].1); + let upper = resampled(&pair[1].0, count, pair[1].1); + mesh.band(&lower, &upper, closed); + } + if sections.first()?.1 { + mesh.cap(§ions.first()?.0, true); + } + if sections.last()?.1 { + mesh.cap(§ions.last()?.0, false); + } + mesh.finish(color) +} + +/// A ring walked in `count` even steps along its own length. +/// +/// Even by distance rather than by index: two profiles given at different +/// densities line up where they are, so a band between them does not twist +/// wherever one of them happened to be sampled more finely. +fn resampled(points: &[[f64; 3]], count: usize, closed: bool) -> Vec<[f64; 3]> { + let mut ring: Vec = points.iter().map(|point| Vec3::from(*point)).collect(); + if closed { + ring.push(ring[0]); + } + let mut walked = vec![0.0]; + for pair in ring.windows(2) { + walked.push(walked[walked.len() - 1] + pair[0].distance(pair[1])); + } + let total = *walked.last().unwrap_or(&0.0); + if total <= 0.0 { + return points.to_vec(); + } + let steps = if closed { count } else { count.max(2) - 1 }; + (0..if closed { count } else { count.max(2) }) + .map(|step| { + let want = total * step as f64 / steps as f64; + let at = walked + .iter() + .rposition(|reached| *reached <= want) + .unwrap_or(0) + .min(ring.len() - 2); + let span = walked[at + 1] - walked[at]; + let along = if span > 0.0 { (want - walked[at]) / span } else { 0.0 }; + ring[at].lerp(ring[at + 1], along).to_array() + }) + .collect() +} + +/// Triangles being gathered from bands and caps. +#[derive(Default)] +struct Ribbon { + positions: Vec<[f64; 3]>, + normals: Vec<[f64; 3]>, + triangles: Vec<[u32; 3]>, +} + +impl Ribbon { + /// A strip of quads between two rings of the same length. + fn band(&mut self, lower: &[[f64; 3]], upper: &[[f64; 3]], closed: bool) { + let count = lower.len().min(upper.len()); + if count < 2 { + return; + } + let spans = if closed { count } else { count - 1 }; + for step in 0..spans { + let next = (step + 1) % count; + self.quad(lower[step], lower[next], upper[next], upper[step]); + } + } + + /// A flat lid over a closed ring, fanned from its middle. + /// + /// A fan rather than a proper triangulation: a lofted section can be + /// concave and a fan would then cover ground outside it, but every + /// profile these commands accept is a single closed curve, and the middle + /// of one is inside it. + fn cap(&mut self, ring: &[[f64; 3]], downward: bool) { + if ring.len() < 3 { + return; + } + let mut middle = Vec3::new(0.0, 0.0, 0.0); + for point in ring { + middle = middle + Vec3::from(*point); + } + let middle = (middle / ring.len() as f64).to_array(); + for step in 0..ring.len() { + let next = (step + 1) % ring.len(); + if downward { + self.triangle(middle, ring[next], ring[step]); + } else { + self.triangle(middle, ring[step], ring[next]); + } + } + } + + fn quad(&mut self, a: [f64; 3], b: [f64; 3], c: [f64; 3], d: [f64; 3]) { + self.triangle(a, b, c); + self.triangle(a, c, d); + } + + fn triangle(&mut self, a: [f64; 3], b: [f64; 3], c: [f64; 3]) { + let Some(normal) = (Vec3::from(b) - Vec3::from(a)) + .cross(Vec3::from(c) - Vec3::from(a)) + .normalize() + else { + // Collapsed: no normal, and nothing to draw. + return; + }; + let base = self.positions.len() as u32; + for corner in [a, b, c] { + self.positions.push(corner); + self.normals.push(normal.to_array()); + } + self.triangles.push([base, base + 1, base + 2]); + } + + /// The gathered triangles as the renderer's mesh, or `None` for none. + fn finish(self, color: [f32; 4]) -> Option { + if self.triangles.is_empty() { + return None; + } + // The renderer holds each position as a coarse float plus a fine + // correction, so a profile at survey coordinates keeps its last + // millimetres instead of losing them to f32. + let mut verts = Vec::with_capacity(self.positions.len()); + let mut verts_low = Vec::with_capacity(self.positions.len()); + for point in &self.positions { + let high = [point[0] as f32, point[1] as f32, point[2] as f32]; + verts.push(high); + verts_low.push([ + (point[0] - high[0] as f64) as f32, + (point[1] - high[1] as f64) as f32, + (point[2] - high[2] as f64) as f32, + ]); + } + Some(MeshLodSet::from_single(MeshModel { + name: String::new(), + verts, + verts_low, + normals: self + .normals + .iter() + .map(|n| [n[0] as f32, n[1] as f32, n[2] as f32]) + .collect(), + indices: self.triangles.iter().flatten().copied().collect(), + triangle_material_handles: Vec::new(), + triangle_colors: Vec::new(), + color, + selected: false, + })) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/scene/project.rs b/src/scene/project.rs index f4ab774dd..8e03aa648 100644 --- a/src/scene/project.rs +++ b/src/scene/project.rs @@ -593,8 +593,8 @@ pub(crate) fn clip_boundary_polygon_for_document( else { return vec![]; }; - // Circles and ellipses tessellate directly — their `to_truck` returns a - // parametric TruckObject (not `Lines`), so extracting a polygon there + // Circles and ellipses tessellate directly — their `to_render` returns a + // parametric RenderObject (not `Lines`), so extracting a polygon there // would come back empty. Everything else (splines, polylines, …) reuses // the entity's own `Lines` tessellation. const N: usize = 64; @@ -688,20 +688,16 @@ pub(crate) fn clip_boundary_polygon_for_document( ] }) .collect(), - _ => { - use crate::entities::traits::EntityTypeOps; - let Some(te) = entity.to_truck_entity(document) else { - return vec![]; - }; - if let crate::scene::convert::acad_to_truck::TruckObject::Lines(pts) = te.object { - pts.into_iter() - .filter(|p| p[0].is_finite() && p[1].is_finite()) - .map(|p| [p[0] as f32, p[1] as f32, z]) - .collect() - } else { - vec![] - } - } + // Anything else that draws as a plane curve — a circle, an ellipse, a + // bulged polyline, a spline — is sampled through its own geometry, + // which is where every entity's curve is defined once. + _ => crate::entities::curve::entity_curve(entity) + .map(|planar| crate::entities::curve::curve_points(&planar)) + .unwrap_or_default() + .into_iter() + .filter(|point| point[0].is_finite() && point[1].is_finite()) + .map(|point| [point[0] as f32, point[1] as f32, z]) + .collect(), } }