feat(scene): render 3D surface entities (loft/sweep/extrude/etc.)
ACAD_SURFACE entities now load and render. Their NURBS faces are tessellated by building a truck BSplineSurface from the ACIS `spline-surface` control net and knot vectors, then sampling its parametric grid — reusing the same NURBS kernel the Model tab already builds on. Analytic faces (plane/cone/sphere/torus) continue through the existing tessellator. - New spline_tess.rs: parse the ACIS `nubs` block (degrees, knots with the clamped end-multiplicity fix, control grid) into a truck BSplineSurface and mesh it. - tessellate_surface + Surface wired through the mesh-build, fallback-wire, and entity-classification paths. - Bump acadrust to the commit that adds the Surface entity. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b173f32d5a
commit
9d823363f1
9 changed files with 235 additions and 7 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
|
@ -56,7 +56,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "acadrust"
|
name = "acadrust"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
source = "git+https://github.com/HakanSeven12/acadrust?branch=main#b4ab9ffedfae44b5731301a94c849a3c8f695105"
|
source = "git+https://github.com/HakanSeven12/acadrust?branch=main#3e2afad2d0baf03be584c28111040e13870b9e70"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|
@ -3178,7 +3178,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d"
|
checksum = "0b577e2d69827c4740cba2b52efaad1c4cc7c73042860b199710b3575c68438d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytecount",
|
"bytecount",
|
||||||
"memchr 1.0.2",
|
"memchr 2.8.1",
|
||||||
"nom 8.0.0",
|
"nom 8.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -245,6 +245,7 @@ pub(super) fn entity_type_key(entity: &acadrust::EntityType) -> String {
|
||||||
Solid3D(_) => "solid3d",
|
Solid3D(_) => "solid3d",
|
||||||
Region(_) => "region",
|
Region(_) => "region",
|
||||||
Body(_) => "body",
|
Body(_) => "body",
|
||||||
|
Surface(_) => "surface",
|
||||||
Mesh(_) => "mesh",
|
Mesh(_) => "mesh",
|
||||||
Ray(_) => "ray",
|
Ray(_) => "ray",
|
||||||
XLine(_) => "xline",
|
XLine(_) => "xline",
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ pub fn ui_name(e: &EntityType) -> &'static str {
|
||||||
EntityType::Solid3D(_) => "3D Solid",
|
EntityType::Solid3D(_) => "3D Solid",
|
||||||
EntityType::Region(_) => "Region",
|
EntityType::Region(_) => "Region",
|
||||||
EntityType::Body(_) => "Body",
|
EntityType::Body(_) => "Body",
|
||||||
|
EntityType::Surface(_) => "Surface",
|
||||||
EntityType::Mesh(_) => "Mesh",
|
EntityType::Mesh(_) => "Mesh",
|
||||||
EntityType::Ray(_) => "Ray",
|
EntityType::Ray(_) => "Ray",
|
||||||
EntityType::XLine(_) => "XLine",
|
EntityType::XLine(_) => "XLine",
|
||||||
|
|
|
||||||
|
|
@ -282,6 +282,7 @@ pub fn fallback_wires(e: &EntityType) -> Option<&[acadrust::entities::Wire]> {
|
||||||
EntityType::Solid3D(s) => Some(&s.wires),
|
EntityType::Solid3D(s) => Some(&s.wires),
|
||||||
EntityType::Region(r) => Some(&r.wires),
|
EntityType::Region(r) => Some(&r.wires),
|
||||||
EntityType::Body(b) => Some(&b.wires),
|
EntityType::Body(b) => Some(&b.wires),
|
||||||
|
EntityType::Surface(s) => Some(&s.wires),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -293,6 +294,7 @@ pub fn tessellate_volume(e: &EntityType, color: [f32; 4], facet_res: f64) -> Opt
|
||||||
EntityType::Solid3D(s) => solid3d_tess::tessellate_solid3d(s, color, facet_res),
|
EntityType::Solid3D(s) => solid3d_tess::tessellate_solid3d(s, color, facet_res),
|
||||||
EntityType::Region(r) => solid3d_tess::tessellate_region(r, color, facet_res),
|
EntityType::Region(r) => solid3d_tess::tessellate_region(r, color, facet_res),
|
||||||
EntityType::Body(b) => solid3d_tess::tessellate_body(b, color, facet_res),
|
EntityType::Body(b) => solid3d_tess::tessellate_body(b, color, facet_res),
|
||||||
|
EntityType::Surface(s) => solid3d_tess::tessellate_surface(s, color, facet_res),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,7 @@ pub fn entity_type_name(et: &EntityType) -> &'static str {
|
||||||
EntityType::Solid3D(_) => "Solid3D",
|
EntityType::Solid3D(_) => "Solid3D",
|
||||||
EntityType::Region(_) => "Region",
|
EntityType::Region(_) => "Region",
|
||||||
EntityType::Body(_) => "Body",
|
EntityType::Body(_) => "Body",
|
||||||
|
EntityType::Surface(_) => "Surface",
|
||||||
EntityType::Table(_) => "Table",
|
EntityType::Table(_) => "Table",
|
||||||
EntityType::Tolerance(_) => "Tolerance",
|
EntityType::Tolerance(_) => "Tolerance",
|
||||||
EntityType::PolyfaceMesh(_) => "PolyfaceMesh",
|
EntityType::PolyfaceMesh(_) => "PolyfaceMesh",
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ pub(super) struct EntityIndex {
|
||||||
pub(crate) mod render;
|
pub(crate) mod render;
|
||||||
mod selection;
|
mod selection;
|
||||||
pub mod solid3d_tess;
|
pub mod solid3d_tess;
|
||||||
|
pub mod spline_tess;
|
||||||
pub mod tess_util;
|
pub mod tess_util;
|
||||||
pub mod tessellate;
|
pub mod tessellate;
|
||||||
pub mod transform;
|
pub mod transform;
|
||||||
|
|
@ -185,7 +186,7 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
|
||||||
match e {
|
match e {
|
||||||
EntityType::Hatch(_) | EntityType::Solid(_) => hatch_handles.push(h),
|
EntityType::Hatch(_) | EntityType::Solid(_) => hatch_handles.push(h),
|
||||||
EntityType::RasterImage(_) => image_handles.push(h),
|
EntityType::RasterImage(_) => image_handles.push(h),
|
||||||
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) => {
|
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) | EntityType::Surface(_) => {
|
||||||
mesh_handles.push(h)
|
mesh_handles.push(h)
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
@ -1839,7 +1840,7 @@ impl Scene {
|
||||||
// draw-order biasing so 3D occlusion is never flattened.
|
// draw-order biasing so 3D occlusion is never flattened.
|
||||||
if matches!(
|
if matches!(
|
||||||
e,
|
e,
|
||||||
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_)
|
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) | EntityType::Surface(_)
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -3448,7 +3449,7 @@ impl Scene {
|
||||||
let facet_res = self.document.header.facet_resolution;
|
let facet_res = self.document.header.facet_resolution;
|
||||||
let mesh_seed = if matches!(
|
let mesh_seed = if matches!(
|
||||||
&entity,
|
&entity,
|
||||||
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_)
|
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) | EntityType::Surface(_)
|
||||||
) {
|
) {
|
||||||
let color = self.render_style(&entity).0;
|
let color = self.render_style(&entity).0;
|
||||||
let woff = self.world_offset;
|
let woff = self.world_offset;
|
||||||
|
|
@ -4320,7 +4321,7 @@ impl Scene {
|
||||||
.document
|
.document
|
||||||
.entities()
|
.entities()
|
||||||
.filter_map(|e| match e {
|
.filter_map(|e| match e {
|
||||||
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) => {
|
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) | EntityType::Surface(_) => {
|
||||||
let color = self.render_style(e).0;
|
let color = self.render_style(e).0;
|
||||||
Some((e.common().handle, e.clone(), color))
|
Some((e.common().handle, e.clone(), color))
|
||||||
}
|
}
|
||||||
|
|
@ -7002,6 +7003,7 @@ fn tessellate_entity(
|
||||||
| EntityType::PolygonMesh(_)
|
| EntityType::PolygonMesh(_)
|
||||||
| EntityType::Body(_)
|
| EntityType::Body(_)
|
||||||
| EntityType::Region(_)
|
| EntityType::Region(_)
|
||||||
|
| EntityType::Surface(_)
|
||||||
);
|
);
|
||||||
if !is_text && w_px.max(h_px) / wpp < 5.0 {
|
if !is_text && w_px.max(h_px) / wpp < 5.0 {
|
||||||
// Sub-pixel entity: emit a stub instead of
|
// Sub-pixel entity: emit a stub instead of
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,16 @@ fn tessellate_sat(
|
||||||
tess_torus_face(&torus, lod, &mut verts, &mut normals, &mut indices);
|
tess_torus_face(&torus, lod, &mut verts, &mut normals, &mut indices);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"spline-surface" => {
|
||||||
|
crate::scene::spline_tess::tess_spline_face(
|
||||||
|
sat,
|
||||||
|
&face,
|
||||||
|
lod,
|
||||||
|
&mut verts,
|
||||||
|
&mut normals,
|
||||||
|
&mut indices,
|
||||||
|
);
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -255,6 +265,24 @@ pub fn tessellate_body(body: &Body, color: [f32; 4], facet_res: f64) -> Option<M
|
||||||
tessellate_sat_lods(&sat, name, color, facet_res)
|
tessellate_sat_lods(&sat, name, color, facet_res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tessellate a `Surface` entity (ACAD_SURFACE family) at all three LOD
|
||||||
|
/// levels. Surfaces are ACIS-backed just like bodies, so the same SAT/SAB
|
||||||
|
/// path applies — including the B-spline `spline-surface` faces that loft /
|
||||||
|
/// sweep / revolve produce.
|
||||||
|
pub fn tessellate_surface(
|
||||||
|
surface: &acadrust::entities::Surface,
|
||||||
|
color: [f32; 4],
|
||||||
|
facet_res: f64,
|
||||||
|
) -> Option<MeshLodSet> {
|
||||||
|
let sat = parse_acis(
|
||||||
|
|| surface.parse_sat(),
|
||||||
|
surface.acis_data.is_binary,
|
||||||
|
&surface.acis_data.sab_data,
|
||||||
|
)?;
|
||||||
|
let name = surface.common.handle.value().to_string();
|
||||||
|
tessellate_sat_lods(&sat, name, color, facet_res)
|
||||||
|
}
|
||||||
|
|
||||||
/// Tessellate a `Solid3D` entity at all three LOD levels.
|
/// Tessellate a `Solid3D` entity at all three LOD levels.
|
||||||
///
|
///
|
||||||
/// Returns `None` when the entity has no parseable SAT data or produces no
|
/// Returns `None` when the entity has no parseable SAT data or produces no
|
||||||
|
|
|
||||||
190
src/scene/spline_tess.rs
Normal file
190
src/scene/spline_tess.rs
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
// B-spline (NURBS) surface tessellation for ACIS `spline-surface` faces.
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
// `BSplineSurface` (the same NURBS kernel the Model tab already builds on),
|
||||||
|
// and sample its parametric grid into triangles.
|
||||||
|
|
||||||
|
use acadrust::entities::acis::{SatDocument, SatFace, SatRecord, SatToken};
|
||||||
|
use truck_modeling::{BSplineSurface, KnotVec, ParametricSurface, ParametricSurface3D, Point3};
|
||||||
|
|
||||||
|
use crate::scene::solid3d_tess::LodConfig;
|
||||||
|
|
||||||
|
/// Tessellate one `spline-surface` face by sampling its B-spline surface.
|
||||||
|
/// Appends triangles to the shared mesh buffers; a no-op when the surface
|
||||||
|
/// record can't be parsed into a B-spline.
|
||||||
|
pub fn tess_spline_face(
|
||||||
|
sat: &SatDocument,
|
||||||
|
face: &SatFace,
|
||||||
|
lod: LodConfig,
|
||||||
|
verts: &mut Vec<[f32; 3]>,
|
||||||
|
normals: &mut Vec<[f32; 3]>,
|
||||||
|
indices: &mut Vec<u32>,
|
||||||
|
) {
|
||||||
|
let Some(surf_rec) = sat.resolve(face.surface()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(surface) = build_bspline_surface(surf_rec) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sample over the knot domain. The B-spline patches stored by loft/sweep
|
||||||
|
// are already trimmed to the face, so the full parametric rectangle is the
|
||||||
|
// visible surface — no separate boundary trim needed.
|
||||||
|
let (u_range, v_range) = surface.parameter_range();
|
||||||
|
let (u0, u1) = range_bounds(u_range);
|
||||||
|
let (v0, v1) = range_bounds(v_range);
|
||||||
|
if !(u1 > u0) || !(v1 > v0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let su = lod.grid_u.max(8);
|
||||||
|
let sv = lod.grid_v.max(8);
|
||||||
|
|
||||||
|
let base = verts.len() as u32;
|
||||||
|
for j in 0..=sv {
|
||||||
|
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 n = surface.normal(u, v);
|
||||||
|
verts.push([p.x as f32, p.y as f32, p.z as f32]);
|
||||||
|
normals.push([n.x as f32, n.y as f32, n.z as f32]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let row = (su + 1) as u32;
|
||||||
|
for j in 0..sv as u32 {
|
||||||
|
for i in 0..su as u32 {
|
||||||
|
let a = base + j * row + i;
|
||||||
|
let b = a + 1;
|
||||||
|
let c = a + row;
|
||||||
|
let d = c + 1;
|
||||||
|
indices.extend_from_slice(&[a, b, d, a, d, c]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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_bspline_surface(rec: &SatRecord) -> Option<BSplineSurface<Point3>> {
|
||||||
|
let toks = &rec.tokens;
|
||||||
|
// Locate the real B-spline block. `nullbs` placeholders (for absent
|
||||||
|
// rail/path surfaces) precede it; the actual surface is `nubs`.
|
||||||
|
let start = toks.iter().position(|t| {
|
||||||
|
matches!(t, SatToken::Ident(s) if s == "nubs" || s == "nurbs")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut p = start + 1;
|
||||||
|
let deg_u = read_int(toks, &mut p)? as usize;
|
||||||
|
let deg_v = read_int(toks, &mut p)? as usize;
|
||||||
|
// Four form flags (closure / singularity in u and v) — skip.
|
||||||
|
for _ in 0..4 {
|
||||||
|
read_int(toks, &mut p)?;
|
||||||
|
}
|
||||||
|
let n_uknot = read_int(toks, &mut p)? as usize;
|
||||||
|
let n_vknot = read_int(toks, &mut p)? as usize;
|
||||||
|
|
||||||
|
let u_knots = read_knot_vec(toks, &mut p, n_uknot, deg_u)?;
|
||||||
|
let v_knots = read_knot_vec(toks, &mut p, n_vknot, deg_v)?;
|
||||||
|
|
||||||
|
let n_ctrl_u = u_knots.len().checked_sub(deg_u + 1)?;
|
||||||
|
let n_ctrl_v = v_knots.len().checked_sub(deg_v + 1)?;
|
||||||
|
if n_ctrl_u == 0 || n_ctrl_v == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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]`.
|
||||||
|
let total = n_ctrl_u * n_ctrl_v;
|
||||||
|
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 uk = KnotVec::from(u_knots);
|
||||||
|
let vk = KnotVec::from(v_knots);
|
||||||
|
BSplineSurface::try_new((uk, vk), ctrl).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read `count` `(knot value, multiplicity)` pairs into an expanded knot
|
||||||
|
/// vector. ACIS stores the end knots with multiplicity = degree; a clamped
|
||||||
|
/// B-spline needs degree + 1, so the first and last multiplicities are bumped
|
||||||
|
/// by one.
|
||||||
|
fn read_knot_vec(
|
||||||
|
toks: &[SatToken],
|
||||||
|
p: &mut usize,
|
||||||
|
count: usize,
|
||||||
|
degree: usize,
|
||||||
|
) -> Option<Vec<f64>> {
|
||||||
|
let mut knots: Vec<f64> = Vec::new();
|
||||||
|
for i in 0..count {
|
||||||
|
let value = read_float(toks, p)?;
|
||||||
|
let mut mult = read_int(toks, p)? as usize;
|
||||||
|
if i == 0 || i == count - 1 {
|
||||||
|
mult += 1;
|
||||||
|
}
|
||||||
|
let _ = degree;
|
||||||
|
for _ in 0..mult {
|
||||||
|
knots.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if knots.len() < 2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(knots)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_int(toks: &[SatToken], p: &mut usize) -> Option<i64> {
|
||||||
|
while *p < toks.len() {
|
||||||
|
let t = &toks[*p];
|
||||||
|
*p += 1;
|
||||||
|
match t {
|
||||||
|
SatToken::Integer(v) => return Some(*v),
|
||||||
|
SatToken::Float(v) => return Some(*v as i64),
|
||||||
|
// Skip block delimiters / idents that may appear inline.
|
||||||
|
SatToken::Ident(_) | SatToken::Enum(_) => continue,
|
||||||
|
_ => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_float(toks: &[SatToken], p: &mut usize) -> Option<f64> {
|
||||||
|
while *p < toks.len() {
|
||||||
|
let t = &toks[*p];
|
||||||
|
*p += 1;
|
||||||
|
match t {
|
||||||
|
SatToken::Float(v) => return Some(*v),
|
||||||
|
SatToken::Integer(v) => return Some(*v as f64),
|
||||||
|
SatToken::Ident(_) | SatToken::Enum(_) => continue,
|
||||||
|
_ => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
@ -617,7 +617,10 @@ fn fallback_geometry(entity: &EntityType, world_offset: [f64; 3]) -> Geometry {
|
||||||
// plus an insertion snap — never the placeholder segment below, which
|
// plus an insertion snap — never the placeholder segment below, which
|
||||||
// would otherwise draw a stray 1-unit line at the origin next to the
|
// would otherwise draw a stray 1-unit line at the origin next to the
|
||||||
// solid.
|
// solid.
|
||||||
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) => {
|
EntityType::Solid3D(_)
|
||||||
|
| EntityType::Region(_)
|
||||||
|
| EntityType::Body(_)
|
||||||
|
| EntityType::Surface(_) => {
|
||||||
let pts = solid_wire_fallback(entity, world_offset);
|
let pts = solid_wire_fallback(entity, world_offset);
|
||||||
let mut snap = vec![];
|
let mut snap = vec![];
|
||||||
if let Some(p) = crate::entities::solid3d::point_of_reference(entity) {
|
if let Some(p) = crate::entities::solid3d::point_of_reference(entity) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue