forked from 1b-agent/opencadstudio-upstream
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
1 parent
163725ca22
commit
aadb64860d
12 files changed
+3016
-2832
No files matched your search
File diff suppressed because it is too large.
Load diff
+181
-1
@@ -3,8 +3,10 @@ use glam::Vec3;
|
||||
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{diamond_grip, edit_prop as edit, parse_f64, ro_prop as ro};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable};
|
||||
use crate::entities::traits::{Grippable, LegacyTess, PropertyEditable, Transformable};
|
||||
use crate::scene::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||||
use crate::scene::tess_util::{arc_segments, arc_signed_span, wire_chord_tol, LegacyGeometry};
|
||||
use crate::scene::wire_model::SnapHint;
|
||||
|
||||
fn properties(h: &Hatch) -> PropSection {
|
||||
let pattern_type = match h.pattern_type {
|
||||
@@ -347,3 +349,181 @@ impl Grippable for Hatch {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LegacyTess for Hatch {
|
||||
fn legacy_geometry(&self, world_offset: [f64; 3]) -> LegacyGeometry {
|
||||
let [ox, oy, oz] = world_offset;
|
||||
let normal = (self.normal.x, self.normal.y, self.normal.z);
|
||||
// Convert a 2D OCS hatch boundary point to WCS, then subtract world_offset.
|
||||
let to_wcs = |x: f64, y: f64| -> [f32; 3] {
|
||||
let (wx, wy, wz) =
|
||||
crate::scene::transform::ocs_point_to_wcs((x, y, self.elevation), normal);
|
||||
[(wx - ox) as f32, (wy - oy) as f32, (wz - oz) as f32]
|
||||
};
|
||||
let mut pts: Vec<[f32; 3]> = Vec::new();
|
||||
let mut key_verts: Vec<[f32; 3]> = Vec::new();
|
||||
let mut snap_pts: Vec<(Vec3, SnapHint)> = Vec::new();
|
||||
for path in &self.paths {
|
||||
for edge in &path.edges {
|
||||
match edge {
|
||||
BoundaryEdge::Polyline(poly) => {
|
||||
// Hatch-boundary polyline vertices encode bulge in
|
||||
// `Vector3.z`; straight segments emit just the
|
||||
// start vertex, bulged segments tessellate the arc
|
||||
// between v0 → v1.
|
||||
let start_idx = pts.len();
|
||||
let verts = &poly.vertices;
|
||||
let count = verts.len();
|
||||
let seg_count = if poly.is_closed {
|
||||
count
|
||||
} else {
|
||||
count.saturating_sub(1)
|
||||
};
|
||||
if count > 0 && pts.len() != start_idx {
|
||||
pts.push([f32::NAN; 3]);
|
||||
}
|
||||
for i in 0..seg_count {
|
||||
let v0 = &verts[i];
|
||||
let v1 = &verts[(i + 1) % count];
|
||||
let bulge = v0.z;
|
||||
if bulge.abs() < 1e-9 {
|
||||
let p = to_wcs(v0.x, v0.y);
|
||||
pts.push(p);
|
||||
key_verts.push(p);
|
||||
continue;
|
||||
}
|
||||
let theta = 4.0 * bulge.atan();
|
||||
let dx = v1.x - v0.x;
|
||||
let dy = v1.y - v0.y;
|
||||
let d = (dx * dx + dy * dy).sqrt();
|
||||
if d < 1e-12 {
|
||||
let p = to_wcs(v0.x, v0.y);
|
||||
pts.push(p);
|
||||
key_verts.push(p);
|
||||
continue;
|
||||
}
|
||||
let r = (d * 0.5) / (theta * 0.5).sin().abs();
|
||||
let mx = (v0.x + v1.x) * 0.5;
|
||||
let my = (v0.y + v1.y) * 0.5;
|
||||
let px = -dy / d;
|
||||
let py = dx / d;
|
||||
let sign = if bulge > 0.0 { 1.0_f64 } else { -1.0_f64 };
|
||||
let center_offset = r * (theta * 0.5).cos();
|
||||
let cx = mx + sign * px * center_offset;
|
||||
let cy = my + sign * py * center_offset;
|
||||
let a0 = (v0.y - cy).atan2(v0.x - cx);
|
||||
let a1 = (v1.y - cy).atan2(v1.x - cx);
|
||||
let mut sweep = a1 - a0;
|
||||
const TAU: f64 = std::f64::consts::TAU;
|
||||
if bulge > 0.0 {
|
||||
if sweep <= 0.0 {
|
||||
sweep += TAU;
|
||||
}
|
||||
} else if sweep >= 0.0 {
|
||||
sweep -= TAU;
|
||||
}
|
||||
if sweep.abs() < 1e-9 {
|
||||
sweep = if bulge > 0.0 { TAU } else { -TAU };
|
||||
}
|
||||
let segs = arc_segments(r, sweep.abs(), wire_chord_tol(r));
|
||||
for j in 0..segs {
|
||||
let a = a0 + sweep * (j as f64 / segs as f64);
|
||||
let p = to_wcs(cx + r * a.cos(), cy + r * a.sin());
|
||||
pts.push(p);
|
||||
if j == 0 {
|
||||
key_verts.push(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Close the loop visually for closed polylines by
|
||||
// returning to the first emitted point.
|
||||
if poly.is_closed {
|
||||
if let Some(first) = pts.get(start_idx).cloned() {
|
||||
if first[0].is_finite() {
|
||||
pts.push(first);
|
||||
}
|
||||
}
|
||||
} else if let Some(last) = verts.last() {
|
||||
let p = to_wcs(last.x, last.y);
|
||||
pts.push(p);
|
||||
key_verts.push(p);
|
||||
}
|
||||
}
|
||||
BoundaryEdge::Line(ln) => {
|
||||
let p0 = to_wcs(ln.start.x, ln.start.y);
|
||||
let p1 = to_wcs(ln.end.x, ln.end.y);
|
||||
if !pts.is_empty() {
|
||||
pts.push([f32::NAN; 3]);
|
||||
}
|
||||
pts.push(p0);
|
||||
pts.push(p1);
|
||||
key_verts.push(p0);
|
||||
key_verts.push(p1);
|
||||
}
|
||||
BoundaryEdge::CircularArc(arc) => {
|
||||
let (sa, span) =
|
||||
arc_signed_span(arc.start_angle, arc.end_angle, arc.counter_clockwise);
|
||||
let segs = arc_segments(arc.radius, span.abs(), wire_chord_tol(arc.radius));
|
||||
if !pts.is_empty() {
|
||||
pts.push([f32::NAN; 3]);
|
||||
}
|
||||
for i in 0..=segs {
|
||||
let t = sa + span * (i as f64 / segs as f64);
|
||||
let p = to_wcs(
|
||||
arc.center.x + arc.radius * t.cos(),
|
||||
arc.center.y + arc.radius * t.sin(),
|
||||
);
|
||||
pts.push(p);
|
||||
if i == 0 || i == segs {
|
||||
key_verts.push(p);
|
||||
}
|
||||
}
|
||||
snap_pts.push((
|
||||
Vec3::from(to_wcs(arc.center.x, arc.center.y)),
|
||||
SnapHint::Center,
|
||||
));
|
||||
}
|
||||
BoundaryEdge::EllipticArc(ell) => {
|
||||
let r_maj = (ell.major_axis_endpoint.x * ell.major_axis_endpoint.x
|
||||
+ ell.major_axis_endpoint.y * ell.major_axis_endpoint.y)
|
||||
.sqrt();
|
||||
let r_min = r_maj * ell.minor_axis_ratio;
|
||||
let rot = ell
|
||||
.major_axis_endpoint
|
||||
.y
|
||||
.atan2(ell.major_axis_endpoint.x);
|
||||
let (sa, span) =
|
||||
arc_signed_span(ell.start_angle, ell.end_angle, ell.counter_clockwise);
|
||||
let segs = arc_segments(r_maj, span.abs(), wire_chord_tol(r_maj));
|
||||
if !pts.is_empty() {
|
||||
pts.push([f32::NAN; 3]);
|
||||
}
|
||||
let (cr, sr) = (rot.cos(), rot.sin());
|
||||
for i in 0..=segs {
|
||||
let t = sa + span * (i as f64 / segs as f64);
|
||||
let lx = r_maj * t.cos();
|
||||
let ly = r_min * t.sin();
|
||||
let p = to_wcs(
|
||||
ell.center.x + lx * cr - ly * sr,
|
||||
ell.center.y + lx * sr + ly * cr,
|
||||
);
|
||||
pts.push(p);
|
||||
if i == 0 || i == segs {
|
||||
key_verts.push(p);
|
||||
}
|
||||
}
|
||||
snap_pts.push((
|
||||
Vec3::from(to_wcs(ell.center.x, ell.center.y)),
|
||||
SnapHint::Center,
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if pts.is_empty() {
|
||||
pts = vec![[0.0, 0.0, 0.0], [0.0, 0.0, 0.0]];
|
||||
}
|
||||
(pts, snap_pts, vec![], key_verts)
|
||||
}
|
||||
}
|
||||
@@ -118,3 +118,27 @@ impl Transformable for Insert {
|
||||
apply_transform(self, t);
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::entities::traits::LegacyTess for Insert {
|
||||
fn legacy_geometry(&self, world_offset: [f64; 3]) -> crate::scene::tess_util::LegacyGeometry {
|
||||
let [ox, oy, oz] = world_offset;
|
||||
let ip = Vec3::new(
|
||||
(self.insert_point.x - ox) as f32,
|
||||
(self.insert_point.y - oy) as f32,
|
||||
(self.insert_point.z - oz) as f32,
|
||||
);
|
||||
let s = 0.1_f32;
|
||||
let pts = vec![
|
||||
[ip.x - s, ip.y, ip.z],
|
||||
[ip.x + s, ip.y, ip.z],
|
||||
[ip.x, ip.y - s, ip.z],
|
||||
[ip.x, ip.y + s, ip.z],
|
||||
];
|
||||
(
|
||||
pts,
|
||||
vec![(ip, crate::scene::wire_model::SnapHint::Insertion)],
|
||||
vec![],
|
||||
vec![],
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -480,3 +480,167 @@ impl Transformable for Leader {
|
||||
apply_transform(self, t);
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-entity tessellation entry for `Leader`. Lives here so all leader
|
||||
/// tess code stays alongside the entity definition. Cross-entity dim
|
||||
/// machinery (arrow shapes, `DimGeom`) lives in `scene::tessellate` and
|
||||
/// is reused via the dim arrow emitter so the leader matches the active
|
||||
/// DIMSTYLE.
|
||||
pub trait LeaderTess {
|
||||
fn tessellate(
|
||||
&self,
|
||||
document: &acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
selected: bool,
|
||||
entity_color: [f32; 4],
|
||||
line_weight_px: f32,
|
||||
world_offset: [f64; 3],
|
||||
anno_scale: f32,
|
||||
) -> crate::scene::wire_model::WireModel;
|
||||
}
|
||||
|
||||
impl LeaderTess for Leader {
|
||||
fn tessellate(
|
||||
&self,
|
||||
document: &acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
selected: bool,
|
||||
entity_color: [f32; 4],
|
||||
line_weight_px: f32,
|
||||
world_offset: [f64; 3],
|
||||
anno_scale: f32,
|
||||
) -> crate::scene::wire_model::WireModel {
|
||||
use crate::scene::tessellate::{append_arrow, arrow_from_block, ArrowKind, DimGeom};
|
||||
use crate::scene::wire_model::WireModel;
|
||||
let color = if selected {
|
||||
WireModel::SELECTED
|
||||
} else {
|
||||
entity_color
|
||||
};
|
||||
let name = handle.value().to_string();
|
||||
let [ox, oy, oz] = world_offset;
|
||||
let p3 = |v: &acadrust::types::Vector3| -> [f32; 3] {
|
||||
[(v.x - ox) as f32, (v.y - oy) as f32, (v.z - oz) as f32]
|
||||
};
|
||||
let nan = [f32::NAN; 3];
|
||||
|
||||
let verts = &self.vertices;
|
||||
|
||||
if verts.len() < 2 {
|
||||
return WireModel {
|
||||
name,
|
||||
points: vec![],
|
||||
color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px,
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
};
|
||||
}
|
||||
|
||||
let mut points: Vec<[f32; 3]> = verts.iter().map(|v| p3(v)).collect();
|
||||
let mut tangents: Vec<TangentGeom> = Vec::new();
|
||||
let key_vertices: Vec<[f32; 3]> = verts.iter().map(|v| p3(v)).collect();
|
||||
let mut fill_tris: Vec<[f32; 3]> = Vec::new();
|
||||
|
||||
for i in 0..verts.len().saturating_sub(1) {
|
||||
tangents.push(TangentGeom::Line {
|
||||
p1: p3(&verts[i]),
|
||||
p2: p3(&verts[i + 1]),
|
||||
});
|
||||
}
|
||||
|
||||
if self.arrow_enabled {
|
||||
// Resolve the active dim style → DIMLDRBLK to pick the arrow shape.
|
||||
// DIMASZ × DIMSCALE drives the size when available; otherwise fall
|
||||
// back to the legacy text-height heuristic.
|
||||
let style = document.dim_styles.iter().find(|s| {
|
||||
s.name.eq_ignore_ascii_case(&self.dimension_style)
|
||||
|| (self.dimension_style.trim().is_empty()
|
||||
&& s.name.eq_ignore_ascii_case("Standard"))
|
||||
});
|
||||
let dim_scale = style
|
||||
.map(|s| {
|
||||
if s.dimscale > 1e-6 {
|
||||
s.dimscale
|
||||
} else {
|
||||
anno_scale as f64
|
||||
}
|
||||
})
|
||||
.unwrap_or(anno_scale as f64);
|
||||
let arrow_size = match style {
|
||||
Some(s) => (s.dimasz * dim_scale) as f32,
|
||||
None => (self.text_height as f32).max(1.0) * 0.8 * anno_scale,
|
||||
};
|
||||
let arrow = match style {
|
||||
Some(s) => arrow_from_block(document, s.dimldrblk, arrow_size.max(0.001)),
|
||||
None => ArrowKind::Triangle {
|
||||
size: arrow_size.max(0.001),
|
||||
filled: true,
|
||||
size_mul: 1.0,
|
||||
},
|
||||
};
|
||||
|
||||
let tip = &verts[0];
|
||||
let next = &verts[1];
|
||||
let dx = (next.x - tip.x) as f32;
|
||||
let dy = (next.y - tip.y) as f32;
|
||||
let len = (dx * dx + dy * dy).sqrt().max(1e-9);
|
||||
let dir = Vec3::new(dx / len, dy / len, 0.0);
|
||||
let tip_f = p3(tip);
|
||||
let tip_v = Vec3::new(tip_f[0], tip_f[1], tip_f[2]);
|
||||
// Reuse the dim arrow emitter so the leader shape matches the
|
||||
// DIMSTYLE in use (Closed Filled by default, Dot, Tick, …).
|
||||
let mut arrow_pts: Vec<[f32; 3]> = Vec::new();
|
||||
let mut arrow_geom = DimGeom::new();
|
||||
append_arrow(&mut arrow_geom, tip_v, dir, &arrow);
|
||||
if !arrow_geom.dim_lines.is_empty() {
|
||||
arrow_pts.push(nan);
|
||||
arrow_pts.extend(arrow_geom.dim_lines);
|
||||
}
|
||||
points.extend(arrow_pts);
|
||||
fill_tris.extend(arrow_geom.arrow_fill);
|
||||
}
|
||||
|
||||
if self.hookline_enabled {
|
||||
let last = verts.last().unwrap();
|
||||
let prev = &verts[verts.len() - 2];
|
||||
let sign = if (last.x - prev.x) >= 0.0 {
|
||||
1.0_f32
|
||||
} else {
|
||||
-1.0_f32
|
||||
};
|
||||
let land_len = self.text_height as f32 * 1.5 * anno_scale;
|
||||
let last_f = p3(last);
|
||||
points.push(nan);
|
||||
points.push(last_f);
|
||||
points.push([last_f[0] + sign * land_len, last_f[1], last_f[2]]);
|
||||
}
|
||||
|
||||
WireModel {
|
||||
name,
|
||||
points,
|
||||
color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px,
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: tangents,
|
||||
key_vertices,
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -850,3 +850,664 @@ impl Transformable for MultiLeader {
|
||||
apply_transform(self, t);
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-entity tessellation entry for `MultiLeader`. Returns multiple
|
||||
/// `WireModel`s (leader/dogleg + arrow fill, optional block content,
|
||||
/// text strokes, frame, background fill) so each piece can carry its
|
||||
/// own colour.
|
||||
pub trait MultiLeaderTess {
|
||||
fn tessellate(
|
||||
&self,
|
||||
document: &acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
selected: bool,
|
||||
entity_color: [f32; 4],
|
||||
line_weight_px: f32,
|
||||
world_offset: [f64; 3],
|
||||
anno_scale: f32,
|
||||
world_per_pixel: Option<f32>,
|
||||
) -> Vec<crate::scene::wire_model::WireModel>;
|
||||
}
|
||||
|
||||
impl MultiLeaderTess for MultiLeader {
|
||||
fn tessellate(
|
||||
&self,
|
||||
document: &acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
selected: bool,
|
||||
entity_color: [f32; 4],
|
||||
line_weight_px: f32,
|
||||
world_offset: [f64; 3],
|
||||
anno_scale: f32,
|
||||
world_per_pixel: Option<f32>,
|
||||
) -> Vec<crate::scene::wire_model::WireModel> {
|
||||
use crate::scene::tessellate::{
|
||||
append_arrow, arrow_from_block, color_or_inherit, tessellate, ArrowKind, DimGeom,
|
||||
};
|
||||
use crate::scene::wire_model::{SnapHint, TangentGeom, WireModel};
|
||||
use glam::Vec3;
|
||||
let ml = self;
|
||||
// line_color falls back to the entity colour when the MultiLeader's own
|
||||
// colour is ByBlock/ByLayer; otherwise the leader uses its stored hue.
|
||||
let line_color = if selected {
|
||||
WireModel::SELECTED
|
||||
} else {
|
||||
color_or_inherit(&ml.line_color, entity_color)
|
||||
};
|
||||
// ml.line_weight is the leader-line weight override. Negative codes
|
||||
// (ByLayer/ByBlock/Default) fall through to the entity's already-resolved
|
||||
// pixel width.
|
||||
let leader_lw_px = match ml.line_weight {
|
||||
acadrust::types::LineWeight::Value(v) if v >= 0 => {
|
||||
(v as f32 / 100.0) * (96.0 / 25.4)
|
||||
}
|
||||
_ => line_weight_px,
|
||||
};
|
||||
// ml.line_type_handle — resolve via line_types table by handle and apply
|
||||
// the resulting dash pattern to the leader wire.
|
||||
let lt_scale = document.header.linetype_scale as f32 * ml.common.linetype_scale as f32;
|
||||
let (leader_pat_len, leader_pat) = match ml.line_type_handle {
|
||||
Some(h) if !h.is_null() => {
|
||||
let name = document
|
||||
.line_types
|
||||
.iter()
|
||||
.find(|lt| lt.handle == h)
|
||||
.map(|lt| lt.name.clone());
|
||||
match name {
|
||||
Some(n) => crate::scene::render::resolve_pattern(&document.line_types, &n, lt_scale),
|
||||
None => (0.0, [0.0; 8]),
|
||||
}
|
||||
}
|
||||
_ => (0.0, [0.0; 8]),
|
||||
};
|
||||
|
||||
let name = handle.value().to_string();
|
||||
let nan = [f32::NAN; 3];
|
||||
let [ox, oy, oz] = world_offset;
|
||||
let p3 = |v: &acadrust::types::Vector3| -> [f32; 3] {
|
||||
[(v.x - ox) as f32, (v.y - oy) as f32, (v.z - oz) as f32]
|
||||
};
|
||||
|
||||
// ── Scaling ──────────────────────────────────────────────────────────────
|
||||
// ml.scale_factor is always applied; anno_scale is only applied when the
|
||||
// multileader is marked annotative.
|
||||
let effective_scale = (ml.scale_factor as f32)
|
||||
* if ml.enable_annotation_scale {
|
||||
anno_scale
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
let arrow_size = ml.arrowhead_size as f32 * effective_scale;
|
||||
let draw_arrow = arrow_size > 0.0;
|
||||
let invisible = ml.path_type == MultiLeaderPathType::Invisible;
|
||||
// arrowhead_handle resolves through the block records to a named arrow
|
||||
// block (matches DIMLDRBLK on Dimension). Null handle / unknown name →
|
||||
// ClosedFilled triangle.
|
||||
let arrow_kind = match ml.arrowhead_handle {
|
||||
Some(h) if !h.is_null() => arrow_from_block(document, h, arrow_size.max(0.001)),
|
||||
_ => ArrowKind::Triangle {
|
||||
size: arrow_size.max(0.001),
|
||||
filled: true,
|
||||
size_mul: 1.0,
|
||||
},
|
||||
};
|
||||
|
||||
// ── Leader / arrow / dogleg points ───────────────────────────────────────
|
||||
let mut points: Vec<[f32; 3]> = Vec::new();
|
||||
let mut key_verts: Vec<[f32; 3]> = Vec::new();
|
||||
let mut snap_pts: Vec<(Vec3, SnapHint)> = Vec::new();
|
||||
let mut tangents: Vec<TangentGeom> = Vec::new();
|
||||
let mut arrow_fill: Vec<[f32; 3]> = Vec::new();
|
||||
let mut first = true;
|
||||
|
||||
for root in &ml.context.leader_roots {
|
||||
let cp = &root.connection_point;
|
||||
let cp_f = p3(cp);
|
||||
snap_pts.push((Vec3::from(cp_f), SnapHint::Node));
|
||||
|
||||
for line in &root.lines {
|
||||
if line.points.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !invisible {
|
||||
if !first {
|
||||
points.push(nan);
|
||||
}
|
||||
first = false;
|
||||
|
||||
let mut ctrl: Vec<[f32; 3]> = line.points.iter().map(|p| p3(p)).collect();
|
||||
let last_f = *ctrl.last().unwrap_or(&cp_f);
|
||||
let dist = ((last_f[0] - cp_f[0]).powi(2) + (last_f[1] - cp_f[1]).powi(2)).sqrt();
|
||||
if dist > 1e-9 {
|
||||
ctrl.push(cp_f);
|
||||
}
|
||||
for &c in &ctrl {
|
||||
key_verts.push(c);
|
||||
snap_pts.push((Vec3::from(c), SnapHint::Node));
|
||||
}
|
||||
|
||||
if ml.path_type == MultiLeaderPathType::Spline && ctrl.len() >= 2 {
|
||||
let ctrl_f64: Vec<[f64; 3]> = ctrl
|
||||
.iter()
|
||||
.map(|c| [c[0] as f64, c[1] as f64, c[2] as f64])
|
||||
.collect();
|
||||
for pt in catmull_rom_pts(&ctrl_f64, 8) {
|
||||
points.push([pt[0] as f32, pt[1] as f32, pt[2] as f32]);
|
||||
}
|
||||
} else {
|
||||
for &c in &ctrl {
|
||||
points.push(c);
|
||||
}
|
||||
}
|
||||
for i in 0..ctrl.len().saturating_sub(1) {
|
||||
tangents.push(TangentGeom::Line {
|
||||
p1: ctrl[i],
|
||||
p2: ctrl[i + 1],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if draw_arrow {
|
||||
let tip = &line.points[0];
|
||||
let tip_f = p3(tip);
|
||||
let next = if line.points.len() >= 2 {
|
||||
line.points[1]
|
||||
} else {
|
||||
*cp
|
||||
};
|
||||
let dx = (next.x - tip.x) as f32;
|
||||
let dy = (next.y - tip.y) as f32;
|
||||
let dl = (dx * dx + dy * dy).sqrt().max(1e-9);
|
||||
let dir = Vec3::new(dx / dl, dy / dl, 0.0);
|
||||
let tip_v = Vec3::new(tip_f[0], tip_f[1], tip_f[2]);
|
||||
// Reuse the dim/leader arrow emitter so MultiLeader's arrow
|
||||
// matches the block referenced by arrowhead_handle.
|
||||
let mut arrow_geom = DimGeom::new();
|
||||
append_arrow(&mut arrow_geom, tip_v, dir, &arrow_kind);
|
||||
if !arrow_geom.dim_lines.is_empty() {
|
||||
points.push(nan);
|
||||
points.extend(arrow_geom.dim_lines);
|
||||
}
|
||||
arrow_fill.extend(arrow_geom.arrow_fill);
|
||||
}
|
||||
}
|
||||
|
||||
if ml.enable_landing && ml.enable_dogleg && ml.dogleg_length > 0.0 {
|
||||
let dir = &root.direction;
|
||||
let dl = (dir.x * dir.x + dir.y * dir.y).sqrt().max(1e-9);
|
||||
let d = ml.dogleg_length * effective_scale as f64;
|
||||
let landing_end = [
|
||||
(cp.x + dir.x / dl * d - ox) as f32,
|
||||
(cp.y + dir.y / dl * d - oy) as f32,
|
||||
cp_f[2],
|
||||
];
|
||||
points.push(nan);
|
||||
points.push(cp_f);
|
||||
points.push(landing_end);
|
||||
// extend_leader_to_text: continue past the landing to the text
|
||||
// insertion point along the same direction.
|
||||
if ml.extend_leader_to_text
|
||||
&& ml.content_type == LeaderContentType::MText
|
||||
&& !ml.context.text_string.is_empty()
|
||||
{
|
||||
let tx = (ml.context.text_location.x - ox) as f32;
|
||||
let ty = (ml.context.text_location.y - oy) as f32;
|
||||
points.push(landing_end);
|
||||
points.push([tx, ty, cp_f[2]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The leader/arrow/dogleg wire goes out as a single WireModel. Text, frame,
|
||||
// and background fill (each with their own color) are appended as separate
|
||||
// WireModels so the renderer respects per-piece coloring.
|
||||
let mut wires: Vec<WireModel> = Vec::new();
|
||||
wires.push(WireModel {
|
||||
name: name.clone(),
|
||||
points,
|
||||
color: line_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: leader_pat_len,
|
||||
pattern: leader_pat,
|
||||
line_weight_px: leader_lw_px,
|
||||
snap_pts,
|
||||
tangent_geoms: tangents,
|
||||
key_vertices: key_verts,
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: arrow_fill,
|
||||
});
|
||||
|
||||
// ── Block content ───────────────────────────────────────────────────────
|
||||
// When content_type == Block, the MultiLeader displays a block reference
|
||||
// at block_content_location with the recorded rotation/scale. Synthesize
|
||||
// an Insert and explode it through the standard tessellator. The block
|
||||
// resolves via block_content_handle (handle → block_record name).
|
||||
if ml.content_type == LeaderContentType::Block && ml.context.has_block_contents {
|
||||
let block_name = match ml.block_content_handle {
|
||||
Some(h) if !h.is_null() => document
|
||||
.block_records
|
||||
.iter()
|
||||
.find(|br| br.handle == h)
|
||||
.map(|br| br.name.clone()),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(block_name) = block_name {
|
||||
let block_color = if selected {
|
||||
line_color
|
||||
} else {
|
||||
color_or_inherit(&ml.block_content_color, entity_color)
|
||||
};
|
||||
let mut synth_ins =
|
||||
acadrust::entities::Insert::new(block_name, ml.context.block_content_location);
|
||||
synth_ins.set_x_scale(ml.block_scale.x);
|
||||
synth_ins.set_y_scale(ml.block_scale.y);
|
||||
synth_ins.set_z_scale(ml.block_scale.z);
|
||||
synth_ins.rotation = ml.block_rotation;
|
||||
synth_ins.common.layer = ml.common.layer.clone();
|
||||
// block_connection_type (BlockExtents vs BasePoint) chooses the
|
||||
// anchor when *creating* the multileader; at render time the
|
||||
// file's stored leader endpoints already encode that choice.
|
||||
let _ = ml.block_connection_type;
|
||||
for sub in synth_ins.explode_from_document(document) {
|
||||
let normalized =
|
||||
crate::modules::home::modify::explode::normalize_insert_entity(sub);
|
||||
let mut sub_wires = tessellate(
|
||||
document,
|
||||
handle,
|
||||
&normalized,
|
||||
selected,
|
||||
block_color,
|
||||
leader_pat_len,
|
||||
leader_pat,
|
||||
leader_lw_px,
|
||||
world_offset,
|
||||
1.0,
|
||||
);
|
||||
for w in &mut sub_wires {
|
||||
w.name = name.clone();
|
||||
}
|
||||
wires.extend(sub_wires);
|
||||
}
|
||||
// Block attributes attached to the multileader — render each as
|
||||
// its own attribute entity at WCS location like INSERT does.
|
||||
for ba in &ml.block_attributes {
|
||||
let _ = ba; // BlockAttribute carries only the value override
|
||||
// string; we'd need the AttributeDefinition handle
|
||||
// to materialise it as ATTRIB geometry. Skipped
|
||||
// until that wiring exists.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Text strokes / frame / background fill ──────────────────────────────
|
||||
// Strip inline format codes, split / word-wrap into lines, then place each
|
||||
// line according to text_attachment_point (horizontal) and
|
||||
// text_left_attachment (vertical), with text_rotation/text_direction applied.
|
||||
if ml.content_type == LeaderContentType::MText && !ml.context.text_string.is_empty() {
|
||||
let ctx = &ml.context;
|
||||
// `ctx.text_height` (when > 0) is the already-resolved WCS text height
|
||||
// stored in the per-instance context — style × scale_factor × anno
|
||||
// scale are all already baked in. Multiplying by `effective_scale`
|
||||
// again would double-scale (e.g., a context height of 100 in a file
|
||||
// with scale_factor=20 was rendering at 2000 units — 20× too big).
|
||||
// Only the fallback path (file omits the context value) needs
|
||||
// scale_factor + annotation scale applied.
|
||||
let height = if ctx.text_height > 0.0 {
|
||||
ctx.text_height as f32
|
||||
} else {
|
||||
ml.text_height as f32 * effective_scale
|
||||
};
|
||||
|
||||
let ins = &ctx.text_location;
|
||||
// Subtract world_offset in f64 before casting to f32: drawings often
|
||||
// sit at large absolute coordinates and casting first then subtracting
|
||||
// throws away the precision needed for the rotated sub-glyph offsets.
|
||||
let local_ins_x = (ins.x - ox) as f32;
|
||||
let local_ins_y = (ins.y - oy) as f32;
|
||||
let z = (ins.z - oz) as f32;
|
||||
|
||||
// Rotation: prefer text_direction (transforms survive rotations /
|
||||
// mirrors when acadrust updates it) and fall back to text_rotation.
|
||||
// ml.text_angle_type then constrains the final angle:
|
||||
// - Horizontal: always 0
|
||||
// - ParallelToLastLeaderLine: keep stored direction
|
||||
// - Optimized: clamp to (-π/2, π/2] (flip upside-down)
|
||||
// text_direction_negative finally adds π so reading goes the other way.
|
||||
let td = ctx.text_direction;
|
||||
let mut rot = if td.x.abs() > 1e-9 || td.y.abs() > 1e-9 {
|
||||
(td.y as f32).atan2(td.x as f32)
|
||||
} else {
|
||||
ctx.text_rotation as f32
|
||||
};
|
||||
match ml.text_angle_type {
|
||||
acadrust::entities::multileader::TextAngleType::Horizontal => rot = 0.0,
|
||||
acadrust::entities::multileader::TextAngleType::Optimized => {
|
||||
let pi = std::f32::consts::PI;
|
||||
if rot > pi / 2.0 {
|
||||
rot -= pi;
|
||||
} else if rot <= -pi / 2.0 {
|
||||
rot += pi;
|
||||
}
|
||||
}
|
||||
acadrust::entities::multileader::TextAngleType::ParallelToLastLeaderLine => {}
|
||||
}
|
||||
if ml.text_direction_negative {
|
||||
rot += std::f32::consts::PI;
|
||||
}
|
||||
|
||||
// Resolve text style via handle when available, falling back to STANDARD.
|
||||
let style_name = ctx
|
||||
.text_style_handle
|
||||
.as_ref()
|
||||
.and_then(|h| {
|
||||
document
|
||||
.text_styles
|
||||
.iter()
|
||||
.find(|s| s.handle == *h)
|
||||
.map(|s| s.name.clone())
|
||||
})
|
||||
.unwrap_or_else(|| "STANDARD".to_string());
|
||||
let style = resolve_text_style(&style_name, document);
|
||||
let mut rot = rot;
|
||||
if style.is_upside_down {
|
||||
rot += std::f32::consts::PI;
|
||||
}
|
||||
let (cos_r, sin_r) = (rot.cos(), rot.sin());
|
||||
|
||||
// ml.text_alignment overrides the per-context attachment_point when
|
||||
// not Left (which is the default zero). Otherwise we honour the
|
||||
// context's stored attachment_point (Left/Center/Right).
|
||||
use acadrust::entities::multileader::{TextAlignmentType, TextAttachmentDirectionType};
|
||||
let h_anchor = match (ml.text_alignment, ctx.text_attachment_point) {
|
||||
(TextAlignmentType::Center, _) => 0.5_f32,
|
||||
(TextAlignmentType::Right, _) => 1.0,
|
||||
(TextAlignmentType::Left, TextAttachmentPointType::Center) => 0.5,
|
||||
(TextAlignmentType::Left, TextAttachmentPointType::Right) => 1.0,
|
||||
_ => 0.0,
|
||||
};
|
||||
// Pick the vertical-anchor attachment based on text_attachment_direction:
|
||||
// Horizontal — leader attaches left/right; use ml.text_left_attachment
|
||||
// (matches the file's stored ctx.text_left_attachment).
|
||||
// Vertical — leader attaches top/bottom; use ml.text_top_attachment
|
||||
// or ml.text_bottom_attachment depending on which side
|
||||
// the leader is coming from (chosen via the first
|
||||
// root.direction.y sign).
|
||||
let leader_from_top = ml
|
||||
.context
|
||||
.leader_roots
|
||||
.first()
|
||||
.map(|r| r.direction.y < 0.0)
|
||||
.unwrap_or(true);
|
||||
let vertical_attach = match ml.text_attachment_direction {
|
||||
TextAttachmentDirectionType::Vertical => {
|
||||
if leader_from_top {
|
||||
ml.text_top_attachment
|
||||
} else {
|
||||
ml.text_bottom_attachment
|
||||
}
|
||||
}
|
||||
TextAttachmentDirectionType::Horizontal => ctx.text_left_attachment,
|
||||
};
|
||||
let v_anchor = mleader_v_anchor(vertical_attach);
|
||||
|
||||
// Shared MText pipeline — every inline format code (`\f`, `\C`/`\c`,
|
||||
// `\H`, `\W`, `\Q`, `\T`, `\A`, `\p…`, decorations, stacked
|
||||
// fractions, …) reaches the stroke output. Stroke origins are
|
||||
// already in offset-relative space because we pass local_ins_x/y.
|
||||
let layout = layout_mtext(&MTextRenderOpts {
|
||||
value: &ctx.text_string,
|
||||
insertion: [local_ins_x as f64, local_ins_y as f64, z as f64],
|
||||
height,
|
||||
rect_w: ctx.text_width as f32,
|
||||
rotation: rot,
|
||||
style: &style,
|
||||
attach_h_anchor: h_anchor,
|
||||
v_anchor,
|
||||
line_spacing_factor: ctx.line_spacing_factor as f32,
|
||||
vertical_text: false,
|
||||
});
|
||||
let line_widths = &layout.line_widths;
|
||||
let max_line_w = line_widths.iter().cloned().fold(0.0_f32, f32::max);
|
||||
let line_h = layout.line_height;
|
||||
let v_offset = layout.v_offset;
|
||||
let n_lines = layout.line_count.max(1) as f32;
|
||||
|
||||
// Resolve text color (falls back to entity color for ByLayer / ByBlock).
|
||||
let text_color = if selected {
|
||||
line_color
|
||||
} else {
|
||||
color_or_inherit(&ctx.text_color, entity_color)
|
||||
};
|
||||
|
||||
// Same LOD ladder used for top-level Text / MText (see scene/mod.rs):
|
||||
// h_px < 1 → baseline line (skip glyphs)
|
||||
// 1 ≤ h < 5 → greeked rect in text color (skip glyphs)
|
||||
// h_px ≥ 5 → full per-glyph stroke tessellation
|
||||
let lod_h_px = world_per_pixel.map(|wpp| height / wpp);
|
||||
let lod_mode = match lod_h_px {
|
||||
Some(h) if h < 1.0 => 0,
|
||||
Some(h) if h < 5.0 => 1,
|
||||
_ => 2,
|
||||
};
|
||||
|
||||
// Helper: map a (local_x, local_y) in the text's pre-rotation frame
|
||||
// (origin at the insertion point) into WCS render space.
|
||||
let to_wcs = |lx: f32, ly: f32| -> [f32; 3] {
|
||||
[
|
||||
local_ins_x + lx * cos_r - ly * sin_r,
|
||||
local_ins_y + lx * sin_r + ly * cos_r,
|
||||
z,
|
||||
]
|
||||
};
|
||||
|
||||
if lod_mode == 0 {
|
||||
// Baseline of the top line only.
|
||||
let line_w = line_widths.first().copied().unwrap_or(0.0);
|
||||
let len_px = world_per_pixel
|
||||
.map(|wpp| line_w / wpp)
|
||||
.unwrap_or(f32::INFINITY);
|
||||
if len_px >= 2.0 {
|
||||
let line_y_local = v_offset;
|
||||
let p0 = to_wcs(-line_w * h_anchor, line_y_local);
|
||||
let p1 = to_wcs(line_w * (1.0 - h_anchor), line_y_local);
|
||||
wires.push(WireModel {
|
||||
name: name.clone(),
|
||||
points: vec![p0, p1],
|
||||
color: text_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px,
|
||||
snap_pts: vec![(
|
||||
Vec3::new(local_ins_x, local_ins_y, z),
|
||||
SnapHint::Node,
|
||||
)],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
});
|
||||
}
|
||||
} else if lod_mode == 1 {
|
||||
// One filled rect per line — keeps the visual "text lives here
|
||||
// per row" hint that multi-line MText carries, in the text's
|
||||
// own color. Empty `points` opts out of the face3d 0.45 dim so
|
||||
// the fill renders at full intensity.
|
||||
let mut greek_tris: Vec<[f32; 3]> = Vec::with_capacity(line_widths.len() * 6);
|
||||
for (i, &line_w) in line_widths.iter().enumerate() {
|
||||
let li = i as f32;
|
||||
let line_y_bottom = -li * line_h + v_offset;
|
||||
let line_y_top = line_y_bottom + height;
|
||||
if line_w <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
let left = -line_w * h_anchor;
|
||||
let right = line_w * (1.0 - h_anchor);
|
||||
let bl = to_wcs(left, line_y_bottom);
|
||||
let br = to_wcs(right, line_y_bottom);
|
||||
let tr = to_wcs(right, line_y_top);
|
||||
let tl = to_wcs(left, line_y_top);
|
||||
greek_tris.extend_from_slice(&[bl, br, tr, bl, tr, tl]);
|
||||
}
|
||||
if !greek_tris.is_empty() {
|
||||
wires.push(WireModel {
|
||||
name: name.clone(),
|
||||
points: vec![],
|
||||
color: text_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px: 1.0,
|
||||
snap_pts: vec![(
|
||||
Vec3::new(local_ins_x, local_ins_y, z),
|
||||
SnapHint::Node,
|
||||
)],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: greek_tris,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Pre-tessellated by `layout_mtext`. Each TextStroke is in
|
||||
// local glyph space with its world origin (already offset-
|
||||
// relative because we passed local_ins_x/y) stored as f64.
|
||||
let mut text_points: Vec<[f32; 3]> = Vec::new();
|
||||
for ts in &layout.strokes {
|
||||
let ox = ts.origin[0] as f32;
|
||||
let oy = ts.origin[1] as f32;
|
||||
for stroke in &ts.strokes {
|
||||
if stroke.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
text_points.push(nan);
|
||||
for &[x, y] in stroke {
|
||||
text_points.push([x + ox, y + oy, z]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !text_points.is_empty() {
|
||||
wires.push(WireModel {
|
||||
name: name.clone(),
|
||||
points: text_points,
|
||||
color: text_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px,
|
||||
snap_pts: vec![(
|
||||
Vec3::new(local_ins_x, local_ins_y, z),
|
||||
SnapHint::Node,
|
||||
)],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Text frame / background-fill rectangle in local frame, then rotated to WCS.
|
||||
if ml.text_frame || ctx.background_fill_enabled {
|
||||
// Visual gap so the frame/fill doesn't touch glyph caps.
|
||||
let pad = height * 0.25;
|
||||
let block_top = v_offset + height + pad;
|
||||
let block_bottom = v_offset - (n_lines - 1.0) * line_h - pad;
|
||||
let block_left = -max_line_w * h_anchor - pad;
|
||||
let block_right = max_line_w * (1.0 - h_anchor) + pad;
|
||||
let local_corners: [[f32; 2]; 4] = [
|
||||
[block_left, block_bottom],
|
||||
[block_right, block_bottom],
|
||||
[block_right, block_top],
|
||||
[block_left, block_top],
|
||||
];
|
||||
let wcs_corners: [[f32; 3]; 4] = std::array::from_fn(|i| {
|
||||
let lx = local_corners[i][0];
|
||||
let ly = local_corners[i][1];
|
||||
let wx = local_ins_x + lx * cos_r - ly * sin_r;
|
||||
let wy = local_ins_y + lx * sin_r + ly * cos_r;
|
||||
[wx, wy, z]
|
||||
});
|
||||
|
||||
// Background fill — emit two triangles; renders under the text strokes.
|
||||
if ctx.background_fill_enabled {
|
||||
let fill_color = if selected {
|
||||
line_color
|
||||
} else {
|
||||
color_or_inherit(&ctx.background_fill_color, entity_color)
|
||||
};
|
||||
let fill_tris: Vec<[f32; 3]> = vec![
|
||||
wcs_corners[0],
|
||||
wcs_corners[1],
|
||||
wcs_corners[2],
|
||||
wcs_corners[0],
|
||||
wcs_corners[2],
|
||||
wcs_corners[3],
|
||||
];
|
||||
wires.push(WireModel {
|
||||
name: name.clone(),
|
||||
points: vec![],
|
||||
color: fill_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px: 1.0,
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris,
|
||||
});
|
||||
}
|
||||
|
||||
// Text frame — closed rectangle, matches text color.
|
||||
if ml.text_frame {
|
||||
let frame_points: Vec<[f32; 3]> = vec![
|
||||
wcs_corners[0],
|
||||
wcs_corners[1],
|
||||
wcs_corners[2],
|
||||
wcs_corners[3],
|
||||
wcs_corners[0],
|
||||
];
|
||||
wires.push(WireModel {
|
||||
name,
|
||||
points: frame_points,
|
||||
color: text_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px,
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wires
|
||||
}
|
||||
}
|
||||
@@ -201,3 +201,44 @@ impl Transformable for Ole2Frame {
|
||||
apply_transform(self, t);
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::entities::traits::LegacyTess for Ole2Frame {
|
||||
fn legacy_geometry(&self, world_offset: [f64; 3]) -> crate::scene::tess_util::LegacyGeometry {
|
||||
// OLE objects carry a bounding rectangle in model space.
|
||||
// Render a simple X-through-rectangle placeholder.
|
||||
let [ox, oy, oz] = world_offset;
|
||||
let x0 = (self.upper_left_corner.x - ox) as f32;
|
||||
let y0 = (self.lower_right_corner.y - oy) as f32;
|
||||
let x1 = (self.lower_right_corner.x - ox) as f32;
|
||||
let y1 = (self.upper_left_corner.y - oy) as f32;
|
||||
let z = (self.upper_left_corner.z - oz) as f32;
|
||||
if (x1 - x0).abs() < 1e-6 && (y1 - y0).abs() < 1e-6 {
|
||||
// Degenerate / unknown size — show a small cross.
|
||||
let s = 0.5_f32;
|
||||
return (
|
||||
vec![[-s, 0.0, 0.0], [s, 0.0, 0.0]],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
);
|
||||
}
|
||||
let pts = vec![
|
||||
// Outer rectangle
|
||||
[x0, y0, z],
|
||||
[x1, y0, z],
|
||||
[x1, y0, z],
|
||||
[x1, y1, z],
|
||||
[x1, y1, z],
|
||||
[x0, y1, z],
|
||||
[x0, y1, z],
|
||||
[x0, y0, z],
|
||||
// Diagonal X
|
||||
[x0, y0, z],
|
||||
[x1, y1, z],
|
||||
[f32::NAN, f32::NAN, f32::NAN],
|
||||
[x1, y0, z],
|
||||
[x0, y1, z],
|
||||
];
|
||||
(pts, vec![], vec![], vec![[x0, y0, z], [x1, y1, z]])
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,20 @@ use acadrust::{CadDocument, EntityType};
|
||||
use crate::command::EntityTransform;
|
||||
use crate::scene::acad_to_truck::TruckEntity;
|
||||
use crate::scene::object::{GripApply, GripDef, PropSection};
|
||||
use crate::scene::tess_util::LegacyGeometry;
|
||||
|
||||
pub trait TruckConvertible {
|
||||
fn to_truck(&self, document: &CadDocument) -> Option<TruckEntity>;
|
||||
}
|
||||
|
||||
/// Legacy fallback geometry for entities not covered by the truck
|
||||
/// topology pipeline (Viewport, Insert, Hatch outline, Ole2Frame, …).
|
||||
/// Returns world-offset-relative `f32` points + snap/key vertices the
|
||||
/// dispatcher wraps into a `WireModel`.
|
||||
pub trait LegacyTess {
|
||||
fn legacy_geometry(&self, world_offset: [f64; 3]) -> LegacyGeometry;
|
||||
}
|
||||
|
||||
pub trait Grippable {
|
||||
fn grips(&self) -> Vec<GripDef>;
|
||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply);
|
||||
|
||||
@@ -566,3 +566,22 @@ impl Transformable for Viewport {
|
||||
apply_transform(self, t);
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::entities::traits::LegacyTess for Viewport {
|
||||
fn legacy_geometry(&self, world_offset: [f64; 3]) -> crate::scene::tess_util::LegacyGeometry {
|
||||
let [ox, oy, oz] = world_offset;
|
||||
let cx = (self.center.x - ox) as f32;
|
||||
let cy = (self.center.y - oy) as f32;
|
||||
let cz = (self.center.z - oz) as f32;
|
||||
let hw = (self.width / 2.0) as f32;
|
||||
let hh = (self.height / 2.0) as f32;
|
||||
let pts = vec![
|
||||
[cx - hw, cy - hh, cz],
|
||||
[cx + hw, cy - hh, cz],
|
||||
[cx + hw, cy + hh, cz],
|
||||
[cx - hw, cy + hh, cz],
|
||||
[cx - hw, cy - hh, cz],
|
||||
];
|
||||
(pts, vec![], vec![], vec![])
|
||||
}
|
||||
}
|
||||
+26
-25
@@ -23,9 +23,10 @@ pub(super) struct EntityIndex {
|
||||
pub tree: quadtree::QuadTree,
|
||||
pub unbounded_handles: Vec<Handle>,
|
||||
}
|
||||
mod render;
|
||||
pub(crate) mod render;
|
||||
mod selection;
|
||||
pub mod solid3d_tess;
|
||||
pub mod tess_util;
|
||||
pub mod tessellate;
|
||||
pub mod transform;
|
||||
pub mod truck_tess;
|
||||
@@ -178,11 +179,11 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
|
||||
};
|
||||
let model = match e {
|
||||
EntityType::Hatch(dxf) => {
|
||||
let color = tessellate::aci_to_rgba(&dxf.common.color);
|
||||
let color = tess_util::aci_to_rgba(&dxf.common.color);
|
||||
Scene::hatch_model_from_dxf(dxf, color, offset)
|
||||
}
|
||||
EntityType::Solid(solid) => {
|
||||
let color = tessellate::aci_to_rgba(&solid.common.color);
|
||||
let color = tess_util::aci_to_rgba(&solid.common.color);
|
||||
Some(Scene::solid_hatch_model(solid, color, offset))
|
||||
}
|
||||
_ => None,
|
||||
@@ -211,7 +212,7 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
|
||||
.par_iter()
|
||||
.filter_map(|&handle| {
|
||||
let e = doc.get_entity(handle)?;
|
||||
let color = tessellate::aci_to_rgba(&e.common().color);
|
||||
let color = tess_util::aci_to_rgba(&e.common().color);
|
||||
let model = match e {
|
||||
EntityType::Solid3D(s) => solid3d_tess::tessellate_solid3d(s, color, facet_res),
|
||||
EntityType::Region(r) => solid3d_tess::tessellate_region(r, color, facet_res),
|
||||
@@ -2445,7 +2446,7 @@ impl Scene {
|
||||
let color = layer_entry
|
||||
.map(|l| &l.color)
|
||||
.unwrap_or(&acadrust::types::Color::WHITE);
|
||||
let [r, g, b, _] = crate::scene::tessellate::aci_to_rgba(color);
|
||||
let [r, g, b, _] = crate::scene::tess_util::aci_to_rgba(color);
|
||||
[r, g, b, 1.0]
|
||||
}
|
||||
|
||||
@@ -2888,10 +2889,10 @@ impl Scene {
|
||||
if sweep.abs() < 1e-9 {
|
||||
sweep = if bulge > 0.0 { TAU } else { -TAU };
|
||||
}
|
||||
let segs = tessellate::arc_segments(
|
||||
let segs = tess_util::arc_segments(
|
||||
r,
|
||||
sweep.abs(),
|
||||
tessellate::fill_chord_tol(r),
|
||||
tess_util::fill_chord_tol(r),
|
||||
);
|
||||
for j in 0..segs {
|
||||
let a = a0 + sweep * (j as f64 / segs as f64);
|
||||
@@ -2910,15 +2911,15 @@ impl Scene {
|
||||
boundary.push(to_xy(line.end.x, line.end.y));
|
||||
}
|
||||
BoundaryEdge::CircularArc(arc) => {
|
||||
let (sa, span) = tessellate::arc_signed_span(
|
||||
let (sa, span) = tess_util::arc_signed_span(
|
||||
arc.start_angle,
|
||||
arc.end_angle,
|
||||
arc.counter_clockwise,
|
||||
);
|
||||
let segs = tessellate::arc_segments(
|
||||
let segs = tess_util::arc_segments(
|
||||
arc.radius,
|
||||
span.abs(),
|
||||
tessellate::fill_chord_tol(arc.radius),
|
||||
tess_util::fill_chord_tol(arc.radius),
|
||||
);
|
||||
for i in 0..=segs {
|
||||
let t = sa + span * (i as f64 / segs as f64);
|
||||
@@ -2937,15 +2938,15 @@ impl Scene {
|
||||
.major_axis_endpoint
|
||||
.y
|
||||
.atan2(ell.major_axis_endpoint.x);
|
||||
let (sa, span) = tessellate::arc_signed_span(
|
||||
let (sa, span) = tess_util::arc_signed_span(
|
||||
ell.start_angle,
|
||||
ell.end_angle,
|
||||
ell.counter_clockwise,
|
||||
);
|
||||
let segs = tessellate::arc_segments(
|
||||
let segs = tess_util::arc_segments(
|
||||
r_maj,
|
||||
span.abs(),
|
||||
tessellate::fill_chord_tol(r_maj),
|
||||
tess_util::fill_chord_tol(r_maj),
|
||||
);
|
||||
let (cr, sr) = (rot.cos(), rot.sin());
|
||||
for i in 0..=segs {
|
||||
@@ -2999,7 +3000,7 @@ impl Scene {
|
||||
let diag = ((sp_max_x - sp_min_x).powi(2)
|
||||
+ (sp_max_y - sp_min_y).powi(2))
|
||||
.sqrt();
|
||||
let tol = tessellate::fill_chord_tol(diag.max(1.0));
|
||||
let tol = tess_util::fill_chord_tol(diag.max(1.0));
|
||||
|
||||
let mut sampled = false;
|
||||
if knot_ok {
|
||||
@@ -3211,11 +3212,11 @@ impl Scene {
|
||||
};
|
||||
let model = match &kind {
|
||||
EntityType::Hatch(dxf) => {
|
||||
let color = tessellate::aci_to_rgba(&dxf.common.color);
|
||||
let color = tess_util::aci_to_rgba(&dxf.common.color);
|
||||
Self::hatch_model_from_dxf(dxf, color, offset)
|
||||
}
|
||||
EntityType::Solid(solid) => {
|
||||
let color = tessellate::aci_to_rgba(&solid.common.color);
|
||||
let color = tess_util::aci_to_rgba(&solid.common.color);
|
||||
Some(Self::solid_hatch_model(solid, color, offset))
|
||||
}
|
||||
_ => None,
|
||||
@@ -3241,7 +3242,7 @@ impl Scene {
|
||||
.entities()
|
||||
.filter_map(|e| match e {
|
||||
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_) => {
|
||||
let color = tessellate::aci_to_rgba(&e.common().color);
|
||||
let color = tess_util::aci_to_rgba(&e.common().color);
|
||||
Some((e.common().handle, e.clone(), color))
|
||||
}
|
||||
_ => None,
|
||||
@@ -3644,7 +3645,7 @@ impl Scene {
|
||||
let h = self.document.add_entity(entity).unwrap_or(Handle::NULL);
|
||||
if !h.is_null() {
|
||||
let new_model = if let Some(EntityType::Hatch(dxf)) = self.document.get_entity(h) {
|
||||
let color = tessellate::aci_to_rgba(&dxf.common.color);
|
||||
let color = tess_util::aci_to_rgba(&dxf.common.color);
|
||||
Self::hatch_model_from_dxf(dxf, color, hatch_offset)
|
||||
} else {
|
||||
None
|
||||
@@ -3736,7 +3737,7 @@ impl Scene {
|
||||
};
|
||||
match self.document.get_entity(handle) {
|
||||
Some(EntityType::Hatch(dxf)) => {
|
||||
let color = tessellate::aci_to_rgba(&dxf.common.color);
|
||||
let color = tess_util::aci_to_rgba(&dxf.common.color);
|
||||
if let Some(model) = Self::hatch_model_from_dxf(dxf, color, hatch_offset) {
|
||||
self.hatches.insert(handle, model);
|
||||
} else {
|
||||
@@ -3744,7 +3745,7 @@ impl Scene {
|
||||
}
|
||||
}
|
||||
Some(EntityType::Solid(solid)) => {
|
||||
let color = tessellate::aci_to_rgba(&solid.common.color);
|
||||
let color = tess_util::aci_to_rgba(&solid.common.color);
|
||||
self.hatches
|
||||
.insert(handle, Self::solid_hatch_model(solid, color, hatch_offset));
|
||||
}
|
||||
@@ -4691,7 +4692,7 @@ fn clip_polyline_to_rect(
|
||||
/// so it picks up the standard text LOD ladder (baseline / greek / full),
|
||||
/// then re-color the returned wires with the dimension's resolved text colour
|
||||
/// (so DIMCLRT / DIMSTYLE colours win over the synthetic Text's defaults).
|
||||
pub(super) fn tessellate_entity_dim_text(
|
||||
pub(crate) fn tessellate_entity_dim_text(
|
||||
document: &acadrust::CadDocument,
|
||||
selected: &HashSet<Handle>,
|
||||
active_viewport: Option<Handle>,
|
||||
@@ -4899,10 +4900,10 @@ fn tessellate_entity(
|
||||
|
||||
if let EntityType::Dimension(dim) = e {
|
||||
let aabb = entity_aabb(e, world_offset);
|
||||
let mut wires = tessellate::tessellate_dimension(
|
||||
use crate::entities::dimension::DimensionTess;
|
||||
let mut wires = dim.tessellate(
|
||||
document,
|
||||
h,
|
||||
dim,
|
||||
sel,
|
||||
entity_color,
|
||||
line_weight_px,
|
||||
@@ -4923,10 +4924,10 @@ fn tessellate_entity(
|
||||
|
||||
if let EntityType::MultiLeader(ml) = e {
|
||||
let aabb = entity_aabb(e, world_offset);
|
||||
let mut wires = tessellate::tessellate_multileader(
|
||||
use crate::entities::multileader::MultiLeaderTess;
|
||||
let mut wires = ml.tessellate(
|
||||
document,
|
||||
h,
|
||||
ml,
|
||||
sel,
|
||||
entity_color,
|
||||
line_weight_px,
|
||||
|
||||
+3
-3
@@ -13,7 +13,7 @@ use std::sync::Arc;
|
||||
|
||||
use super::pipeline::viewcube::{hover_id, VIEWCUBE_PX};
|
||||
use super::pipeline::Pipeline;
|
||||
use super::tessellate;
|
||||
use super::tess_util;
|
||||
use super::{HatchModel, ImageModel, MeshLodSet, Scene, Uniforms, WireModel};
|
||||
|
||||
// ── PaperViewportPipeline / PaperViewportPrimitive ────────────────────────
|
||||
@@ -242,7 +242,7 @@ pub(super) fn render_style_for(
|
||||
AcadColor::Index(i) => *i,
|
||||
_ => 0,
|
||||
};
|
||||
let [r, g, b, _] = tessellate::aci_to_rgba(resolved);
|
||||
let [r, g, b, _] = tess_util::aci_to_rgba(resolved);
|
||||
let alpha = 1.0 - e.common().transparency.as_percent() as f32;
|
||||
([r, g, b, alpha], aci)
|
||||
};
|
||||
@@ -473,7 +473,7 @@ impl Scene {
|
||||
|
||||
// ── Linetype pattern helper ───────────────────────────────────────────────
|
||||
|
||||
pub(super) fn resolve_pattern(
|
||||
pub(crate) fn resolve_pattern(
|
||||
table: &acadrust::tables::Table<LineType>,
|
||||
name: &str,
|
||||
scale: f32,
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// Shared helpers used by per-entity tessellation impls in `crate::entities`
|
||||
// and the dispatcher in `crate::scene::tessellate`.
|
||||
//
|
||||
// Anything cross-entity (colour resolution, arc sampling, chord tolerance)
|
||||
// lives here. Entity-specific helpers (dim format strings, mleader path
|
||||
// types, etc.) stay with their entity file.
|
||||
|
||||
use acadrust::types::Color as AcadColor;
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::scene::wire_model::{SnapHint, TangentGeom, WireModel};
|
||||
|
||||
/// Output of the legacy per-entity geometry path used by entities not
|
||||
/// (yet) covered by the truck topology pipeline (Viewport, Insert,
|
||||
/// Hatch outline, Ole2Frame, …). Tuple form preserved to avoid
|
||||
/// touching every callsite when the dispatcher wraps these into a
|
||||
/// WireModel.
|
||||
///
|
||||
/// Layout: `(points, snap_pts, tangent_geoms, key_vertices)`.
|
||||
pub type LegacyGeometry = (
|
||||
Vec<[f32; 3]>,
|
||||
Vec<(Vec3, SnapHint)>,
|
||||
Vec<TangentGeom>,
|
||||
Vec<[f32; 3]>,
|
||||
);
|
||||
|
||||
// ── Arc tessellation helpers ─────────────────────────────────────────────
|
||||
|
||||
/// Convert hatch-boundary arc `(start, end, ccw)` into a
|
||||
/// `(start, signed_span)` ready for the sampling loop. Matches the
|
||||
/// legacy `(TAU - sa, TAU - ea)` flip used here for years — direction
|
||||
/// semantics are preserved on real files. (Wrap-through-2π is a known
|
||||
/// edge case in that convention; do not "fix" it without a wider audit
|
||||
/// of how upstream writers emit CW boundary arcs.)
|
||||
pub fn arc_signed_span(start: f64, end: f64, ccw: bool) -> (f64, f64) {
|
||||
const TAU: f64 = std::f64::consts::TAU;
|
||||
let (sa, ea) = if ccw { (start, end) } else { (TAU - start, TAU - end) };
|
||||
(sa, ea - sa)
|
||||
}
|
||||
|
||||
/// Segment count for an arc, targeting `chord_tol_world` chord-height
|
||||
/// error in world units. Floor 8, cap 512.
|
||||
///
|
||||
/// Two production callers:
|
||||
/// - hatch fill polygon (built at load / on edit) passes a radius-
|
||||
/// relative tol via [`fill_chord_tol`] — ~0.1% radius, zoom-free so
|
||||
/// the polygon stays sharp at extreme zoom-in without re-tessellation.
|
||||
/// - hatch wire outline (re-tessellated every frame inside the render
|
||||
/// scope) passes a zoom-adaptive tol via [`wire_chord_tol`] — pulls
|
||||
/// from the per-frame override set by `Scene::wires_for_block` so
|
||||
/// far-out arcs collapse to a handful of segments.
|
||||
pub fn arc_segments(radius: f64, span_abs: f64, chord_tol_world: f64) -> u32 {
|
||||
if span_abs < 1e-9 || radius < 1e-9 {
|
||||
return 1;
|
||||
}
|
||||
let tol = chord_tol_world.max(1e-9).min(radius * 0.99);
|
||||
// θ where r * (1 - cos(θ/2)) = tol → θ = 2 * acos(1 - tol/r).
|
||||
let max_step = (2.0 * (1.0 - tol / radius).acos()).max(1e-6);
|
||||
((span_abs / max_step).ceil() as u32).clamp(8, 512)
|
||||
}
|
||||
|
||||
/// Chord tolerance for the load-time fill polygon: 0.1% of radius,
|
||||
/// floor 1 µm so degenerate radii still produce a workable count.
|
||||
pub fn fill_chord_tol(radius: f64) -> f64 {
|
||||
(radius * 0.001).max(1e-6)
|
||||
}
|
||||
|
||||
/// Chord tolerance for the per-frame wire outline: pulls the active
|
||||
/// `truck_tess::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::truck_tess::active_curve_tol() {
|
||||
Some(t) => t.min(fill_chord_tol(radius)).max(1e-9),
|
||||
None => fill_chord_tol(radius),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Colour helper ──────────────────────────────────────────────────────────
|
||||
|
||||
/// Convert an acadrust Color (ACI index or true-color) to a GPU RGBA value.
|
||||
pub fn aci_to_rgba(color: &AcadColor) -> [f32; 4] {
|
||||
if let Some((r, g, b)) = color.rgb() {
|
||||
[r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0]
|
||||
} else {
|
||||
WireModel::WHITE
|
||||
}
|
||||
}
|
||||
+29
-2803
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user