refactor(render): unify scene rendering paths
Route model, paper, viewport and nested block content through one traversal while preserving leaf renderers. Carry decoded material state into mesh shading and harden solid geometry conversion.
This commit is contained in:
parent
afc94dce13
commit
b667eeeb05
17 changed files with 2218 additions and 1871 deletions
|
|
@ -4,8 +4,8 @@
|
|||
// • plane-surface faces → collect coedge-loop polygon, fan-triangulate.
|
||||
// • cone-surface faces → sample a parametric grid (handles both cylinders
|
||||
// and true cones).
|
||||
// • sphere-surface faces → sample a full UV grid.
|
||||
// • torus-surface faces → sample a full UV grid.
|
||||
// • sphere-surface faces → sample a boundary-clipped UV grid.
|
||||
// • torus-surface faces → sample a boundary-clipped UV grid.
|
||||
//
|
||||
// Coverage is tracked explicitly. Unsupported or malformed faces retain
|
||||
// feature/display wires, and partial shells are marked non-complete so solid
|
||||
|
|
@ -552,16 +552,21 @@ fn torus_isolines(sat: &SatDocument, face: &SatFace, count: usize, out: &mut Vec
|
|||
let v = cross3(axis, u);
|
||||
let major = torus.major_radius();
|
||||
let minor = torus.minor_radius();
|
||||
let (phi_min, phi_span, full) = torus_phi_range(sat, face, [cx, cy, cz], u, v);
|
||||
let (phi_min, phi_span, full) =
|
||||
torus_phi_range(sat, face, [cx, cy, cz], u, v, major);
|
||||
let phi_total = if full { TAU } else { phi_span };
|
||||
let (theta_min, theta_span, theta_full) =
|
||||
torus_theta_range(sat, face, [cx, cy, cz], axis, major, minor);
|
||||
let theta_total = if theta_full { TAU } else { theta_span };
|
||||
|
||||
// Minor (cross-section) circles — constant revolution angle, full tube.
|
||||
// Minor (cross-section) arcs — constant revolution angle, clipped to the
|
||||
// face's tube-angle window.
|
||||
// Segment count from the tube (minor) radius at the shared edge tolerance.
|
||||
let m = edge_arc_segs(minor, TAU);
|
||||
let m = edge_arc_segs(minor, theta_total);
|
||||
for phi in iso_params(phi_min, phi_span, full, count) {
|
||||
for t in 0..m {
|
||||
let t0 = TAU * (t as f64 / m as f64);
|
||||
let t1 = TAU * ((t + 1) as f64 / m as f64);
|
||||
let t0 = theta_min + theta_total * (t as f64 / m as f64);
|
||||
let t1 = theta_min + theta_total * ((t + 1) as f64 / m as f64);
|
||||
out.push(torus_pt(cx, cy, cz, axis, u, v, major, minor, t0, phi));
|
||||
out.push(torus_pt(cx, cy, cz, axis, u, v, major, minor, t1, phi));
|
||||
}
|
||||
|
|
@ -573,8 +578,7 @@ fn torus_isolines(sat: &SatDocument, face: &SatFace, count: usize, out: &mut Vec
|
|||
// cross-sections. `count.max(2)` guarantees outer + inner even at ISOLINES=1.
|
||||
let ring_segs = edge_arc_segs(major, phi_total).max(2);
|
||||
let n_ring = count.max(2);
|
||||
for k in 0..n_ring {
|
||||
let theta = TAU * (k as f64 / n_ring as f64);
|
||||
for theta in iso_params(theta_min, theta_span, theta_full, n_ring) {
|
||||
for s in 0..ring_segs {
|
||||
let p0 = phi_min + phi_total * (s as f64 / ring_segs as f64);
|
||||
let p1 = phi_min + phi_total * ((s + 1) as f64 / ring_segs as f64);
|
||||
|
|
@ -622,54 +626,24 @@ fn sphere_param_range(
|
|||
|
||||
/// Revolution-angle arc a torus face spans, walking all its boundary loops.
|
||||
///
|
||||
/// A partial tube ends in two minor-circle caps sitting in constant-φ planes;
|
||||
/// the arc between them is the opening. But the tube body can also carry
|
||||
/// interior hole loops where another solid punches through it, so the widest
|
||||
/// empty gap is *not* reliably the opening — a hole splits the body into wide
|
||||
/// hole-free stretches that masquerade as it. Only a gap flanked by two end
|
||||
/// caps is genuinely surface-free, so that is the opening; the body is the rest
|
||||
/// of the turn. Returns `(body_start, body_span, full)`.
|
||||
/// A partial tube ends in two minor-circle caps sitting in constant-φ planes.
|
||||
/// Their adjacent planar faces carry outward normals that identify which cap is
|
||||
/// the start and which is the end, so both short elbows and long open tubes use
|
||||
/// the correct side of the analytic surface. If that topology is absent or
|
||||
/// ambiguous, the face remains untrimmed instead of guessing between the two
|
||||
/// possible arcs. Returns `(body_start, body_span, full)`.
|
||||
pub(crate) fn torus_phi_range(
|
||||
sat: &SatDocument,
|
||||
face: &SatFace,
|
||||
center: [f64; 3],
|
||||
u: [f64; 3],
|
||||
v: [f64; 3],
|
||||
major: f64,
|
||||
) -> (f64, f64, bool) {
|
||||
// Below this revolution-angle spread a loop is a minor circle sitting in a
|
||||
// constant-φ plane — a tube end cap. Interference loops (where another solid
|
||||
// punches through the tube) wander several degrees in φ, well above it.
|
||||
const CAP_SPREAD: f64 = 0.15; // rad (~8.6°)
|
||||
// Ignore sub-degree gaps *within* a cap's own point cluster; a genuine tube
|
||||
// opening is far wider.
|
||||
const MIN_OPENING: f64 = 0.02; // rad (~1.1°)
|
||||
|
||||
let phi_of = |p: [f64; 3]| -> f64 {
|
||||
let rel = [p[0] - center[0], p[1] - center[1], p[2] - center[2]];
|
||||
dot3(rel, v).atan2(dot3(rel, u)).rem_euclid(TAU)
|
||||
};
|
||||
// Angular spread of a set of φ values, wrap-aware: the turn minus the widest
|
||||
// gap between them. A cap collapses to ~0; a hole loop keeps its real width.
|
||||
let spread = |phis: &[f64]| -> f64 {
|
||||
if phis.len() < 2 {
|
||||
return 0.0;
|
||||
}
|
||||
let mut s = phis.to_vec();
|
||||
s.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
let mut gmax = 0.0f64;
|
||||
for i in 0..s.len() {
|
||||
let next = if i + 1 < s.len() {
|
||||
s[i + 1]
|
||||
} else {
|
||||
s[0] + TAU
|
||||
};
|
||||
gmax = gmax.max(next - s[i]);
|
||||
}
|
||||
TAU - gmax
|
||||
};
|
||||
|
||||
// Every boundary point, tagged by whether its loop is a tube end cap.
|
||||
let mut tagged: Vec<(f64, bool)> = Vec::new();
|
||||
const GEOM_EPS: f64 = 1e-6;
|
||||
let axis = norm3(cross3(u, v));
|
||||
let mut starts = Vec::new();
|
||||
let mut ends = Vec::new();
|
||||
let mut lp = face.first_loop();
|
||||
let mut seen: HashSet<i32> = HashSet::default();
|
||||
while !lp.is_null() && seen.insert(lp.0) {
|
||||
|
|
@ -677,46 +651,215 @@ pub(crate) fn torus_phi_range(
|
|||
let Some(sl) = SatLoop::from_record(lr) else {
|
||||
break;
|
||||
};
|
||||
let poly = collect_loop_polygon(sat, &sl, BOUNDARY_CHORD_FRAC);
|
||||
lp = sl.next_loop();
|
||||
if poly.is_empty() {
|
||||
let Some((cap_center, cap_normal)) = adjacent_planar_circle_cap(sat, &sl) else {
|
||||
continue;
|
||||
};
|
||||
let rel = [
|
||||
cap_center[0] - center[0],
|
||||
cap_center[1] - center[1],
|
||||
cap_center[2] - center[2],
|
||||
];
|
||||
let axial = dot3(rel, axis);
|
||||
let radial_u = dot3(rel, u);
|
||||
let radial_v = dot3(rel, v);
|
||||
let radial_len = (radial_u * radial_u + radial_v * radial_v).sqrt();
|
||||
let scale = major.abs().max(radial_len).max(1.0);
|
||||
if axial.abs() > scale * GEOM_EPS
|
||||
|| (radial_len - major.abs()).abs() > scale * GEOM_EPS
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let phis: Vec<f64> = poly.iter().map(|&p| phi_of(p)).collect();
|
||||
let is_cap = spread(&phis) < CAP_SPREAD;
|
||||
for phi in phis {
|
||||
tagged.push((phi, is_cap));
|
||||
}
|
||||
}
|
||||
if tagged.len() < 2 {
|
||||
return (0.0, TAU, true);
|
||||
}
|
||||
tagged.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
|
||||
|
||||
// The opening is the arc with no surface: a gap flanked on *both* sides by
|
||||
// end-cap points (a hole never bounds the opening). Of those, the tube's own
|
||||
// opening is the narrowest above the intra-cluster noise floor; the body is
|
||||
// the rest of the turn. No cap-cap gap ⇒ the tube closes into a full ring.
|
||||
let n = tagged.len();
|
||||
let mut opening_gap = f64::MAX;
|
||||
let mut body_start = 0.0;
|
||||
for i in 0..n {
|
||||
let (phi_i, cap_i) = tagged[i];
|
||||
let (phi_j, cap_j) = if i + 1 < n {
|
||||
tagged[i + 1]
|
||||
let physical_phi = radial_v.atan2(radial_u);
|
||||
let phi = if major < 0.0 {
|
||||
(physical_phi - std::f64::consts::PI).rem_euclid(TAU)
|
||||
} else {
|
||||
(tagged[0].0 + TAU, tagged[0].1)
|
||||
physical_phi.rem_euclid(TAU)
|
||||
};
|
||||
let gap = phi_j - phi_i;
|
||||
if cap_i && cap_j && gap > MIN_OPENING && gap < opening_gap {
|
||||
opening_gap = gap;
|
||||
body_start = phi_j.rem_euclid(TAU); // body resumes past the far cap
|
||||
let major_sign = if major < 0.0 { -1.0 } else { 1.0 };
|
||||
let tangent = norm3([
|
||||
major_sign * (-u[0] * phi.sin() + v[0] * phi.cos()),
|
||||
major_sign * (-u[1] * phi.sin() + v[1] * phi.cos()),
|
||||
major_sign * (-u[2] * phi.sin() + v[2] * phi.cos()),
|
||||
]);
|
||||
let alignment = dot3(tangent, cap_normal);
|
||||
if (alignment.abs() - 1.0).abs() > GEOM_EPS {
|
||||
continue;
|
||||
}
|
||||
if alignment < 0.0 {
|
||||
starts.push(phi);
|
||||
} else {
|
||||
ends.push(phi);
|
||||
}
|
||||
}
|
||||
if opening_gap == f64::MAX {
|
||||
|
||||
starts.sort_by(|left, right| left.partial_cmp(right).unwrap());
|
||||
ends.sort_by(|left, right| left.partial_cmp(right).unwrap());
|
||||
starts.dedup_by(|left, right| (*left - *right).abs() < GEOM_EPS);
|
||||
ends.dedup_by(|left, right| (*left - *right).abs() < GEOM_EPS);
|
||||
if let ([start], [end]) = (starts.as_slice(), ends.as_slice()) {
|
||||
let span = (end - start).rem_euclid(TAU);
|
||||
if span > GEOM_EPS && span < TAU - GEOM_EPS {
|
||||
return (*start, span, false);
|
||||
}
|
||||
}
|
||||
(0.0, TAU, true)
|
||||
}
|
||||
|
||||
/// Analytic circle data carried by an ellipse edge in a boundary loop.
|
||||
fn loop_circle_geometry(
|
||||
sat: &SatDocument,
|
||||
sat_loop: &SatLoop,
|
||||
) -> Option<([f64; 3], [f64; 3], f64)> {
|
||||
let first = sat_loop.first_coedge();
|
||||
let mut current = first;
|
||||
let mut seen: HashSet<i32> = HashSet::default();
|
||||
while !current.is_null() && seen.insert(current.0) {
|
||||
let coedge = SatCoedge::from_record(sat.resolve(current)?)?;
|
||||
if let Some(ellipse) = sat
|
||||
.resolve(coedge.edge())
|
||||
.and_then(SatEdge::from_record)
|
||||
.and_then(|edge| sat.resolve(edge.curve()))
|
||||
.and_then(SatEllipseCurve::from_record)
|
||||
{
|
||||
if (ellipse.ratio() - 1.0).abs() <= 1e-6 {
|
||||
let (cx, cy, cz) = ellipse.center();
|
||||
let (nx, ny, nz) = ellipse.normal();
|
||||
let (mx, my, mz) = ellipse.major_axis();
|
||||
let radius = (mx * mx + my * my + mz * mz).sqrt();
|
||||
return Some(([cx, cy, cz], norm3([nx, ny, nz]), radius));
|
||||
}
|
||||
}
|
||||
current = coedge.next();
|
||||
if current == first {
|
||||
break;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Circle centre and outward normal of the planar cap sharing a boundary loop
|
||||
/// with a curved face. Both values come from analytic SAT records.
|
||||
fn adjacent_planar_circle_cap(
|
||||
sat: &SatDocument,
|
||||
sat_loop: &SatLoop,
|
||||
) -> Option<([f64; 3], [f64; 3])> {
|
||||
let (circle_center, _, _) = loop_circle_geometry(sat, sat_loop)?;
|
||||
let first = sat_loop.first_coedge();
|
||||
let mut current = first;
|
||||
let mut seen: HashSet<i32> = HashSet::default();
|
||||
while !current.is_null() && seen.insert(current.0) {
|
||||
let coedge = SatCoedge::from_record(sat.resolve(current)?)?;
|
||||
if let Some(partner) = sat.resolve(coedge.partner()).and_then(SatCoedge::from_record) {
|
||||
if let Some(partner_loop) = sat
|
||||
.resolve(partner.owner_loop())
|
||||
.and_then(SatLoop::from_record)
|
||||
{
|
||||
if let Some(adjacent_face) = sat
|
||||
.resolve(partner_loop.face())
|
||||
.and_then(SatFace::from_record)
|
||||
{
|
||||
if let Some(plane) = sat
|
||||
.resolve(adjacent_face.surface())
|
||||
.and_then(SatPlaneSurface::from_record)
|
||||
{
|
||||
let (nx, ny, nz) = plane.normal();
|
||||
let normal = if matches!(adjacent_face.sense(), Sense::Reversed) {
|
||||
[-nx, -ny, -nz]
|
||||
} else {
|
||||
[nx, ny, nz]
|
||||
};
|
||||
return Some((circle_center, norm3(normal)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
current = coedge.next();
|
||||
if current == first {
|
||||
break;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Tube-angle span of a torus face from its boundary loops.
|
||||
///
|
||||
/// Ring tori normally cover the complete minor circle, while a spindle-torus
|
||||
/// cap terminates at the surface's axial singularity and owns just one
|
||||
/// constant-θ boundary ring. Sampling the complete minor circle in that case
|
||||
/// duplicates the cap across the rest of the self-intersecting analytic
|
||||
/// surface, producing a ball much larger than the bounded face.
|
||||
fn torus_theta_range(
|
||||
sat: &SatDocument,
|
||||
face: &SatFace,
|
||||
center: [f64; 3],
|
||||
axis: [f64; 3],
|
||||
major: f64,
|
||||
minor: f64,
|
||||
) -> (f64, f64, bool) {
|
||||
const GEOM_EPS: f64 = 1e-6;
|
||||
|
||||
if minor <= 1e-9 || major.abs() >= minor {
|
||||
return (0.0, TAU, true);
|
||||
}
|
||||
(body_start, TAU - opening_gap, false)
|
||||
|
||||
let mut rims = Vec::new();
|
||||
let mut lp = face.first_loop();
|
||||
let mut seen: HashSet<i32> = HashSet::default();
|
||||
while !lp.is_null() && seen.insert(lp.0) {
|
||||
let Some(sat_loop) = sat.resolve(lp).and_then(SatLoop::from_record) else {
|
||||
break;
|
||||
};
|
||||
lp = sat_loop.next_loop();
|
||||
let Some((circle_center, circle_normal, circle_radius)) =
|
||||
loop_circle_geometry(sat, &sat_loop)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let rel = [
|
||||
circle_center[0] - center[0],
|
||||
circle_center[1] - center[1],
|
||||
circle_center[2] - center[2],
|
||||
];
|
||||
let axial = dot3(rel, axis);
|
||||
let planar = [
|
||||
rel[0] - axial * axis[0],
|
||||
rel[1] - axial * axis[1],
|
||||
rel[2] - axial * axis[2],
|
||||
];
|
||||
let scale = major.abs().max(minor).max(circle_radius).max(1.0);
|
||||
if dot3(planar, planar).sqrt() > scale * GEOM_EPS
|
||||
|| (dot3(circle_normal, axis).abs() - 1.0).abs() > GEOM_EPS
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// The principal spindle branch has non-negative revolution radius:
|
||||
// circle_radius = major + minor·cos(θ). Together with the circle
|
||||
// plane's axial coordinate this recovers θ directly.
|
||||
let sin_theta = axial / minor;
|
||||
let cos_theta = (circle_radius - major) / minor;
|
||||
if (sin_theta * sin_theta + cos_theta * cos_theta - 1.0).abs() > GEOM_EPS {
|
||||
continue;
|
||||
}
|
||||
rims.push(sin_theta.atan2(cos_theta));
|
||||
}
|
||||
|
||||
rims.sort_by(|left, right| left.partial_cmp(right).unwrap());
|
||||
rims.dedup_by(|left, right| (*left - *right).abs() < GEOM_EPS);
|
||||
if rims.len() != 1 || rims[0].abs() < GEOM_EPS {
|
||||
return (0.0, TAU, true);
|
||||
}
|
||||
|
||||
let rim = rims[0];
|
||||
let singular = (-major / minor).clamp(-1.0, 1.0).acos();
|
||||
if rim > 0.0 && rim < singular {
|
||||
(rim, singular - rim, false)
|
||||
} else if rim < 0.0 && rim > -singular {
|
||||
(-singular, singular + rim, false)
|
||||
} else {
|
||||
(0.0, TAU, true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reduce a set of angles to a `(min, span, full)` arc. Mirrors `angular_range`'s
|
||||
|
|
@ -868,7 +1011,22 @@ fn collect_curved_gens(
|
|||
torus.u_direction().2,
|
||||
]);
|
||||
let v = cross3(axis, u);
|
||||
let (pmin, pspan, full) = torus_phi_range(sat, &face, [cx, cy, cz], u, v);
|
||||
let (pmin, pspan, full) = torus_phi_range(
|
||||
sat,
|
||||
&face,
|
||||
[cx, cy, cz],
|
||||
u,
|
||||
v,
|
||||
torus.major_radius(),
|
||||
);
|
||||
let (tmin, tspan, tfull) = torus_theta_range(
|
||||
sat,
|
||||
&face,
|
||||
[cx, cy, cz],
|
||||
axis,
|
||||
torus.major_radius(),
|
||||
torus.minor_radius(),
|
||||
);
|
||||
let (center, center_low) = place([cx, cy, cz]);
|
||||
out.push(CurvedGen::Torus {
|
||||
center,
|
||||
|
|
@ -881,6 +1039,9 @@ fn collect_curved_gens(
|
|||
phi_min: pmin as f32,
|
||||
phi_span: pspan as f32,
|
||||
full,
|
||||
theta_min: tmin as f32,
|
||||
theta_span: tspan as f32,
|
||||
theta_full: tfull,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
|
|
@ -2093,14 +2254,19 @@ pub(crate) fn tess_torus_face(
|
|||
let v_dir = cross3(axis, u_dir);
|
||||
let major_r = torus.major_radius();
|
||||
let minor_r = torus.minor_radius();
|
||||
|
||||
// Tube cross-section is a closed minor circle → full-circle segment count at
|
||||
// the tube (minor) radius.
|
||||
let nu = lod.circle_segs(minor_r).max(3); // around the tube
|
||||
let (theta_start, theta_arc, theta_full) =
|
||||
torus_theta_range(sat, face, [cx, cy, cz], axis, major_r, minor_r);
|
||||
let theta_total = if theta_full { TAU } else { theta_arc };
|
||||
let nu = if theta_full {
|
||||
lod.circle_segs(minor_r).max(3)
|
||||
} else {
|
||||
lod.arc_segs(minor_r, theta_total).max(1)
|
||||
};
|
||||
|
||||
// Mesh only the revolution arc the face covers. A partial tube (an open "C")
|
||||
// otherwise builds as a full closed ring where the solid is open.
|
||||
let (phi_start, phi_arc, full) = torus_phi_range(sat, face, [cx, cy, cz], u_dir, v_dir);
|
||||
let (phi_start, phi_arc, full) =
|
||||
torus_phi_range(sat, face, [cx, cy, cz], u_dir, v_dir, major_r);
|
||||
let phi_total = if full { TAU } else { phi_arc };
|
||||
// Along-length divisions from the ring (major) radius and the covered arc at
|
||||
// the LOD's chord tolerance — a short arc samples proportionally less.
|
||||
|
|
@ -2111,8 +2277,8 @@ pub(crate) fn tess_torus_face(
|
|||
let phi1 = phi_start + phi_total * ((j + 1) as f64 / nv as f64);
|
||||
|
||||
for i in 0..nu {
|
||||
let theta0 = TAU * (i as f64 / nu as f64);
|
||||
let theta1 = TAU * ((i + 1) as f64 / nu as f64);
|
||||
let theta0 = theta_start + theta_total * (i as f64 / nu as f64);
|
||||
let theta1 = theta_start + theta_total * ((i + 1) as f64 / nu as f64);
|
||||
|
||||
let p = [
|
||||
torus_pt(
|
||||
|
|
@ -2162,14 +2328,20 @@ fn torus_pt(
|
|||
theta: f64, // tube angle
|
||||
phi: f64, // revolution angle
|
||||
) -> [f64; 3] {
|
||||
// Ring center at angle phi.
|
||||
let ring = [
|
||||
cx + major_r * (u_dir[0] * phi.cos() + v_dir[0] * phi.sin()),
|
||||
cy + major_r * (u_dir[1] * phi.cos() + v_dir[1] * phi.sin()),
|
||||
cz + major_r * (u_dir[2] * phi.cos() + v_dir[2] * phi.sin()),
|
||||
// Parametric radial direction at angle phi. Keep this independent from the
|
||||
// signed major radius: a zero-major spindle is still well-defined, while
|
||||
// normalizing `ring - center` would collapse it and would flip the tube
|
||||
// frame whenever the stored major radius is negative.
|
||||
let radial = [
|
||||
u_dir[0] * phi.cos() + v_dir[0] * phi.sin(),
|
||||
u_dir[1] * phi.cos() + v_dir[1] * phi.sin(),
|
||||
u_dir[2] * phi.cos() + v_dir[2] * phi.sin(),
|
||||
];
|
||||
let ring = [
|
||||
cx + major_r * radial[0],
|
||||
cy + major_r * radial[1],
|
||||
cz + major_r * radial[2],
|
||||
];
|
||||
// Radial direction from torus axis to ring center.
|
||||
let radial = norm3([ring[0] - cx, ring[1] - cy, ring[2] - cz]);
|
||||
// Point on tube.
|
||||
[
|
||||
ring[0] + minor_r * (radial[0] * theta.cos() + axis[0] * theta.sin()),
|
||||
|
|
|
|||
|
|
@ -800,58 +800,53 @@ pub(crate) fn tessellate_entity(
|
|||
// re-apply the table's scale factor on top of already-baked geometry.
|
||||
// When the block exists we render it directly. Same pattern as
|
||||
// Dimension's `block_name`.
|
||||
if let EntityType::Table(tab) = e {
|
||||
if let Some(br_h) = tab.block_record_handle {
|
||||
if let Some(br) = document.block_records.iter().find(|br| br.handle == br_h) {
|
||||
if !br.entity_handles.is_empty() {
|
||||
let mut wires: Vec<WireModel> = Vec::with_capacity(br.entity_handles.len());
|
||||
// The Table's own layer style — layer-0 inheritance target
|
||||
// for baked sub-entities on layer "0" (#221).
|
||||
let tab_l0_color = view::render::adapt_to_bg(
|
||||
view::render::layer_render_style(document, &e.common().layer).color,
|
||||
bg_color,
|
||||
if let EntityType::Table(table) = e {
|
||||
if let Some(record) = table.block_record_handle.and_then(|handle| {
|
||||
document
|
||||
.block_records
|
||||
.iter()
|
||||
.find(|record| record.handle == handle)
|
||||
}) {
|
||||
if !record.entity_handles.is_empty() {
|
||||
let mut insert =
|
||||
acadrust::entities::Insert::new(
|
||||
record.name.clone(),
|
||||
table.insertion_point,
|
||||
);
|
||||
let tab_l0_aci = document
|
||||
.layers
|
||||
.get(&e.common().layer)
|
||||
.map(|l| match &l.color {
|
||||
acadrust::types::Color::Index(i) => *i,
|
||||
_ => 0,
|
||||
})
|
||||
.unwrap_or(0);
|
||||
// The *T block is laid out in block-local space (origin at
|
||||
// the table's top-left corner); the Table entity carries the
|
||||
// world placement in `insertion_point` + `horizontal_direction`
|
||||
// (like an INSERT). Place each baked sub-entity there before
|
||||
// tessellating — without it the whole table renders at the
|
||||
// origin. Rotating about the local origin then translating
|
||||
// gives world = insertion + R·local; tessellate_entity then
|
||||
// handles the UTM-scale relative-to-eye split as usual.
|
||||
let ins = tab.insertion_point;
|
||||
let angle = tab.horizontal_direction.y.atan2(tab.horizontal_direction.x);
|
||||
for &eh in &br.entity_handles {
|
||||
let Some(sub) = document.get_entity(eh) else {
|
||||
continue;
|
||||
};
|
||||
insert.rotation = table
|
||||
.horizontal_direction
|
||||
.y
|
||||
.atan2(table.horizontal_direction.x);
|
||||
insert.common = table.common.clone();
|
||||
|
||||
let depths = rustc_hash::FxHashMap::default();
|
||||
let graph = crate::scene::render_graph::RenderSceneGraph::new(
|
||||
document,
|
||||
None,
|
||||
annotation_scale_handle,
|
||||
true,
|
||||
&depths,
|
||||
);
|
||||
let mut wires = Vec::new();
|
||||
graph.walk_insert(
|
||||
&insert,
|
||||
h,
|
||||
|_, _| true,
|
||||
|sub, context| {
|
||||
let has_book_color =
|
||||
view::render::has_resolved_book_color(document, sub);
|
||||
let sub_color_is_byblock = !has_book_color
|
||||
&& sub.common().color == acadrust::types::Color::ByBlock;
|
||||
let sub_is_l0_bylayer =
|
||||
!has_book_color
|
||||
&& view::render::is_effective_layer_zero(&sub.common().layer)
|
||||
&& sub.common().color == acadrust::types::Color::ByLayer;
|
||||
let color_byblock = !has_book_color
|
||||
&& sub.common().color
|
||||
== acadrust::types::Color::ByBlock;
|
||||
let color_layer0 = !has_book_color
|
||||
&& view::render::is_effective_layer_zero(
|
||||
&sub.common().layer,
|
||||
)
|
||||
&& sub.common().color
|
||||
== acadrust::types::Color::ByLayer;
|
||||
let style = context.style_for(document, sub);
|
||||
let mut placed = sub.clone();
|
||||
{
|
||||
let ent = placed.as_entity_mut();
|
||||
if angle.abs() > 1e-9 {
|
||||
ent.apply_rotation(
|
||||
acadrust::types::Vector3::new(0.0, 0.0, 1.0),
|
||||
angle,
|
||||
);
|
||||
}
|
||||
ent.translate(ins);
|
||||
}
|
||||
placed.apply_transform(&context.transform);
|
||||
let sub_wires = tessellate_entity(
|
||||
document,
|
||||
selected,
|
||||
|
|
@ -865,43 +860,39 @@ pub(crate) fn tessellate_entity(
|
|||
world_per_pixel,
|
||||
paper_space,
|
||||
);
|
||||
for mut w in sub_wires {
|
||||
w.name = h.value().to_string();
|
||||
if sub_color_is_byblock {
|
||||
w.color = if sel {
|
||||
for mut wire in sub_wires {
|
||||
wire.name = h.value().to_string();
|
||||
if color_byblock {
|
||||
wire.color = if sel {
|
||||
WireModel::SELECTED
|
||||
} else {
|
||||
entity_color
|
||||
view::render::adapt_to_bg(style.0, bg_color)
|
||||
};
|
||||
w.aci = aci;
|
||||
} else if sub_is_l0_bylayer && !sel {
|
||||
w.color = tab_l0_color;
|
||||
w.aci = tab_l0_aci;
|
||||
wire.aci = style.4;
|
||||
} else if color_layer0 && !sel {
|
||||
wire.color =
|
||||
view::render::adapt_to_bg(style.0, bg_color);
|
||||
wire.aci = style.4;
|
||||
}
|
||||
wires.push(w);
|
||||
wires.push(wire);
|
||||
}
|
||||
},
|
||||
);
|
||||
if !wires.is_empty() {
|
||||
let aabb = entity_aabb(e);
|
||||
for wire in &mut wires {
|
||||
if !wire.points.is_empty() || !wire.fill_tris.is_empty() {
|
||||
set_wire_aabb(wire, aabb);
|
||||
}
|
||||
}
|
||||
if !wires.is_empty() {
|
||||
let aabb = entity_aabb(e);
|
||||
for w in &mut wires {
|
||||
// Empty SDF-text cells keep their tight glyph-box
|
||||
// AABB; only stroke/fill wires take the whole-block
|
||||
// box as a broad-phase pick hint.
|
||||
if !w.points.is_empty() || !w.fill_tris.is_empty() {
|
||||
set_wire_aabb(w, aabb);
|
||||
}
|
||||
}
|
||||
return wires;
|
||||
}
|
||||
return wires;
|
||||
}
|
||||
}
|
||||
}
|
||||
// No baked block (e.g. a table created in-app) — synthesise coloured
|
||||
// geometry from the rows + TableStyle so fills/colours/borders/margins
|
||||
// are honoured instead of the monochrome fallback.
|
||||
|
||||
let table_anno = 1.0;
|
||||
let mut wires = crate::entities::table::tessellate_table(
|
||||
tab,
|
||||
table,
|
||||
document,
|
||||
sel,
|
||||
entity_color,
|
||||
|
|
@ -909,7 +900,7 @@ pub(crate) fn tessellate_entity(
|
|||
table_anno,
|
||||
);
|
||||
for insert in
|
||||
crate::entities::table::block_cell_inserts(tab, document, table_anno)
|
||||
crate::entities::table::block_cell_inserts(table, document, table_anno)
|
||||
{
|
||||
wires.extend(tessellate_entity(
|
||||
document,
|
||||
|
|
@ -927,13 +918,10 @@ pub(crate) fn tessellate_entity(
|
|||
}
|
||||
if !wires.is_empty() {
|
||||
let aabb = entity_aabb(e);
|
||||
for w in &mut wires {
|
||||
w.aci = aci;
|
||||
// Empty SDF-text cells keep their tight glyph-box AABB; only
|
||||
// stroke/fill wires take the whole-table box as a broad-phase
|
||||
// pick hint (matches the dim / mleader / baked-block paths).
|
||||
if !w.points.is_empty() || !w.fill_tris.is_empty() {
|
||||
set_wire_aabb(w, aabb);
|
||||
for wire in &mut wires {
|
||||
wire.aci = aci;
|
||||
if !wire.points.is_empty() || !wire.fill_tris.is_empty() {
|
||||
set_wire_aabb(wire, aabb);
|
||||
}
|
||||
}
|
||||
return wires;
|
||||
|
|
@ -986,145 +974,63 @@ pub(crate) fn tessellate_entity(
|
|||
fill_tris_low: Vec::new(),
|
||||
};
|
||||
|
||||
if let Some(cache) = block_cache {
|
||||
// Xrefs render with the same hue but faded toward `bg_color` so
|
||||
// the user can recognise external-reference geometry at a glance.
|
||||
let is_xref = document
|
||||
.block_records
|
||||
.get(&ins.block_name)
|
||||
.map(|br| br.flags.is_xref || br.flags.is_xref_overlay)
|
||||
.unwrap_or(false);
|
||||
if let Some(mut wires) = cache::block_cache::expand_insert(
|
||||
cache,
|
||||
ins,
|
||||
h,
|
||||
ins_color,
|
||||
ins_pat_len,
|
||||
ins_pat,
|
||||
ins_lw_px,
|
||||
ins_layer,
|
||||
sel,
|
||||
pslt_factor,
|
||||
view_aabb,
|
||||
world_per_pixel,
|
||||
is_xref,
|
||||
bg_color,
|
||||
anno_scale,
|
||||
) {
|
||||
// XCLIP: if this INSERT carries an enabled spatial filter,
|
||||
// clip the expanded block geometry to the boundary polygon so
|
||||
// only the portion inside the clip is drawn.
|
||||
if let Some(sf) = pick::xclip::insert_spatial_filter(document, ins) {
|
||||
let poly = pick::xclip::world_clip_polygon_f64(sf, ins);
|
||||
pick::xclip::clip_wires(&mut wires, &poly);
|
||||
// XCLIPFRAME 1/2 shows the boundary itself on screen
|
||||
// (2 = shown but not plotted).
|
||||
if document.header.xclip_frame != 0 && poly.len() >= 3 {
|
||||
wires.push(pick::xclip::frame_wire(
|
||||
&poly,
|
||||
format!("{}_xclipframe", h.value()),
|
||||
ins_color,
|
||||
sel,
|
||||
ins_lw_px,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Per-INSERT attribute values. The block defn carries the
|
||||
// AttributeDefinitions (templates) which expand_insert skips;
|
||||
// the AttributeEntity instances live on the Insert itself in
|
||||
// WCS and need their own tessellation so the user sees the
|
||||
// values they actually filled in. See #20.
|
||||
crate::entities::insert::append_insert_attribute_wires(
|
||||
&mut wires,
|
||||
let fallback_depths = rustc_hash::FxHashMap::default();
|
||||
let fallback_cache;
|
||||
let cache = match block_cache {
|
||||
Some(cache) if cache.defn(&ins.block_name).is_some() => cache,
|
||||
_ => {
|
||||
fallback_cache = cache::block_cache::BlockCache::build_for_block(
|
||||
document,
|
||||
ins,
|
||||
h,
|
||||
sel,
|
||||
ins_color,
|
||||
ins_pat_len,
|
||||
ins_pat,
|
||||
ins_lw_px,
|
||||
ins_layer,
|
||||
bg_color,
|
||||
is_xref,
|
||||
pslt_factor,
|
||||
&ins.block_name,
|
||||
anno_scale,
|
||||
annotation_scale_handle,
|
||||
true,
|
||||
bg_color,
|
||||
&fallback_depths,
|
||||
);
|
||||
wires.push(marker);
|
||||
return wires;
|
||||
&fallback_cache
|
||||
}
|
||||
};
|
||||
let is_xref = document
|
||||
.block_records
|
||||
.get(&ins.block_name)
|
||||
.map(|record| record.flags.is_xref || record.flags.is_xref_overlay)
|
||||
.unwrap_or(false);
|
||||
let mut wires = cache::block_cache::expand_insert(
|
||||
cache,
|
||||
ins,
|
||||
h,
|
||||
ins_color,
|
||||
ins_pat_len,
|
||||
ins_pat,
|
||||
ins_lw_px,
|
||||
ins_layer,
|
||||
sel,
|
||||
pslt_factor,
|
||||
view_aabb,
|
||||
world_per_pixel,
|
||||
is_xref,
|
||||
bg_color,
|
||||
anno_scale,
|
||||
)
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(filter) = pick::xclip::insert_spatial_filter(document, ins) {
|
||||
let transform = crate::scene::render_graph::insert_transform(document, ins);
|
||||
let polygon =
|
||||
pick::xclip::world_clip_polygon_for_transform(filter, &transform);
|
||||
pick::xclip::clip_wires(&mut wires, &polygon);
|
||||
if document.header.xclip_frame != 0 && polygon.len() >= 3 {
|
||||
wires.push(pick::xclip::frame_wire(
|
||||
&polygon,
|
||||
format!("{}_xclipframe", h.value()),
|
||||
ins_color,
|
||||
sel,
|
||||
ins_lw_px,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Cache miss / unavailable: fall back to the original explode path.
|
||||
// The block_cache primary path covers all typical Inserts; this
|
||||
// branch only fires for pathological cache failures.
|
||||
let br = document.block_records.get(&ins.block_name);
|
||||
let is_xref = br
|
||||
.map(|br| br.flags.is_xref || br.flags.is_xref_overlay)
|
||||
.unwrap_or(false);
|
||||
let mut wires: Vec<WireModel> = ins
|
||||
.explode_from_document(document)
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(crate::modules::draw::modify::explode::normalize_insert_entity)
|
||||
.flat_map(|sub| {
|
||||
let (sub_color, sub_pattern_length, sub_pattern, sub_line_weight_px, sub_aci) =
|
||||
view::render::render_style_for_block_sub(
|
||||
document,
|
||||
&sub,
|
||||
ins_color,
|
||||
ins_pat_len,
|
||||
ins_pat,
|
||||
ins_lw_px,
|
||||
ins_layer,
|
||||
);
|
||||
let sub_color = view::render::adapt_to_bg(sub_color, bg_color);
|
||||
let sub_color = if is_xref && !sel {
|
||||
cache::block_cache::fade_toward_bg(sub_color, bg_color)
|
||||
} else {
|
||||
sub_color
|
||||
};
|
||||
let sub_aabb = entity_aabb(&sub);
|
||||
let sub_pattern_length = sub_pattern_length * pslt_factor;
|
||||
let sub_pattern = sub_pattern.map(|v| v * pslt_factor);
|
||||
let mut wires = convert::tessellate::tessellate(
|
||||
document,
|
||||
h,
|
||||
&sub,
|
||||
sel,
|
||||
sub_color,
|
||||
sub_pattern_length,
|
||||
sub_pattern,
|
||||
sub_line_weight_px,
|
||||
anno_scale,
|
||||
annotation_scale_handle,
|
||||
world_per_pixel,
|
||||
bg_color,
|
||||
false,
|
||||
);
|
||||
for w in &mut wires {
|
||||
w.name = h.value().to_string();
|
||||
w.aci = sub_aci;
|
||||
// Keep the glyph-bounds AABB tessellate set on SDF text wires
|
||||
// (their geometry is in `text_verts`, not `points`); clobbering
|
||||
// it here left block text with an UNBOUNDED box. For every
|
||||
// other wire use the sub-entity box, falling back to the wire's
|
||||
// own world points when that box is degenerate/unimplemented
|
||||
// (UNBOUNDED) — otherwise it never culls and stalls snapping on
|
||||
// block-heavy drawings.
|
||||
if w.text_verts.is_empty() {
|
||||
let box_ = if sub_aabb == WireModel::UNBOUNDED_AABB {
|
||||
wire_points_aabb(w)
|
||||
} else {
|
||||
sub_aabb
|
||||
};
|
||||
set_wire_aabb(w, box_);
|
||||
}
|
||||
}
|
||||
wires
|
||||
})
|
||||
.collect();
|
||||
crate::entities::insert::append_insert_attribute_wires(
|
||||
&mut wires,
|
||||
document,
|
||||
|
|
|
|||
|
|
@ -1547,6 +1547,15 @@ pub(crate) fn arrow_from_block(
|
|||
doc: &CadDocument,
|
||||
handle: acadrust::types::Handle,
|
||||
dimasz: f32,
|
||||
) -> ArrowKind {
|
||||
arrow_from_block_with_deferred_hatch(doc, handle, dimasz, false)
|
||||
}
|
||||
|
||||
pub(crate) fn arrow_from_block_with_deferred_hatch(
|
||||
doc: &CadDocument,
|
||||
handle: acadrust::types::Handle,
|
||||
dimasz: f32,
|
||||
defer_hatch: bool,
|
||||
) -> ArrowKind {
|
||||
if handle.is_null() {
|
||||
return arrow_from_block_name(None, dimasz);
|
||||
|
|
@ -1557,7 +1566,7 @@ pub(crate) fn arrow_from_block(
|
|||
if let Some(arrow) = builtin_arrow_from_block_name(&record.name, dimasz) {
|
||||
return arrow;
|
||||
}
|
||||
custom_arrow_from_block(doc, record, dimasz)
|
||||
custom_arrow_from_block(doc, record, dimasz, defer_hatch)
|
||||
.unwrap_or_else(|| arrow_from_block_name(None, dimasz))
|
||||
}
|
||||
|
||||
|
|
@ -1653,6 +1662,7 @@ fn custom_arrow_from_block(
|
|||
doc: &CadDocument,
|
||||
record: &acadrust::tables::BlockRecord,
|
||||
dimasz: f32,
|
||||
defer_hatch: bool,
|
||||
) -> Option<ArrowKind> {
|
||||
if record.is_layout()
|
||||
|| record.is_model_space()
|
||||
|
|
@ -1663,17 +1673,42 @@ fn custom_arrow_from_block(
|
|||
{
|
||||
return None;
|
||||
}
|
||||
let base = block_base_point(doc, record);
|
||||
|
||||
let depths = rustc_hash::FxHashMap::default();
|
||||
let graph = crate::scene::render_graph::RenderSceneGraph::new(
|
||||
doc,
|
||||
None,
|
||||
None,
|
||||
true,
|
||||
&depths,
|
||||
);
|
||||
let insert = acadrust::entities::Insert::new(
|
||||
record.name.clone(),
|
||||
acadrust::types::Vector3::ZERO,
|
||||
);
|
||||
let mut lines = Vec::new();
|
||||
let mut fill = Vec::new();
|
||||
let mut stack = vec![record.name.clone()];
|
||||
for &entity_handle in &record.entity_handles {
|
||||
let Some(entity) = doc.get_entity(entity_handle) else {
|
||||
continue;
|
||||
};
|
||||
collect_custom_arrow_entity(doc, entity, base, &mut lines, &mut fill, &mut stack, 0);
|
||||
}
|
||||
if lines.is_empty() && fill.is_empty() {
|
||||
let mut deferred_hatch = false;
|
||||
graph.walk_insert(
|
||||
&insert,
|
||||
record.handle,
|
||||
|_, _| true,
|
||||
|entity, context| {
|
||||
if defer_hatch && matches!(entity, EntityType::Hatch(_)) {
|
||||
deferred_hatch = true;
|
||||
return;
|
||||
}
|
||||
let mut placed = entity.clone();
|
||||
placed.apply_transform(&context.transform);
|
||||
append_custom_arrow_leaf(
|
||||
doc,
|
||||
&placed,
|
||||
&mut lines,
|
||||
&mut fill,
|
||||
);
|
||||
},
|
||||
);
|
||||
if lines.is_empty() && fill.is_empty() && !deferred_hatch {
|
||||
None
|
||||
} else {
|
||||
Some(ArrowKind::Custom {
|
||||
|
|
@ -1684,67 +1719,27 @@ fn custom_arrow_from_block(
|
|||
}
|
||||
}
|
||||
|
||||
fn block_base_point(
|
||||
doc: &CadDocument,
|
||||
record: &acadrust::tables::BlockRecord,
|
||||
) -> acadrust::types::Vector3 {
|
||||
doc.get_entity(record.block_entity_handle)
|
||||
.and_then(|entity| match entity {
|
||||
EntityType::Block(block) => Some(block.base_point),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(acadrust::types::Vector3::ZERO)
|
||||
}
|
||||
|
||||
fn collect_custom_arrow_entity(
|
||||
fn append_custom_arrow_leaf(
|
||||
doc: &CadDocument,
|
||||
entity: &EntityType,
|
||||
root_base: acadrust::types::Vector3,
|
||||
lines: &mut Vec<[f32; 3]>,
|
||||
fill: &mut Vec<[f32; 3]>,
|
||||
stack: &mut Vec<String>,
|
||||
depth: usize,
|
||||
) {
|
||||
if depth >= 32 || entity.common().invisible {
|
||||
return;
|
||||
}
|
||||
match entity {
|
||||
EntityType::Block(_)
|
||||
| EntityType::BlockEnd(_)
|
||||
| EntityType::AttributeDefinition(_)
|
||||
| EntityType::Dimension(_)
|
||||
| EntityType::Leader(_)
|
||||
| EntityType::MultiLeader(_) => return,
|
||||
EntityType::Insert(insert) => {
|
||||
if stack.iter().any(|name| name.eq_ignore_ascii_case(&insert.block_name)) {
|
||||
return;
|
||||
}
|
||||
let Some(record) = doc.block_records.get(&insert.block_name) else {
|
||||
return;
|
||||
};
|
||||
let nested_base = block_base_point(doc, record);
|
||||
let transform = insert.get_transform();
|
||||
let transformed_zero = transform.apply(acadrust::types::Vector3::ZERO);
|
||||
let transformed_base = transform.apply(nested_base);
|
||||
let correction = transformed_zero - transformed_base;
|
||||
stack.push(insert.block_name.clone());
|
||||
for mut child in insert.explode_from_document(doc) {
|
||||
child.as_entity_mut().translate(correction);
|
||||
collect_custom_arrow_entity(
|
||||
doc,
|
||||
&child,
|
||||
root_base,
|
||||
lines,
|
||||
fill,
|
||||
stack,
|
||||
depth + 1,
|
||||
);
|
||||
}
|
||||
stack.pop();
|
||||
return;
|
||||
}
|
||||
| EntityType::MultiLeader(_)
|
||||
| EntityType::Insert(_) => return,
|
||||
EntityType::Hatch(hatch) => {
|
||||
append_custom_hatch_fill(hatch, root_base, fill);
|
||||
append_custom_hatch_geometry(
|
||||
hatch,
|
||||
acadrust::types::Vector3::ZERO,
|
||||
lines,
|
||||
fill,
|
||||
);
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
|
|
@ -1766,15 +1761,26 @@ fn collect_custom_arrow_entity(
|
|||
true,
|
||||
);
|
||||
for wire in wires {
|
||||
append_custom_wire_points(&wire.points, &wire.points_low, root_base, lines);
|
||||
append_custom_fill_points(&wire.fill_tris, &wire.fill_tris_low, root_base, fill);
|
||||
append_custom_wire_points(
|
||||
&wire.points,
|
||||
&wire.points_low,
|
||||
acadrust::types::Vector3::ZERO,
|
||||
lines,
|
||||
);
|
||||
append_custom_fill_points(
|
||||
&wire.fill_tris,
|
||||
&wire.fill_tris_low,
|
||||
acadrust::types::Vector3::ZERO,
|
||||
fill,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn append_custom_hatch_fill(
|
||||
fn append_custom_hatch_geometry(
|
||||
hatch: &acadrust::entities::Hatch,
|
||||
base: acadrust::types::Vector3,
|
||||
out: &mut Vec<[f32; 3]>,
|
||||
lines: &mut Vec<[f32; 3]>,
|
||||
fill: &mut Vec<[f32; 3]>,
|
||||
) {
|
||||
use lyon_tessellation::math::point;
|
||||
use lyon_tessellation::path::Path;
|
||||
|
|
@ -1786,6 +1792,29 @@ fn append_custom_hatch_fill(
|
|||
return;
|
||||
};
|
||||
|
||||
if matches!(
|
||||
&model.pattern,
|
||||
crate::scene::model::hatch_model::HatchPattern::Pattern(_)
|
||||
) {
|
||||
let z = -base.z as f32;
|
||||
for [start, end] in model.pattern_segments() {
|
||||
if !lines.is_empty() && !lines.last().is_some_and(|point| point[0].is_nan()) {
|
||||
lines.push([f32::NAN; 3]);
|
||||
}
|
||||
lines.push([
|
||||
(start[0] - base.x) as f32,
|
||||
(start[1] - base.y) as f32,
|
||||
z,
|
||||
]);
|
||||
lines.push([
|
||||
(end[0] - base.x) as f32,
|
||||
(end[1] - base.y) as f32,
|
||||
z,
|
||||
]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let mut builder = Path::builder();
|
||||
let mut ring: Vec<[f32; 2]> = Vec::new();
|
||||
let mut finish_ring = |ring: &mut Vec<[f32; 2]>| {
|
||||
|
|
@ -1825,7 +1854,7 @@ fn append_custom_hatch_fill(
|
|||
return;
|
||||
}
|
||||
let z = -base.z as f32;
|
||||
out.extend(
|
||||
fill.extend(
|
||||
geometry
|
||||
.indices
|
||||
.iter()
|
||||
|
|
|
|||
Loading…
Reference in a new issue