refactor: drop truck, and evaluate every curve through the kernel

The last four truck crates are gone from the manifest. What they were doing
falls into three parts, and each now has one answer instead of two.

NURBS evaluation. truck was carrying B-spline curves and surfaces for the
SPLINE entity, hatch boundaries, BLEND's endpoint frames, the spline preview
and ACIS spline-surface faces. The kernel now has both, over one de Boor
written across however many coordinates a control point holds — so a plane
curve and a space curve cannot drift apart, and a surface is the same
algorithm applied twice. The rational and the polynomial cases stop being
separate types: weights absent means polynomial.

The entity conversion. Every LINE, ARC, ELLIPSE, SPLINE and polyline was
built into truck topology purely so it could be sampled back into points.
They are sampled through `entities::curve` now, which is where each one's
geometry is already defined once and what EXTRUDE and REVOLVE read — so a
circle drawn on screen and a circle handed to the Model tab come from the
same definition. That retires four of TruckObject's variants and the module
is renamed for what it does.

SWEEP and LOFT. Both only ever produced a mesh, so both are built from point
lists: a band of quads per span, and a lid where a profile closes. Lofting
profiles of different densities resamples them by distance rather than by
index, so a circle lofted to a square no longer twists.

Two things worth noting for anyone reading the old comments. The tolerance
globals that lived in the tessellation module were never about truck at all —
they are the per-frame chord height, and they move to `curve_tol`. And the
rule that a profile handed over as `Lines` silently broke EXTRUDE and REVOLVE
no longer holds: those read the curve directly and never look at this channel.

The two `automation` test failures are unchanged from before this and are not
caused by it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-08-10 01:08:50 +03:00
commit 8c7ebdc9f1
58 changed files with 946 additions and 1760 deletions

277
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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<truck_modeling::Wire> = 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<crate::scene::model::mesh_model::MeshModel> = (|| {
if wires.len() < 2 {
return None;
}
// Build ruled shells between consecutive profile pairs.
let mut all_faces: Vec<truck_modeling::Face> = 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<acadrust::EntityType> = 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;

View file

@ -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

View file

@ -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<Message> {
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<Handle> = 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<Message> {
let i = self.active_tab;
let handles: Vec<Handle> = 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<Message> {
let i = self.active_tab;
let handles: Vec<Handle> = self.tabs[i]

View file

@ -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<acadrust::Handle> = 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<Message> {
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<Message> {
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(

View file

@ -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,

View file

@ -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<TruckEntity> {
Some(to_truck(self))
impl RenderConvertible for Arc {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
Some(to_render(self))
}
}

View file

@ -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<TruckEntity> {
impl RenderConvertible for AttributeDefinition {
fn to_render(&self, document: &acadrust::CadDocument) -> Option<RenderEntity> {
// 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<TruckEntity> {
Some(build_attr_truck(
impl RenderConvertible for AttributeEntity {
fn to_render(&self, document: &acadrust::CadDocument) -> Option<RenderEntity> {
Some(build_attr_render(
AttrTextInputs {
value: &self.value,
insertion_point: self.insertion_point,

View file

@ -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<TruckEntity> {
Some(to_truck(self))
impl RenderConvertible for Circle {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
Some(to_render(self))
}
}

View file

@ -590,7 +590,7 @@ pub fn parse_angle_deg(value: &str) -> Option<f64> {
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.
///

View file

@ -344,7 +344,7 @@ pub fn entity_curve_xy(entity: &EntityType) -> Option<Curve> {
/// 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

View file

@ -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<acadrust::Handle>,
active_viewport: Option<acadrust::Handle>,
@ -1478,7 +1478,7 @@ fn tessellate_dimension_inner(
) -> Vec<WireModel> {
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| {

View file

@ -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<Point3> = 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::<Wire>())
} 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<Point3>) -> Curve {
let spans = points.len().saturating_sub(1);
Curve::BSplineCurve(BSplineCurve::new(KnotVec::uniform_knot(1, spans), points))
}
fn grips(ell: &Ellipse) -> Vec<GripDef> {
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<TruckEntity> {
Some(to_truck(self))
impl RenderConvertible for Ellipse {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
Some(to_render(self))
}
}

View file

@ -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<TruckEntity> {
fn to_render(entity: &ExtendedEntity, document: &acadrust::CadDocument) -> Option<RenderEntity> {
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<TruckEntity> {
to_truck(self, document)
impl RenderConvertible for ExtendedEntity {
fn to_render(&self, document: &acadrust::CadDocument) -> Option<RenderEntity> {
to_render(self, document)
}
}

View file

@ -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<TruckEntity> {
self.spline.to_truck(document)
impl RenderConvertible for Helix {
fn to_render(&self, document: &acadrust::CadDocument) -> Option<RenderEntity> {
self.spline.to_render(document)
}
}

View file

@ -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<TruckEntity> {
impl RenderConvertible for Leader {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
if self.vertices.is_empty() {
return None;
}
Some(to_truck(self))
Some(to_render(self))
}
}

View file

@ -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<f32>,
) -> Option<TruckEntity> {
) -> Option<RenderEntity> {
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<TruckEntity> {
impl RenderConvertible for Light {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
Some(build(self, FIXED_WORLD))
}
}

View file

@ -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<TruckEntity> {
Some(to_truck(self))
impl RenderConvertible for Line {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
Some(to_render(self))
}
}

View file

@ -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<TangentGeom>,
) -> 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<TangentGeom>,
) -> 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<Edge> = Vec::new();
let mut tangents: Vec<TangentGeom> = 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::<Wire>()),
// 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<TruckEntity> {
Some(to_truck(self))
impl RenderConvertible for LwPolyline {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
Some(to_render(self))
}
}

View file

@ -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<TruckEntity> {
impl RenderConvertible for Face3D {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
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<TruckEntity> {
impl RenderConvertible for PolygonMesh {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
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<TruckEntity> {
impl RenderConvertible for PolyfaceMesh {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
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<TruckEntity> {
impl RenderConvertible for Mesh {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
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,

View file

@ -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<MLineLine
out
}
impl TruckConvertible for MLine {
fn to_truck(&self, document: &acadrust::CadDocument) -> Option<TruckEntity> {
impl RenderConvertible for MLine {
fn to_render(&self, document: &acadrust::CadDocument) -> Option<RenderEntity> {
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,

View file

@ -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<GlyphBox>
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<TruckEntity> {
Some(to_truck(self, document))
impl RenderConvertible for MText {
fn to_render(&self, document: &acadrust::CadDocument) -> Option<RenderEntity> {
Some(to_render(self, document))
}
}

View file

@ -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<TruckEntity> {
fn to_render(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<RenderEntity> {
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<TruckE
return None;
}
Some(TruckEntity {
Some(RenderEntity {
pick_tris: Vec::new(),
object: TruckObject::Lines(points),
object: RenderObject::Lines(points),
snap_pts,
tangent_geoms: tangents,
key_vertices: key_verts,
@ -1120,9 +1120,9 @@ fn reflect_xy_direction(dx: &mut f64, dy: &mut f64, p1: DVec3, p2: DVec3) {
// ── Trait impls ────────────────────────────────────────────────────────────
impl TruckConvertible for MultiLeader {
fn to_truck(&self, document: &acadrust::CadDocument) -> Option<TruckEntity> {
to_truck(self, document)
impl RenderConvertible for MultiLeader {
fn to_render(&self, document: &acadrust::CadDocument) -> Option<RenderEntity> {
to_render(self, document)
}
}

View file

@ -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<TruckEntity> {
Some(to_truck(self))
impl RenderConvertible for Ole2Frame {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
Some(to_render(self))
}
}

View file

@ -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<f32>,
) -> Option<TruckEntity> {
) -> Option<RenderEntity> {
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<GripDef> {
@ -235,9 +233,9 @@ fn apply_transform(pt: &mut Point, t: &EntityTransform) {
});
}
impl TruckConvertible for Point {
fn to_truck(&self, document: &acadrust::CadDocument) -> Option<TruckEntity> {
Some(to_truck(self, document))
impl RenderConvertible for Point {
fn to_render(&self, document: &acadrust::CadDocument) -> Option<RenderEntity> {
Some(to_render(self, document))
}
}

View file

@ -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<TruckEntity> {
impl RenderConvertible for Polyline {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
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<Edge> = Vec::new();
let mut tangents: Vec<TangentGeom> = 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::<Wire>()),
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<TruckEntity> {
impl RenderConvertible for Polyline2D {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
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<TruckEntity> {
impl RenderConvertible for Polyline3D {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
Some(tessellate_polyline3d(self))
}
}

View file

@ -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<TruckEntity> {
impl RenderConvertible for RasterImage {
fn to_render(&self, document: &acadrust::CadDocument) -> Option<RenderEntity> {
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<TruckEntity> {
impl RenderConvertible for Wipeout {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
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()

View file

@ -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<TruckEntity> {
impl RenderConvertible for Ray {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
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<TruckEntity> {
impl RenderConvertible for XLine {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
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]],

View file

@ -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<TruckEntity> {
impl RenderConvertible for Shape {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
// 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]],

View file

@ -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<TruckEntity> {
impl RenderConvertible for Solid {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
// 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(),

View file

@ -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<Vector4> = 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<Point3> = 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::<Vec<_>>();
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::<Vec<_>>();
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<TruckEntity> {
Some(to_truck(self))
impl RenderConvertible for Spline {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
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();
}

View file

@ -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<TruckEntity> {
impl RenderConvertible for Table {
fn to_render(&self, document: &acadrust::CadDocument) -> Option<RenderEntity> {
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![],

View file

@ -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<TruckEntity> {
Some(to_truck(self, document))
impl RenderConvertible for Text {
fn to_render(&self, document: &acadrust::CadDocument) -> Option<RenderEntity> {
Some(to_render(self, document))
}
}

View file

@ -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;

View file

@ -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<TruckEntity> {
impl RenderConvertible for Tolerance {
fn to_render(&self, document: &acadrust::CadDocument) -> Option<RenderEntity> {
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");
};

View file

@ -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<TruckEntity>;
pub trait RenderConvertible {
fn to_render(&self, document: &CadDocument) -> Option<RenderEntity>;
}
/// 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<TruckEntity>;
fn to_render_entity(&self, document: &CadDocument) -> Option<RenderEntity>;
fn grips(&self) -> Vec<GripDef>;
fn grip_menu(&self, grip_id: usize) -> Vec<GripMenuItem>;
fn geometry_properties(&self, text_style_names: &[String]) -> Vec<PropSection>;
@ -284,9 +284,9 @@ macro_rules! impl_entity_basics_with_text_styles {
}
impl EntityTypeOps for EntityType {
fn to_truck_entity(&self, document: &CadDocument) -> Option<TruckEntity> {
fn to_render_entity(&self, document: &CadDocument) -> Option<RenderEntity> {
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,

View file

@ -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<TruckEntity> {
impl RenderConvertible for Underlay {
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
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],

View file

@ -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·(1cos(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() {

View file

@ -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<Point3> = 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()
}

View file

@ -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<EndpointFram
if degree == 0 || count <= degree {
return None;
}
let knots = if spline.knots.len() == count + degree + 1 {
KnotVec::from(spline.knots.clone())
} else {
KnotVec::uniform_knot(degree, count - 1)
};
// The kernel's space curve holds the rational and the polynomial case
// alike — weights absent means polynomial — so there is nothing here to
// tell apart, and a malformed knot vector is replaced with a clamped
// uniform one rather than refused.
let controls: Vec<[f64; 3]> = 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<Vector4> = 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<Point3> = 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 {

View file

@ -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.

View file

@ -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)]

View file

@ -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;

View file

@ -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;

View file

@ -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<TextStroke>),
/// 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<f32>),
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<TangentGeom>,
/// 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<TruckEntity> {
entity.to_truck_entity(document)
pub fn convert(entity: &EntityType, document: &CadDocument) -> Option<RenderEntity> {
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

View file

@ -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<f64>) {
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<f64> {
let bits = CURVE_TOL_BITS.load(Ordering::Relaxed);
(bits != 0).then(|| f64::from_bits(bits).max(CURVE_TOL))
}

View file

@ -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;

View file

@ -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<MeshLodSet> {
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,

View file

@ -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<Point3>),
Nurbs(NurbsSurface<Vector4>),
}
impl SplineSurf {
fn parameter_range(
&self,
) -> (
(std::ops::Bound<f64>, std::ops::Bound<f64>),
(std::ops::Bound<f64>, std::ops::Bound<f64>),
) {
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<f64>, std::ops::Bound<f64>)) -> (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<SplineSurf> {
/// record's token stream into a kernel surface.
fn build_spline_surface(sat: &SatDocument, rec: &SatRecord) -> Option<NurbsSurface3> {
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<SplineSurf
let v_knots = with_clamped_ends(raw_v_knots, clamp_v)?;
// Control points are stored row-major with u varying fastest (a full row
// of u control points per v step). truck wants `ctrl[i_u][j_v]`.
// of u control points per v step). the kernel wants `ctrl[i_u][j_v]`.
let total = n_ctrl_u * n_ctrl_v;
let uk = KnotVec::from(u_knots);
let vk = KnotVec::from(v_knots);
if rational {
// Rational: read x, y, z, w and store the homogeneous point (xw, yw,
// zw, w) that truck's `NurbsSurface` expects.
let mut flat: Vec<Vector4> = 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<Point3> = 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<f64> = 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<SplineSurf> {
fn build_decoded_spline_surface(sat: &SatDocument, rec: &SatRecord) -> Option<NurbsSurface3> {
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

View file

@ -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),
}

View file

@ -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];
}
}
}

View file

@ -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<f64>) {
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<f64> {
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<u32>,
},
}
// ── 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<u32> = 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,
}
}

View file

@ -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<Vector4> = 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<Point3> = 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

View file

@ -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<Handle, HatchModel>,
/// 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<Handle, MeshLodSet>,
@ -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

View file

@ -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 {

View file

@ -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<MeshLodSet> {
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<MeshLodSet> {
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(&sections.first()?.0, true);
}
if sections.last()?.1 {
mesh.cap(&sections.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<Vec3> = 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<MeshLodSet> {
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::*;

View file

@ -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(),
}
}