2535 lines
102 KiB
Rust
2535 lines
102 KiB
Rust
// Tessellation — convert acadrust EntityType to GPU-ready WireModel or MeshModel.
|
|
//
|
|
// Flow:
|
|
// EntityType
|
|
// ↓ acad_to_render::convert()
|
|
// RenderEntity { object: RenderObject, snap_pts, tangent_geoms, key_vertices }
|
|
// ↓
|
|
// RenderObject::Lines → WireModel
|
|
// RenderObject::Dot → WireModel (a dot sized in pixels)
|
|
// RenderObject::Text → WireModel (glyph strokes) + SDF quads
|
|
// RenderObject::Text → one WireModel per glyph stroke (elevation from entity Z)
|
|
//
|
|
// Entities not handled by acad_to_render (Viewport, Insert, Hatch, Ole2Frame)
|
|
// are tessellated by the FallbackTess fallback_geometry() path.
|
|
|
|
use crate::entities::leader::LeaderTess;
|
|
use acadrust::types::Color as AcadColor;
|
|
use acadrust::{CadDocument, EntityType, Handle};
|
|
use glam::Vec3;
|
|
|
|
use crate::scene::convert::acad_to_render::{convert, RenderObject};
|
|
use crate::scene::model::wire_model::{SnapHint, WireModel};
|
|
|
|
/// Split an f64 offset-relative coordinate into the double-single (high, low)
|
|
/// f32 pair the renderer consumes. `high + low ≈ value` to ~f64 precision; the
|
|
/// RTE shader subtracts the eye's own high/low so vertices stay smooth even at
|
|
/// coordinates where a plain f32 cast would quantize to half a metre.
|
|
#[inline]
|
|
fn split_ds(v: f64) -> (f32, f32) {
|
|
let h = v as f32;
|
|
let l = (v - h as f64) as f32;
|
|
(h, l)
|
|
}
|
|
|
|
#[inline]
|
|
fn split_ds_xyz(x: f64, y: f64, z: f64) -> ([f32; 3], [f32; 3]) {
|
|
let (xh, xl) = split_ds(x);
|
|
let (yh, yl) = split_ds(y);
|
|
let (zh, zl) = split_ds(z);
|
|
([xh, yh, zh], [xl, yl, zl])
|
|
}
|
|
|
|
fn oriented_text_corners(
|
|
verts: &[crate::scene::pipeline::text_gpu::TextVertex],
|
|
origin: [f64; 2],
|
|
rotation: f64,
|
|
pad: f64,
|
|
) -> [[f64; 2]; 4] {
|
|
let (sin_r, cos_r) = rotation.sin_cos();
|
|
let mut bounds = [f64::MAX, f64::MAX, f64::MIN, f64::MIN];
|
|
for vertex in verts {
|
|
let x = vertex.pos[0] as f64 + vertex.pos_low[0] as f64 - origin[0];
|
|
let y = vertex.pos[1] as f64 + vertex.pos_low[1] as f64 - origin[1];
|
|
let local_x = x * cos_r + y * sin_r;
|
|
let local_y = -x * sin_r + y * cos_r;
|
|
bounds[0] = bounds[0].min(local_x);
|
|
bounds[1] = bounds[1].min(local_y);
|
|
bounds[2] = bounds[2].max(local_x);
|
|
bounds[3] = bounds[3].max(local_y);
|
|
}
|
|
let [left, bottom, right, top] = [
|
|
bounds[0] - pad,
|
|
bounds[1] - pad,
|
|
bounds[2] + pad,
|
|
bounds[3] + pad,
|
|
];
|
|
let to_world = |x: f64, y: f64| {
|
|
[
|
|
origin[0] + x * cos_r - y * sin_r,
|
|
origin[1] + x * sin_r + y * cos_r,
|
|
]
|
|
};
|
|
[
|
|
to_world(left, bottom),
|
|
to_world(right, bottom),
|
|
to_world(right, top),
|
|
to_world(left, top),
|
|
]
|
|
}
|
|
|
|
fn oriented_mtext_corner_groups(
|
|
verts: &[crate::scene::pipeline::text_gpu::TextVertex],
|
|
text: &acadrust::MText,
|
|
rotation: f64,
|
|
pad: f64,
|
|
annotation_scale: f64,
|
|
) -> Vec<[[f64; 2]; 4]> {
|
|
let columns = &text.column_data;
|
|
let count = columns.column_count.max(0) as usize;
|
|
if columns.column_type == 0 || count <= 1 || columns.width <= 0.0 {
|
|
return vec![oriented_text_corners(
|
|
verts,
|
|
[text.insertion_point.x, text.insertion_point.y],
|
|
rotation,
|
|
pad,
|
|
)];
|
|
}
|
|
|
|
let width = columns.width * annotation_scale;
|
|
let gutter = columns.gutter.max(0.0) * annotation_scale;
|
|
let total_width = width * count as f64 + gutter * count.saturating_sub(1) as f64;
|
|
let anchor = match text.attachment_point {
|
|
acadrust::entities::mtext::AttachmentPoint::TopCenter
|
|
| acadrust::entities::mtext::AttachmentPoint::MiddleCenter
|
|
| acadrust::entities::mtext::AttachmentPoint::BottomCenter => 0.5,
|
|
acadrust::entities::mtext::AttachmentPoint::TopRight
|
|
| acadrust::entities::mtext::AttachmentPoint::MiddleRight
|
|
| acadrust::entities::mtext::AttachmentPoint::BottomRight => 1.0,
|
|
_ => 0.0,
|
|
};
|
|
let block_left = -anchor * total_width;
|
|
let origin = [text.insertion_point.x, text.insertion_point.y];
|
|
let (sin_r, cos_r) = rotation.sin_cos();
|
|
let mut bounds = vec![[f64::MAX, f64::MAX, f64::MIN, f64::MIN]; count];
|
|
for vertex in verts {
|
|
let x = vertex.pos[0] as f64 + vertex.pos_low[0] as f64 - origin[0];
|
|
let y = vertex.pos[1] as f64 + vertex.pos_low[1] as f64 - origin[1];
|
|
let local_x = x * cos_r + y * sin_r;
|
|
let local_y = -x * sin_r + y * cos_r;
|
|
let stride = width + gutter;
|
|
let physical = ((local_x - block_left) / stride)
|
|
.floor()
|
|
.clamp(0.0, count.saturating_sub(1) as f64) as usize;
|
|
bounds[physical][0] = bounds[physical][0].min(local_x);
|
|
bounds[physical][1] = bounds[physical][1].min(local_y);
|
|
bounds[physical][2] = bounds[physical][2].max(local_x);
|
|
bounds[physical][3] = bounds[physical][3].max(local_y);
|
|
}
|
|
let to_world = |x: f64, y: f64| {
|
|
[
|
|
origin[0] + x * cos_r - y * sin_r,
|
|
origin[1] + x * sin_r + y * cos_r,
|
|
]
|
|
};
|
|
bounds
|
|
.into_iter()
|
|
.filter(|bounds| bounds[0] <= bounds[2] && bounds[1] <= bounds[3])
|
|
.map(|bounds| {
|
|
let [left, bottom, right, top] = [
|
|
bounds[0] - pad,
|
|
bounds[1] - pad,
|
|
bounds[2] + pad,
|
|
bounds[3] + pad,
|
|
];
|
|
[
|
|
to_world(left, bottom),
|
|
to_world(right, bottom),
|
|
to_world(right, top),
|
|
to_world(left, top),
|
|
]
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) fn explicit_mtext_background(entity: &EntityType) -> Option<[f32; 4]> {
|
|
let EntityType::MText(text) = entity else {
|
|
return None;
|
|
};
|
|
if text.background_fill_flags & 0x01 == 0 || text.background_fill_flags & 0x02 != 0 {
|
|
return None;
|
|
}
|
|
text.background_color.rgb().map(|(r, g, b)| {
|
|
[
|
|
r as f32 / 255.0,
|
|
g as f32 / 255.0,
|
|
b as f32 / 255.0,
|
|
1.0,
|
|
]
|
|
})
|
|
}
|
|
|
|
pub(crate) fn text_contrast_background(
|
|
entity: &EntityType,
|
|
canvas: [f32; 4],
|
|
) -> [f32; 4] {
|
|
explicit_mtext_background(entity).unwrap_or(canvas)
|
|
}
|
|
|
|
/// Split each absolute f64 source point into double-single (high, low) f32
|
|
/// buffers in one pass — the relative-to-eye residual the GPU/CPU reconstruct
|
|
/// to f64 precision at UTM-scale coordinates.
|
|
pub(crate) fn points_to_ds(
|
|
src: impl IntoIterator<Item = [f64; 3]>,
|
|
) -> (Vec<[f32; 3]>, Vec<[f32; 3]>) {
|
|
let it = src.into_iter();
|
|
let (lo, hi) = it.size_hint();
|
|
let cap = hi.unwrap_or(lo);
|
|
let mut high = Vec::with_capacity(cap);
|
|
let mut low = Vec::with_capacity(cap);
|
|
for [x, y, z] in it {
|
|
if x.is_nan() {
|
|
// Wire-model NaN-separator: keep both buffers index-paired.
|
|
high.push([f32::NAN; 3]);
|
|
low.push([0.0; 3]);
|
|
continue;
|
|
}
|
|
let (h, l) = split_ds_xyz(x, y, z);
|
|
high.push(h);
|
|
low.push(l);
|
|
}
|
|
(high, low)
|
|
}
|
|
|
|
fn point_cloud_wires(
|
|
document: &CadDocument,
|
|
handle: Handle,
|
|
entity: &EntityType,
|
|
selected: bool,
|
|
color: [f32; 4],
|
|
line_weight_px: f32,
|
|
) -> Option<Vec<WireModel>> {
|
|
let EntityType::Extended(extended) = entity else {
|
|
return None;
|
|
};
|
|
let frame_points = crate::entities::extended::point_cloud_frame_lines(extended)?;
|
|
let rendered = convert(entity, document)?;
|
|
let RenderObject::Lines(body_points) = rendered.object else {
|
|
return None;
|
|
};
|
|
let (points, points_low) = points_to_ds(body_points);
|
|
let mut wires = vec![WireModel {
|
|
point_marker: None,
|
|
taper_widths: Vec::new(),
|
|
pattern_stations: Vec::new(),
|
|
world_width: 0.0,
|
|
depth_override: None,
|
|
display_visible: true,
|
|
plot_visible: true,
|
|
fill_is_3d: false,
|
|
fill_is_2d_solid: false,
|
|
render_instance: None,
|
|
pick_tris: Vec::new(),
|
|
pick_tris_low: Vec::new(),
|
|
dash_from_start: false,
|
|
dash_align_end: None,
|
|
text_verts: Vec::new(),
|
|
name: handle.value().to_string(),
|
|
points,
|
|
points_low,
|
|
color,
|
|
selected,
|
|
pattern_length: 0.0,
|
|
pattern: [0.0; 8],
|
|
line_weight_px,
|
|
snap_pts: rendered.snap_pts,
|
|
tangent_geoms: rendered.tangent_geoms,
|
|
aci: 0,
|
|
key_vertices: rendered.key_vertices,
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
plinegen: true,
|
|
fill_tris: Vec::new(),
|
|
fill_tris_low: Vec::new(),
|
|
}];
|
|
let mode = crate::scene::frame::mode(
|
|
document,
|
|
crate::scene::frame::FrameKind::PointCloudClip,
|
|
);
|
|
if !frame_points.is_empty() {
|
|
let (points, points_low) = points_to_ds(frame_points);
|
|
let mut frame = WireModel::solid(
|
|
handle.value().to_string(),
|
|
points,
|
|
color,
|
|
selected,
|
|
);
|
|
frame.points_low = points_low;
|
|
frame.line_weight_px = line_weight_px;
|
|
frame.display_visible = mode != 0;
|
|
frame.plot_visible = mode == 1;
|
|
wires.push(frame);
|
|
}
|
|
Some(wires)
|
|
}
|
|
|
|
/// Lift a WireModel built in a local frame (a fixed f64 origin subtracted) back
|
|
/// to absolute world coordinates, re-splitting every position — polyline points
|
|
/// and SDF glyph vertices — into double-single so it stays precise at UTM scale.
|
|
/// The MLINE complex-linetype path uses this because `apply_along` lays out its
|
|
/// dashes and glyphs in f32, which would otherwise quantise fine spacing far
|
|
/// from the origin.
|
|
pub(crate) fn shift_wire_to_world(w: &mut WireModel, origin: [f64; 3]) {
|
|
if !w.points.is_empty() {
|
|
let mut hi = Vec::with_capacity(w.points.len());
|
|
let mut lo = Vec::with_capacity(w.points.len());
|
|
for p in &w.points {
|
|
if p[0].is_nan() {
|
|
hi.push([f32::NAN; 3]);
|
|
lo.push([0.0; 3]);
|
|
continue;
|
|
}
|
|
let (h, l) = split_ds_xyz(
|
|
p[0] as f64 + origin[0],
|
|
p[1] as f64 + origin[1],
|
|
p[2] as f64 + origin[2],
|
|
);
|
|
hi.push(h);
|
|
lo.push(l);
|
|
}
|
|
w.points = hi;
|
|
w.points_low = lo;
|
|
}
|
|
for tv in &mut w.text_verts {
|
|
let (h, l) = split_ds_xyz(
|
|
tv.pos[0] as f64 + tv.pos_low[0] as f64 + origin[0],
|
|
tv.pos[1] as f64 + tv.pos_low[1] as f64 + origin[1],
|
|
tv.pos[2] as f64 + tv.pos_low[2] as f64 + origin[2],
|
|
);
|
|
tv.pos = h;
|
|
tv.pos_low = l;
|
|
}
|
|
if w.aabb != WireModel::UNBOUNDED_AABB {
|
|
w.aabb = [
|
|
w.aabb[0] + origin[0] as f32,
|
|
w.aabb[1] + origin[1] as f32,
|
|
w.aabb[2] + origin[0] as f32,
|
|
w.aabb[3] + origin[1] as f32,
|
|
];
|
|
}
|
|
}
|
|
|
|
// ── Public entry points ────────────────────────────────────────────────────
|
|
|
|
/// Tessellate one entity into a WireModel.
|
|
/// For Text/MText entities this produces one WireModel with all glyph strokes
|
|
/// encoded as NaN-separated segments (wire_gpu skips NaN pairs).
|
|
/// For Solid3D entities this returns an empty wire; mesh tessellation lives
|
|
/// in `solid3d_tess` and is uploaded via the mesh pipeline instead.
|
|
pub fn tessellate(
|
|
document: &CadDocument,
|
|
handle: Handle,
|
|
entity: &EntityType,
|
|
selected: bool,
|
|
entity_color: [f32; 4],
|
|
pattern_length: f32,
|
|
pattern: [f32; 8],
|
|
line_weight_px: f32,
|
|
anno_scale: f32,
|
|
annotation_scale_handle: Option<Handle>,
|
|
world_per_pixel: Option<f32>,
|
|
// Canvas background colour — used for the MTEXT background *mask* fill
|
|
// (flag 0x02, "use drawing window colour") so the mask erases geometry
|
|
// behind the text the way a wipeout does.
|
|
bg_color: [f32; 4],
|
|
// When true, TEXT/MTEXT run-groups ALSO emit their glyph outline strokes as
|
|
// polyline points (not just SDF quads). The in-app MTEXT editor preview
|
|
// draws those strokes on a 2D canvas that can't run the SDF shader; every
|
|
// other caller passes false and gets the normal SDF-only text.
|
|
force_text_strokes: bool,
|
|
) -> Vec<WireModel> {
|
|
let color = if selected {
|
|
WireModel::SELECTED
|
|
} else {
|
|
entity_color
|
|
};
|
|
let name = handle.value().to_string();
|
|
|
|
// Determine the effective annotation scale for this entity.
|
|
//
|
|
// Only annotative entities are auto-scaled by the current annotation scale;
|
|
// everything else is manually pre-scaled (old convention with $DIMSCALE and
|
|
// oversized text). Annotative-ness is resolved centrally from the entity's
|
|
// per-object context, legacy XDATA, or annotative style (see
|
|
// `scene::annotative::is_annotative`) so the bake and the panel agree.
|
|
let anno_scale = crate::scene::annotative::effective_annotation_scale_for(
|
|
document,
|
|
entity,
|
|
anno_scale,
|
|
annotation_scale_handle,
|
|
);
|
|
|
|
// A HATCH is drawn as a fill by the hatch pipeline and highlighted via a
|
|
// fill tint when selected (issue #71), so it carries no boundary outline in
|
|
// the wire set. Skipping it here drops the dense boundary polyline — the
|
|
// dominant wire-instance cost on hatch-heavy drawings (issue #131). Picking
|
|
// is unaffected: hatches are caught by their fill area through the existing
|
|
// `pick::hit_test::click_hit_hatch` path, not this outline.
|
|
if matches!(entity, EntityType::Hatch(_)) {
|
|
return vec![];
|
|
}
|
|
|
|
// MultiLeader is handled by scene/mod.rs since it emits multiple WireModels
|
|
// (leader, text, frame, fill) with distinct colors.
|
|
if let EntityType::Leader(leader) = entity {
|
|
return vec![leader.tessellate(
|
|
document,
|
|
handle,
|
|
selected,
|
|
entity_color,
|
|
line_weight_px,
|
|
anno_scale,
|
|
)];
|
|
}
|
|
|
|
// MLINE emits one WireModel per style element so each parallel line keeps
|
|
// its own colour and linetype — a red Continuous line under a yellow dashed
|
|
// line reads as the two-tone multiline the style defines. Handled here, like
|
|
// Leader, because the single-colour the kernel `Lines` path can't carry
|
|
// per-element colour.
|
|
if let EntityType::MLine(m) = entity {
|
|
let lines = crate::entities::mline::mline_lines(m, document);
|
|
if lines.is_empty() {
|
|
return vec![];
|
|
}
|
|
let lt_scale =
|
|
document.header.linetype_scale as f32 * m.common.linetype_scale as f32;
|
|
let snap_pts: Vec<(glam::DVec3, SnapHint)> = m
|
|
.vertices
|
|
.iter()
|
|
.map(|v| {
|
|
(
|
|
glam::DVec3::new(v.position.x, v.position.y, v.position.z),
|
|
SnapHint::Node,
|
|
)
|
|
})
|
|
.collect();
|
|
let key_vertices: Vec<[f64; 3]> = m
|
|
.vertices
|
|
.iter()
|
|
.map(|v| [v.position.x, v.position.y, v.position.z])
|
|
.collect();
|
|
|
|
// Local-frame origin (mline start) for the CPU-dashed / glyph-laid
|
|
// elements. `apply_along` walks positions in f32, which quantises fine
|
|
// spacing — dash gaps AND inter-glyph advance — at UTM coordinates
|
|
// (the low half of the double-single is dropped). Subtracting this f64
|
|
// origin first keeps the walk near zero and precise; the result is
|
|
// shifted back to absolute double-single afterwards. Mirrors the
|
|
// Tolerance frame, which also builds geometry locally and applies its
|
|
// f64 origin later.
|
|
let origin = [
|
|
m.vertices[0].position.x,
|
|
m.vertices[0].position.y,
|
|
m.vertices[0].position.z,
|
|
];
|
|
// Centre-line (vertex path) length — the shared "A"-type reference so
|
|
// every parallel element uses the same end-dash length and thus the same
|
|
// interior phase (perpendicular dashes line up). f64 deltas so it stays
|
|
// precise at UTM coordinates.
|
|
let ref_total: f32 = {
|
|
let mut acc = 0.0_f64;
|
|
for w in m.vertices.windows(2) {
|
|
let dx = w[1].position.x - w[0].position.x;
|
|
let dy = w[1].position.y - w[0].position.y;
|
|
let dz = w[1].position.z - w[0].position.z;
|
|
acc += (dx * dx + dy * dy + dz * dz).sqrt();
|
|
}
|
|
if m.is_closed() && m.vertices.len() > 1 {
|
|
let first = &m.vertices[0].position;
|
|
let last = &m.vertices[m.vertices.len() - 1].position;
|
|
let dx = first.x - last.x;
|
|
let dy = first.y - last.y;
|
|
let dz = first.z - last.z;
|
|
acc += (dx * dx + dy * dy + dz * dz).sqrt();
|
|
}
|
|
acc as f32
|
|
};
|
|
let mut out: Vec<WireModel> = Vec::with_capacity(lines.len());
|
|
if let Some(style) = crate::entities::mline::resolved_mline_style(m, document) {
|
|
let triangles =
|
|
crate::entities::mline::mline_fill_triangles_with_style(m, style);
|
|
if !triangles.is_empty() {
|
|
let (fill_tris, fill_tris_low) = points_to_ds(triangles);
|
|
let fill_color = if selected {
|
|
WireModel::SELECTED
|
|
} else {
|
|
match style.fill_color {
|
|
AcadColor::ByLayer | AcadColor::ByBlock => entity_color,
|
|
other => {
|
|
let [r, g, b, _] =
|
|
crate::scene::convert::tess_util::aci_to_rgba(&other);
|
|
[r, g, b, entity_color[3]]
|
|
}
|
|
}
|
|
};
|
|
out.push(WireModel {
|
|
point_marker: None,
|
|
taper_widths: Vec::new(),
|
|
pattern_stations: Vec::new(),
|
|
world_width: 0.0,
|
|
depth_override: None,
|
|
display_visible: true,
|
|
plot_visible: true,
|
|
fill_is_3d: false,
|
|
fill_is_2d_solid: true,
|
|
render_instance: None,
|
|
pick_tris: Vec::new(),
|
|
pick_tris_low: Vec::new(),
|
|
dash_from_start: false,
|
|
dash_align_end: None,
|
|
text_verts: Vec::new(),
|
|
name: name.clone(),
|
|
points: Vec::new(),
|
|
points_low: Vec::new(),
|
|
color: fill_color,
|
|
selected,
|
|
pattern_length: 0.0,
|
|
pattern: [0.0; 8],
|
|
line_weight_px,
|
|
snap_pts: Vec::new(),
|
|
tangent_geoms: Vec::new(),
|
|
aci: 0,
|
|
key_vertices: Vec::new(),
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
plinegen: true,
|
|
fill_tris,
|
|
fill_tris_low,
|
|
});
|
|
}
|
|
}
|
|
let mut snap_attached = false;
|
|
for l in lines {
|
|
if l.points.is_empty() {
|
|
continue;
|
|
}
|
|
// Element colour: ByLayer / ByBlock inherit the entity's resolved
|
|
// colour; an explicit ACI / true-colour is used as-is.
|
|
let wcolor = if selected {
|
|
WireModel::SELECTED
|
|
} else {
|
|
match l.color {
|
|
AcadColor::ByLayer | AcadColor::ByBlock => entity_color,
|
|
other => {
|
|
let [r, g, b, _] =
|
|
crate::scene::convert::tess_util::aci_to_rgba(&other);
|
|
[r, g, b, entity_color[3]]
|
|
}
|
|
}
|
|
};
|
|
let aci = match l.color {
|
|
AcadColor::Index(i) => i,
|
|
_ => 0,
|
|
};
|
|
|
|
// Every dashed element is CPU-dashed by `apply_along` (not the GPU
|
|
// pattern) so all parallel lines walk the polyline with the *same*
|
|
// arithmetic and stay in phase — otherwise a shader-dashed line and an
|
|
// apply_along-dashed sibling drift apart at large (UTM) coordinates and
|
|
// one line's dash lands in the other's gap, striking through embedded
|
|
// text. Document definition wins over the bundled catalog; a
|
|
// continuous element yields `None` and falls to the solid path below.
|
|
// Selection forces a plain solid highlight, so skip dashing then.
|
|
let clt = if selected {
|
|
None
|
|
} else if let Some(doc_seg) =
|
|
crate::io::linetypes::document_lt_segments(document, &l.linetype)
|
|
{
|
|
// In-document linetype: CPU-expand (apply_along) only when it
|
|
// embeds TEXT glyphs that must be laid out along the curve. Pure
|
|
// dash / space / dot (and undrawn shape) elements fall through to
|
|
// the GPU dash shader below — cheaper (one WireModel + pattern
|
|
// instead of N CPU segments) and now UTM-precise, so they stay in
|
|
// phase with any glyph-bearing sibling element.
|
|
if doc_seg
|
|
.segments
|
|
.iter()
|
|
.any(|s| matches!(s, crate::io::linetypes::LtSegment::Text { .. }))
|
|
{
|
|
Some(doc_seg)
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
// Not in the document: bundled-catalog linetype (may embed
|
|
// text / shape) → keep the CPU path; `resolve_pattern` below can't
|
|
// see it, so GPU-dashing would drop the pattern to solid.
|
|
crate::io::linetypes::complex_lt(&l.linetype).cloned()
|
|
};
|
|
|
|
let mut elem_wires: Vec<WireModel> = if let Some(clt) = clt {
|
|
// Walk the dash / glyph layout in a local frame so the f32 math
|
|
// stays precise, then lift each wire back to world DS.
|
|
let local: Vec<[f32; 3]> = l
|
|
.points
|
|
.iter()
|
|
.map(|p| {
|
|
[
|
|
(p[0] - origin[0]) as f32,
|
|
(p[1] - origin[1]) as f32,
|
|
(p[2] - origin[2]) as f32,
|
|
]
|
|
})
|
|
.collect();
|
|
let mut w = crate::scene::text::complex_lt::apply_along(
|
|
&name,
|
|
&local,
|
|
&clt,
|
|
lt_scale.max(1e-4),
|
|
wcolor,
|
|
selected,
|
|
line_weight_px,
|
|
// Shared "A"-type reference: this text-bearing element aligns
|
|
// with the GPU-dashed sibling elements (same centre-line
|
|
// reference) instead of tiling independently from the start.
|
|
Some(ref_total),
|
|
);
|
|
for wm in &mut w {
|
|
wm.aci = aci;
|
|
shift_wire_to_world(wm, origin);
|
|
}
|
|
w
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
|
|
// Simple path: the linetype isn't complex, or `apply_along` bailed
|
|
// (pattern blow-up guard) and returned nothing — draw the element as a
|
|
// dashed / solid polyline so it is never lost.
|
|
if elem_wires.is_empty() {
|
|
let (pts, pts_low) = points_to_ds(l.points);
|
|
let (pattern_length, pattern) = if selected {
|
|
(0.0, [0.0; 8])
|
|
} else {
|
|
crate::scene::view::render::resolve_pattern(
|
|
&document.line_types,
|
|
&l.linetype,
|
|
lt_scale,
|
|
)
|
|
};
|
|
// Shared "A"-type: derive the begin/end solid-dash length ONCE
|
|
// from the multiline centre-line (`ref_total`) so every parallel
|
|
// element runs the same interior phase and its dashes line up
|
|
// perpendicular; `align_total` stays each element's own length in
|
|
// the shader so each still ends on a dash. Dash-first patterns
|
|
// only (`+dash, -gap, …`); shorter-than-a-period lines fall back
|
|
// to the per-wire path (solid).
|
|
let dash_align_end = if pattern_length > 1e-6
|
|
&& pattern[0] > 0.0
|
|
&& pattern[1] < 0.0
|
|
&& ref_total > pattern_length
|
|
{
|
|
let a = pattern[0];
|
|
let p = pattern_length;
|
|
let k = ((ref_total - a) / p).round().max(1.0);
|
|
Some(((ref_total - k * p + a) * 0.5).max(1e-4))
|
|
} else {
|
|
None
|
|
};
|
|
elem_wires.push(WireModel {
|
|
point_marker: None,
|
|
taper_widths: Vec::new(),
|
|
pattern_stations: Vec::new(),
|
|
world_width: 0.0,
|
|
depth_override: None,
|
|
display_visible: true,
|
|
plot_visible: true,
|
|
fill_is_3d: false,
|
|
fill_is_2d_solid: false,
|
|
render_instance: None,
|
|
pick_tris: Vec::new(),
|
|
pick_tris_low: Vec::new(),
|
|
// MLINE dashes: A-type aligned, but the end-dash length is
|
|
// shared across all parallel elements (`dash_align_end`) so
|
|
// their interiors stay in phase (perpendicular dashes line up)
|
|
// while each still ends on a dash at its own endpoint.
|
|
dash_from_start: false,
|
|
dash_align_end,
|
|
text_verts: Vec::new(),
|
|
name: name.clone(),
|
|
points: pts,
|
|
points_low: pts_low,
|
|
color: wcolor,
|
|
selected,
|
|
pattern_length,
|
|
pattern,
|
|
line_weight_px,
|
|
snap_pts: Vec::new(),
|
|
tangent_geoms: Vec::new(),
|
|
aci,
|
|
key_vertices: Vec::new(),
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
plinegen: false,
|
|
fill_tris: Vec::new(),
|
|
fill_tris_low: Vec::new(),
|
|
});
|
|
}
|
|
|
|
// Snap / key vertices ride the first emitted wire only (they describe
|
|
// the whole entity, not one element).
|
|
if !snap_attached {
|
|
if let Some(w0) = elem_wires.first_mut() {
|
|
w0.snap_pts = snap_pts.clone();
|
|
w0.key_vertices = key_vertices.clone();
|
|
snap_attached = true;
|
|
}
|
|
}
|
|
out.append(&mut elem_wires);
|
|
}
|
|
if out.is_empty() {
|
|
return vec![];
|
|
}
|
|
return out;
|
|
}
|
|
|
|
if let Some(wires) = point_cloud_wires(
|
|
document,
|
|
handle,
|
|
entity,
|
|
selected,
|
|
color,
|
|
line_weight_px,
|
|
) {
|
|
return wires;
|
|
}
|
|
|
|
// ── Try the kernel path first ───────────────────────────────────────────
|
|
// Relative-PDSIZE points size their glyph from the current zoom so they
|
|
// stay a roughly constant on-screen size; otherwise the header-driven path.
|
|
let te = crate::entities::point::relative_render(entity, document, world_per_pixel)
|
|
.or_else(|| crate::entities::light::relative_render(entity, document, world_per_pixel))
|
|
.or_else(|| convert(entity, document));
|
|
if let Some(te) = te {
|
|
match te.object {
|
|
// ── Text / MText: pre-tessellated glyph strokes ───────────────
|
|
//
|
|
// Strokes are pre-grouped by world origin (one TextStroke per
|
|
// line / per run / per fragment), each carrying an optional
|
|
// colour override produced by MTEXT inline `\C` / `\c`. We bin
|
|
// groups by override colour and emit one WireModel per bin so a
|
|
// single MTEXT can hand back N colour-distinct wires when the
|
|
// value mixes inline colours.
|
|
RenderObject::Text(stroke_groups) => {
|
|
let entity_zf = entity_z(entity) as f64;
|
|
let elev_v = entity_zf;
|
|
|
|
// Scale MTEXT around its attachment point.
|
|
let ref_origin = match entity {
|
|
EntityType::MText(m) => [
|
|
m.insertion_point.x,
|
|
m.insertion_point.y,
|
|
],
|
|
_ => stroke_groups
|
|
.first()
|
|
.map(|g| g.origin)
|
|
.unwrap_or([0.0, 0.0]),
|
|
};
|
|
|
|
let ref_lx_v = ref_origin[0];
|
|
let ref_ly_v = ref_origin[1];
|
|
|
|
// Selection forces a single uniform colour — never split.
|
|
let split_by_color = !selected;
|
|
|
|
// Bins: key = (Some(rgb), bold). Bold strokes bin separately so
|
|
// the editor preview can draw them with a wider pen.
|
|
struct TextBin {
|
|
color: Option<[f32; 3]>,
|
|
bold: bool,
|
|
pts: Vec<[f32; 3]>,
|
|
pts_low: Vec<[f32; 3]>,
|
|
fill_tris: Vec<[f32; 3]>,
|
|
fill_tris_low: Vec<[f32; 3]>,
|
|
}
|
|
let mut bins: Vec<TextBin> = Vec::new();
|
|
let mut bin_first: Vec<bool> = Vec::new();
|
|
let find_or_make = |key: Option<[f32; 3]>,
|
|
bold: bool,
|
|
bins: &mut Vec<TextBin>,
|
|
firsts: &mut Vec<bool>|
|
|
-> usize {
|
|
if let Some(i) = bins.iter().position(|b| b.color == key && b.bold == bold) {
|
|
i
|
|
} else {
|
|
bins.push(TextBin {
|
|
color: key,
|
|
bold,
|
|
pts: Vec::new(),
|
|
pts_low: Vec::new(),
|
|
fill_tris: Vec::new(),
|
|
fill_tris_low: Vec::new(),
|
|
});
|
|
firsts.push(true);
|
|
bins.len() - 1
|
|
}
|
|
};
|
|
|
|
let anno = anno_scale as f64;
|
|
// Run groups normally render as textured quads. Web runs that
|
|
// require bidi or joined-script shaping keep their already
|
|
// shaped vector geometry because the per-glyph SDF path has no
|
|
// cluster-position data.
|
|
for group in stroke_groups
|
|
.iter()
|
|
.filter(|group| {
|
|
force_text_strokes
|
|
|| group.run.is_none()
|
|
|| group.run.as_ref().is_some_and(|run| {
|
|
crate::scene::text::web_font::requires_shaping(&run.text)
|
|
})
|
|
})
|
|
{
|
|
let lx_v = group.origin[0];
|
|
let ly_v = group.origin[1];
|
|
let slx_v = (lx_v - ref_lx_v) * anno + ref_lx_v;
|
|
let sly_v = (ly_v - ref_ly_v) * anno + ref_ly_v;
|
|
let bin_key = if split_by_color { group.color } else { None };
|
|
let group_bold = group.run.as_ref().is_some_and(|r| r.bold);
|
|
let bi = find_or_make(bin_key, group_bold, &mut bins, &mut bin_first);
|
|
|
|
// 1. Process outline strokes
|
|
for stroke in &group.strokes {
|
|
if stroke.len() < 2 {
|
|
continue;
|
|
}
|
|
if !bin_first[bi] && !bins[bi].pts.is_empty() {
|
|
bins[bi].pts.push([f32::NAN, f32::NAN, f32::NAN]);
|
|
bins[bi].pts_low.push([0.0; 3]);
|
|
}
|
|
bin_first[bi] = false;
|
|
for &[x, y] in stroke {
|
|
let xv = x as f64 * anno + slx_v;
|
|
let yv = y as f64 * anno + sly_v;
|
|
let (h, l) = split_ds_xyz(xv, yv, elev_v);
|
|
bins[bi].pts.push(h);
|
|
bins[bi].pts_low.push(l);
|
|
}
|
|
}
|
|
|
|
// 2. Process fill triangles
|
|
for &[x, y] in &group.fill_tris {
|
|
let xv = x as f64 * anno + slx_v;
|
|
let yv = y as f64 * anno + sly_v;
|
|
let (h, l) = split_ds_xyz(xv, yv, elev_v);
|
|
bins[bi].fill_tris.push(h);
|
|
bins[bi].fill_tris_low.push(l);
|
|
}
|
|
}
|
|
|
|
// ── SDF glyph quads ──────────────────────────────────────
|
|
// Build each run's glyph quads here and carry them on the wire.
|
|
// They ride with the wire through the tess memo, hit-
|
|
// materialisation and (for block content) the block-expand
|
|
// transform — no separate document-wide collector.
|
|
let mut sdf_verts: Vec<crate::scene::pipeline::text_gpu::TextVertex> = Vec::new();
|
|
{
|
|
if let Ok(mut atlas) = crate::scene::text::sdf_atlas::text_atlas().lock() {
|
|
// Selection tints the whole run; otherwise inline `\C`
|
|
// colours (bin key) win, falling back to entity colour.
|
|
for group in &stroke_groups {
|
|
let Some(run) = &group.run else { continue };
|
|
if crate::scene::text::web_font::requires_shaping(&run.text) {
|
|
continue;
|
|
}
|
|
let slx_v = (group.origin[0] - ref_lx_v) * anno + ref_lx_v;
|
|
let sly_v = (group.origin[1] - ref_ly_v) * anno + ref_ly_v;
|
|
// Base colour only (inline `\C` wins). Selection /
|
|
// hover recolouring is done by the text-highlight
|
|
// overlay, so the base glyphs stay neutral — else a
|
|
// deselect would leave stale-tinted glyphs until the
|
|
// next geometry rebuild (the base text buffer only
|
|
// rebuilds on geometry, not on a pick).
|
|
let gcolor = group
|
|
.color
|
|
.map(|c| [c[0], c[1], c[2], entity_color[3]])
|
|
.unwrap_or(entity_color);
|
|
let gcolor = crate::scene::view::render::adapt_to_bg(
|
|
gcolor,
|
|
text_contrast_background(entity, bg_color),
|
|
);
|
|
let quads = crate::scene::text::glyph_quads::layout_glyph_quads(
|
|
&mut atlas,
|
|
run.height,
|
|
run.rotation,
|
|
run.width_factor,
|
|
run.oblique,
|
|
run.tracking,
|
|
&run.font,
|
|
run.bold,
|
|
&run.text,
|
|
);
|
|
crate::scene::pipeline::text_gpu::push_glyph_vertices(
|
|
&mut sdf_verts,
|
|
&quads,
|
|
[slx_v, sly_v, elev_v],
|
|
anno,
|
|
gcolor,
|
|
0.0,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
let snap_pts = te.snap_pts;
|
|
let key_vertices: Vec<[f64; 3]> = te
|
|
.key_vertices
|
|
.into_iter()
|
|
.map(|[x, y, z]| [x, y, z])
|
|
.collect();
|
|
|
|
// Derive the pick box from the rendered glyph quads.
|
|
let text_aabb = if !sdf_verts.is_empty() {
|
|
let (mut nx, mut ny, mut xx, mut xy) =
|
|
(f64::MAX, f64::MAX, f64::MIN, f64::MIN);
|
|
for v in &sdf_verts {
|
|
let x = v.pos[0] as f64 + v.pos_low[0] as f64;
|
|
let y = v.pos[1] as f64 + v.pos_low[1] as f64;
|
|
nx = nx.min(x);
|
|
xx = xx.max(x);
|
|
ny = ny.min(y);
|
|
xy = xy.max(y);
|
|
}
|
|
[nx as f32, ny as f32, xx as f32, xy as f32]
|
|
} else {
|
|
WireModel::UNBOUNDED_AABB
|
|
};
|
|
|
|
// Empty input (no glyphs) → emit a single empty wire so the
|
|
// entity still has a hit-test target via snap_pts. With SDF on
|
|
// this is the normal path (strokes suppressed) and the wire
|
|
// also carries the glyph quads built above.
|
|
if bins.is_empty() {
|
|
let mut wires: Vec<WireModel> = Vec::new();
|
|
// MTEXT background and frame follow the glyph bounds.
|
|
if text_aabb != WireModel::UNBOUNDED_AABB {
|
|
if let EntityType::MText(m) = entity {
|
|
let has_fill = m.background_fill_flags & 0x03 != 0;
|
|
let has_frame = m.background_fill_flags & 0x10 != 0;
|
|
if has_fill || has_frame {
|
|
let text_rotation = stroke_groups
|
|
.iter()
|
|
.find_map(|group| {
|
|
group.run.as_ref().map(|run| run.rotation as f64)
|
|
})
|
|
.unwrap_or(m.rotation);
|
|
let text_height = m.height * anno;
|
|
let pad = (m.background_scale - 1.0).max(0.0) * text_height;
|
|
let corner_groups = oriented_mtext_corner_groups(
|
|
&sdf_verts,
|
|
m,
|
|
text_rotation,
|
|
pad,
|
|
anno,
|
|
);
|
|
// Fill / mask — two triangles behind the glyphs.
|
|
if has_fill {
|
|
let fill_color = if m.background_fill_flags & 0x02 != 0 {
|
|
bg_color
|
|
} else {
|
|
color_or_inherit(&m.background_color, bg_color)
|
|
};
|
|
let mut ft = Vec::with_capacity(6 * corner_groups.len());
|
|
let mut ftl = Vec::with_capacity(6 * corner_groups.len());
|
|
for corners in &corner_groups {
|
|
for &k in &[0usize, 1, 2, 0, 2, 3] {
|
|
let (h, lo) = split_ds_xyz(
|
|
corners[k][0],
|
|
corners[k][1],
|
|
elev_v,
|
|
);
|
|
ft.push(h);
|
|
ftl.push(lo);
|
|
}
|
|
}
|
|
wires.push(WireModel {
|
|
point_marker: None,
|
|
taper_widths: Vec::new(),
|
|
pattern_stations: Vec::new(),
|
|
world_width: 0.0,
|
|
depth_override: None,
|
|
display_visible: true,
|
|
plot_visible: true,
|
|
fill_is_3d: false,
|
|
fill_is_2d_solid: false,
|
|
render_instance: None,
|
|
pick_tris: Vec::new(),
|
|
pick_tris_low: Vec::new(),
|
|
dash_from_start: false,
|
|
dash_align_end: None,
|
|
text_verts: Vec::new(),
|
|
name: name.clone(),
|
|
points: vec![],
|
|
points_low: Vec::new(),
|
|
color: fill_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,
|
|
fill_tris: ft,
|
|
fill_tris_low: ftl,
|
|
});
|
|
}
|
|
// Text frame — a closed rectangle in the text
|
|
// colour around the same box.
|
|
if has_frame {
|
|
let mut fp = Vec::with_capacity(6 * corner_groups.len());
|
|
let mut fpl = Vec::with_capacity(6 * corner_groups.len());
|
|
for (group_index, corners) in corner_groups.iter().enumerate() {
|
|
if group_index > 0 {
|
|
fp.push([f32::NAN; 3]);
|
|
fpl.push([0.0; 3]);
|
|
}
|
|
for &[x, y] in &[
|
|
corners[0],
|
|
corners[1],
|
|
corners[2],
|
|
corners[3],
|
|
corners[0],
|
|
] {
|
|
let (h, lo) = split_ds_xyz(x, y, elev_v);
|
|
fp.push(h);
|
|
fpl.push(lo);
|
|
}
|
|
}
|
|
wires.push(WireModel {
|
|
point_marker: None,
|
|
taper_widths: Vec::new(),
|
|
pattern_stations: Vec::new(),
|
|
world_width: 0.0,
|
|
depth_override: None,
|
|
display_visible: true,
|
|
plot_visible: true,
|
|
fill_is_3d: false,
|
|
fill_is_2d_solid: false,
|
|
render_instance: None,
|
|
pick_tris: Vec::new(),
|
|
pick_tris_low: Vec::new(),
|
|
dash_from_start: false,
|
|
dash_align_end: None,
|
|
text_verts: Vec::new(),
|
|
name: name.clone(),
|
|
points: fp,
|
|
points_low: fpl,
|
|
color: entity_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,
|
|
fill_tris: vec![],
|
|
fill_tris_low: Vec::new(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Debug (env OCS_TEXT_BOX): draw a rectangle around the glyph
|
|
// bounds as a separate outline wire so the text box is
|
|
// visible for testing. The empty text wire below is left
|
|
// untouched (still the SDF + pick target).
|
|
if !sdf_verts.is_empty()
|
|
&& crate::scene::text::sdf_atlas::text_box_debug()
|
|
{
|
|
let [nx, ny, xx, xy] = text_aabb;
|
|
let (nx, ny, xx, xy) = (nx as f64, ny as f64, xx as f64, xy as f64);
|
|
let mut pts = Vec::with_capacity(5);
|
|
let mut low = Vec::with_capacity(5);
|
|
for (x, y) in [(nx, ny), (xx, ny), (xx, xy), (nx, xy), (nx, ny)] {
|
|
let (hh, ll) = split_ds_xyz(x, y, elev_v);
|
|
pts.push(hh);
|
|
low.push(ll);
|
|
}
|
|
wires.push(WireModel {
|
|
point_marker: None,
|
|
taper_widths: Vec::new(),
|
|
pattern_stations: Vec::new(),
|
|
world_width: 0.0,
|
|
depth_override: None,
|
|
display_visible: true,
|
|
plot_visible: true,
|
|
fill_is_3d: false,
|
|
fill_is_2d_solid: false,
|
|
render_instance: None,
|
|
pick_tris: Vec::new(),
|
|
pick_tris_low: Vec::new(),
|
|
dash_from_start: false,
|
|
dash_align_end: None,
|
|
text_verts: Vec::new(),
|
|
name: name.clone(),
|
|
points: pts,
|
|
points_low: low,
|
|
color: [1.0, 0.0, 1.0, 1.0],
|
|
selected,
|
|
pattern_length: 0.0,
|
|
pattern: [0.0; 8],
|
|
line_weight_px,
|
|
snap_pts: Vec::new(),
|
|
tangent_geoms: Vec::new(),
|
|
aci: 0,
|
|
key_vertices: Vec::new(),
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
plinegen: true,
|
|
fill_tris: vec![],
|
|
fill_tris_low: Vec::new(),
|
|
});
|
|
}
|
|
wires.push(WireModel {
|
|
point_marker: None,
|
|
taper_widths: Vec::new(),
|
|
pattern_stations: Vec::new(),
|
|
world_width: 0.0,
|
|
depth_override: None,
|
|
display_visible: true,
|
|
plot_visible: true,
|
|
fill_is_3d: false,
|
|
fill_is_2d_solid: false,
|
|
render_instance: None,
|
|
pick_tris: Vec::new(),
|
|
pick_tris_low: Vec::new(),
|
|
dash_from_start: false,
|
|
dash_align_end: None,
|
|
text_verts: sdf_verts,
|
|
name,
|
|
points: Vec::new(),
|
|
points_low: Vec::new(),
|
|
color,
|
|
selected,
|
|
pattern_length: 0.0,
|
|
pattern: [0.0; 8],
|
|
line_weight_px,
|
|
snap_pts,
|
|
tangent_geoms: te.tangent_geoms,
|
|
aci: 0,
|
|
key_vertices,
|
|
aabb: text_aabb,
|
|
plinegen: true,
|
|
fill_tris: vec![],
|
|
fill_tris_low: Vec::new(),
|
|
});
|
|
return wires;
|
|
}
|
|
|
|
let mut out: Vec<WireModel> = Vec::new();
|
|
let mut is_first = true;
|
|
for bin in bins {
|
|
let wire_color = match bin.color {
|
|
Some([r, g, b]) => [r, g, b, color[3]],
|
|
None => color,
|
|
};
|
|
// Bold text strokes carry a wider pen so the editor preview
|
|
// draws them thicker (these stroke wires exist only when
|
|
// strokes are forced, i.e. the preview; the main render uses
|
|
// SDF where bold is a wider baked pen).
|
|
let bin_lw = if bin.bold {
|
|
line_weight_px.max(1.0) * 2.4
|
|
} else {
|
|
line_weight_px
|
|
};
|
|
|
|
if !bin.pts.is_empty() {
|
|
let (snap, keys, tangents) = if is_first {
|
|
is_first = false;
|
|
(
|
|
snap_pts.clone(),
|
|
key_vertices.clone(),
|
|
te.tangent_geoms.clone(),
|
|
)
|
|
} else {
|
|
(Vec::new(), Vec::new(), Vec::new())
|
|
};
|
|
out.push(WireModel {
|
|
point_marker: None,
|
|
taper_widths: Vec::new(),
|
|
pattern_stations: Vec::new(),
|
|
world_width: 0.0,
|
|
depth_override: None,
|
|
display_visible: true,
|
|
plot_visible: true,
|
|
fill_is_3d: false,
|
|
fill_is_2d_solid: false,
|
|
render_instance: None,
|
|
pick_tris: Vec::new(),
|
|
pick_tris_low: Vec::new(),
|
|
dash_from_start: false,
|
|
dash_align_end: None,
|
|
text_verts: Vec::new(),
|
|
name: name.clone(),
|
|
points: bin.pts,
|
|
points_low: bin.pts_low,
|
|
color: wire_color,
|
|
selected,
|
|
pattern_length: 0.0,
|
|
pattern: [0.0; 8],
|
|
line_weight_px: bin_lw,
|
|
snap_pts: snap,
|
|
tangent_geoms: tangents,
|
|
aci: 0,
|
|
key_vertices: keys,
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
plinegen: true,
|
|
fill_tris: vec![],
|
|
fill_tris_low: Vec::new(),
|
|
});
|
|
}
|
|
|
|
if !bin.fill_tris.is_empty() {
|
|
let (snap, keys, tangents) = if is_first {
|
|
is_first = false;
|
|
(
|
|
snap_pts.clone(),
|
|
key_vertices.clone(),
|
|
te.tangent_geoms.clone(),
|
|
)
|
|
} else {
|
|
(Vec::new(), Vec::new(), Vec::new())
|
|
};
|
|
out.push(WireModel {
|
|
point_marker: None,
|
|
taper_widths: Vec::new(),
|
|
pattern_stations: Vec::new(),
|
|
world_width: 0.0,
|
|
depth_override: None,
|
|
display_visible: true,
|
|
plot_visible: true,
|
|
fill_is_3d: false,
|
|
fill_is_2d_solid: false,
|
|
render_instance: None,
|
|
pick_tris: Vec::new(),
|
|
pick_tris_low: Vec::new(),
|
|
dash_from_start: false,
|
|
dash_align_end: None,
|
|
text_verts: Vec::new(),
|
|
name: name.clone(),
|
|
points: Vec::new(),
|
|
points_low: Vec::new(),
|
|
color: wire_color,
|
|
selected,
|
|
pattern_length: 0.0,
|
|
pattern: [0.0; 8],
|
|
line_weight_px,
|
|
snap_pts: snap,
|
|
tangent_geoms: tangents,
|
|
aci: 0,
|
|
key_vertices: keys,
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
plinegen: true,
|
|
fill_tris: bin.fill_tris,
|
|
fill_tris_low: bin.fill_tris_low,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Composite Text objects (e.g. a tolerance frame) keep geometry
|
|
// in `bins` (run-less groups) and text in `sdf_verts` (run
|
|
// groups). Emit the glyphs on their own wire carrying the tight
|
|
// glyph-box AABB so the text draws + picks alongside the box
|
|
// strokes. TEXT / MTEXT never reach here with SDF on (their bins
|
|
// are empty → the early-return path above).
|
|
if !sdf_verts.is_empty() {
|
|
out.push(WireModel {
|
|
point_marker: None,
|
|
taper_widths: Vec::new(),
|
|
pattern_stations: Vec::new(),
|
|
world_width: 0.0,
|
|
depth_override: None,
|
|
display_visible: true,
|
|
plot_visible: true,
|
|
fill_is_3d: false,
|
|
fill_is_2d_solid: false,
|
|
render_instance: None,
|
|
pick_tris: Vec::new(),
|
|
pick_tris_low: Vec::new(),
|
|
dash_from_start: false,
|
|
dash_align_end: None,
|
|
text_verts: sdf_verts,
|
|
name: name.clone(),
|
|
points: Vec::new(),
|
|
points_low: Vec::new(),
|
|
color,
|
|
selected,
|
|
pattern_length: 0.0,
|
|
pattern: [0.0; 8],
|
|
line_weight_px,
|
|
snap_pts: Vec::new(),
|
|
tangent_geoms: Vec::new(),
|
|
aci: 0,
|
|
key_vertices: Vec::new(),
|
|
aabb: text_aabb,
|
|
plinegen: true,
|
|
fill_tris: vec![],
|
|
fill_tris_low: Vec::new(),
|
|
});
|
|
}
|
|
|
|
if out.is_empty() {
|
|
out.push(WireModel {
|
|
point_marker: None,
|
|
taper_widths: Vec::new(),
|
|
pattern_stations: Vec::new(),
|
|
world_width: 0.0,
|
|
depth_override: None,
|
|
display_visible: true,
|
|
plot_visible: true,
|
|
fill_is_3d: false,
|
|
fill_is_2d_solid: false,
|
|
render_instance: None,
|
|
pick_tris: Vec::new(),
|
|
pick_tris_low: Vec::new(),
|
|
dash_from_start: false,
|
|
dash_align_end: None,
|
|
text_verts: Vec::new(),
|
|
name,
|
|
points: Vec::new(),
|
|
points_low: Vec::new(),
|
|
color,
|
|
selected,
|
|
pattern_length: 0.0,
|
|
pattern: [0.0; 8],
|
|
line_weight_px,
|
|
snap_pts,
|
|
tangent_geoms: te.tangent_geoms,
|
|
aci: 0,
|
|
key_vertices,
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
plinegen: true,
|
|
fill_tris: vec![],
|
|
fill_tris_low: Vec::new(),
|
|
});
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// ── Standard topology objects ─────────────────────────────────
|
|
RenderObject::Dot(position) => {
|
|
{
|
|
{
|
|
// Split into a coarse float and a fine correction, so
|
|
// a point at survey coordinates keeps its last
|
|
// millimetres instead of losing them to f32.
|
|
let [x, y, z] = [
|
|
position[0] as f32,
|
|
position[1] as f32,
|
|
position[2] as f32,
|
|
];
|
|
let [xl, yl, zl] = [
|
|
(position[0] - x as f64) as f32,
|
|
(position[1] - y as f64) as f32,
|
|
(position[2] - z as f64) as f32,
|
|
];
|
|
// A PDMODE=0 point is a single dot. Size its marker to
|
|
// ~1 px so it reads as a dot rather than a large
|
|
// world-space "+" in small drawings — otherwise the
|
|
// dimension def-points on the Defpoints layer litter
|
|
// the view with crosses. The tessellation cache keys on
|
|
// world-per-pixel, so this re-sizes on zoom and stays a
|
|
// constant on-screen size. (#139)
|
|
let s = world_per_pixel
|
|
.map(|w| (w * 0.75).max(1e-6))
|
|
.unwrap_or(0.1);
|
|
let snap_pts = te.snap_pts;
|
|
let key_vertices: Vec<[f64; 3]> = te
|
|
.key_vertices
|
|
.into_iter()
|
|
.map(|[kx, ky, kz]| [kx, ky, kz])
|
|
.collect();
|
|
return vec![WireModel {
|
|
point_marker: None,
|
|
taper_widths: Vec::new(),
|
|
pattern_stations: Vec::new(),
|
|
world_width: 0.0,
|
|
depth_override: None,
|
|
display_visible: true,
|
|
plot_visible: true,
|
|
fill_is_3d: false,
|
|
fill_is_2d_solid: false,
|
|
render_instance: None,
|
|
pick_tris: Vec::new(),
|
|
pick_tris_low: Vec::new(),
|
|
dash_from_start: false,
|
|
dash_align_end: None,
|
|
text_verts: Vec::new(),
|
|
name,
|
|
points: vec![
|
|
[x - s, y, z],
|
|
[x + s, y, z],
|
|
[x, y - s, z],
|
|
[x, y + s, z],
|
|
],
|
|
// All four cross points share the Point's residual
|
|
// (the cross arms are tiny, < 0.1 m, so the low
|
|
// component of the centre is also the right one
|
|
// for the arm tips at f32 precision).
|
|
points_low: vec![[xl, yl, zl]; 4],
|
|
color,
|
|
selected,
|
|
pattern_length: 0.0,
|
|
pattern: [0.0; 8],
|
|
line_weight_px: 1.0,
|
|
snap_pts,
|
|
tangent_geoms: te.tangent_geoms,
|
|
aci: 0,
|
|
key_vertices,
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
plinegen: true,
|
|
fill_tris: vec![],
|
|
fill_tris_low: Vec::new(),
|
|
}];
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
RenderObject::Lines(points) => {
|
|
// Points are world-space f64 from entity converters (polyline,
|
|
// leader, mesh, solid2d, etc.). Subtract world_offset in f64
|
|
// and split into double-single (high, low) f32 buffers — the
|
|
// GPU shader pairs them so drawings at large UTM-style
|
|
// coordinates keep sub-unit precision in the wire model and
|
|
// don't jitter on camera movement.
|
|
let (local_pts, local_pts_low) = points_to_ds(points);
|
|
let snap_pts = te.snap_pts;
|
|
let key_vertices: Vec<[f64; 3]> = te
|
|
.key_vertices
|
|
.into_iter()
|
|
.map(|[x, y, z]| [x, y, z])
|
|
.collect();
|
|
let (fill_tris, fill_tris_low) = points_to_ds(te.fill_tris);
|
|
// Only a real 3-D mesh surface (PolyfaceMesh / PolygonMesh /
|
|
// the modern subdivision Mesh) fill renders shaded-only with
|
|
// hidden-surface depth; every other `fill_tris` here (a SOLID's
|
|
// filled quad — e.g. a `_BoxFilled` arrowhead) is a flat 2-D
|
|
// overlay visible in every view mode. Leaving `Mesh` out drops
|
|
// its face fill into the 2-D buffer, so it drew in wireframe too.
|
|
let fill_is_3d = matches!(
|
|
entity,
|
|
EntityType::Face3D(_)
|
|
| EntityType::PolyfaceMesh(_)
|
|
| EntityType::PolygonMesh(_)
|
|
| EntityType::Mesh(_)
|
|
) || matches!(entity, EntityType::Solid(solid) if solid.thickness.abs() > 1.0e-10);
|
|
// Thickness walls ride on the wire that carries their edges, not
|
|
// on a wire of their own: they are pick geometry for that entity,
|
|
// and `fill_tris` below deliberately splits off into a fill-only
|
|
// wire (`is_fill_only`) which has no `points` to hang them from.
|
|
let (pick_tris, pick_tris_low) = points_to_ds(te.pick_tris);
|
|
let mut out = Vec::new();
|
|
let mut is_first = true;
|
|
|
|
// A thickened polyline's extrusion — its corner / cap edges
|
|
// frame the solid tube and read black (like solid-with-edges
|
|
// outlines), while the tube fill keeps the entity colour. Both
|
|
// wide polyline kinds (LwPolyline + Polyline2D) extrude tubes.
|
|
let is_thick_extrusion = matches!(
|
|
entity,
|
|
EntityType::LwPolyline(p) if p.thickness.abs() > 1e-10
|
|
) || matches!(
|
|
entity,
|
|
EntityType::Polyline2D(p) if p.thickness.abs() > 1e-10
|
|
);
|
|
let edge_color = if is_thick_extrusion {
|
|
[0.0, 0.0, 0.0, 1.0]
|
|
} else {
|
|
color
|
|
};
|
|
// Basic curves keep their resolved linetype.
|
|
let (edge_pattern_length, edge_pattern) =
|
|
if matches!(
|
|
entity,
|
|
EntityType::Line(_)
|
|
| EntityType::Circle(_)
|
|
| EntityType::Arc(_)
|
|
| EntityType::Ellipse(_)
|
|
) {
|
|
(pattern_length, pattern)
|
|
} else {
|
|
(0.0, [0.0; 8])
|
|
};
|
|
if !local_pts.is_empty() {
|
|
let (snap, keys, tangents) = if is_first {
|
|
is_first = false;
|
|
(
|
|
snap_pts.clone(),
|
|
key_vertices.clone(),
|
|
te.tangent_geoms.clone(),
|
|
)
|
|
} else {
|
|
(Vec::new(), Vec::new(), Vec::new())
|
|
};
|
|
let point_marker =
|
|
crate::entities::point::relative_marker_spec(entity, document);
|
|
out.push(WireModel {
|
|
point_marker,
|
|
taper_widths: Vec::new(),
|
|
pattern_stations: Vec::new(),
|
|
world_width: polyline_band_width(entity, document.header.fill_mode),
|
|
depth_override: None,
|
|
display_visible: true,
|
|
plot_visible: true,
|
|
fill_is_3d,
|
|
fill_is_2d_solid: false,
|
|
render_instance: None,
|
|
pick_tris,
|
|
pick_tris_low,
|
|
dash_from_start: false,
|
|
dash_align_end: None,
|
|
text_verts: Vec::new(),
|
|
name: name.clone(),
|
|
points: local_pts,
|
|
points_low: local_pts_low,
|
|
color: edge_color,
|
|
selected,
|
|
pattern_length: edge_pattern_length,
|
|
pattern: edge_pattern,
|
|
line_weight_px,
|
|
snap_pts: snap,
|
|
tangent_geoms: tangents,
|
|
aci: 0,
|
|
key_vertices: keys,
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
plinegen: true,
|
|
fill_tris: vec![],
|
|
fill_tris_low: Vec::new(),
|
|
});
|
|
}
|
|
|
|
if !fill_tris.is_empty() {
|
|
let (snap, keys, tangents) = if is_first {
|
|
(
|
|
snap_pts.clone(),
|
|
key_vertices.clone(),
|
|
te.tangent_geoms.clone(),
|
|
)
|
|
} else {
|
|
(Vec::new(), Vec::new(), Vec::new())
|
|
};
|
|
out.push(WireModel {
|
|
point_marker: None,
|
|
taper_widths: Vec::new(),
|
|
pattern_stations: Vec::new(),
|
|
world_width: 0.0,
|
|
pick_tris: Vec::new(),
|
|
pick_tris_low: Vec::new(),
|
|
dash_from_start: false,
|
|
dash_align_end: None,
|
|
text_verts: Vec::new(),
|
|
name: name.clone(),
|
|
points: Vec::new(),
|
|
points_low: Vec::new(),
|
|
color,
|
|
selected,
|
|
pattern_length: 0.0,
|
|
pattern: [0.0; 8],
|
|
line_weight_px,
|
|
snap_pts: snap,
|
|
tangent_geoms: tangents,
|
|
aci: 0,
|
|
key_vertices: keys,
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
plinegen: true,
|
|
fill_tris,
|
|
fill_tris_low,
|
|
fill_is_3d,
|
|
fill_is_2d_solid: matches!(entity, EntityType::Solid(_)),
|
|
render_instance: None,
|
|
depth_override: None,
|
|
display_visible: true,
|
|
plot_visible: true,
|
|
});
|
|
}
|
|
|
|
if out.is_empty() {
|
|
out.push(WireModel {
|
|
point_marker: None,
|
|
taper_widths: Vec::new(),
|
|
pattern_stations: Vec::new(),
|
|
world_width: 0.0,
|
|
depth_override: None,
|
|
display_visible: true,
|
|
plot_visible: true,
|
|
fill_is_3d: false,
|
|
fill_is_2d_solid: false,
|
|
render_instance: None,
|
|
pick_tris: Vec::new(),
|
|
pick_tris_low: Vec::new(),
|
|
dash_from_start: false,
|
|
dash_align_end: None,
|
|
text_verts: Vec::new(),
|
|
name,
|
|
points: Vec::new(),
|
|
points_low: Vec::new(),
|
|
color,
|
|
selected,
|
|
pattern_length: 0.0,
|
|
pattern: [0.0; 8],
|
|
line_weight_px,
|
|
snap_pts,
|
|
tangent_geoms: te.tangent_geoms,
|
|
aci: 0,
|
|
key_vertices,
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
plinegen: true,
|
|
fill_tris: vec![],
|
|
fill_tris_low: Vec::new(),
|
|
});
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
RenderObject::BoundaryLines {
|
|
points,
|
|
stations,
|
|
point_segments,
|
|
station_pieces,
|
|
source_length,
|
|
plinegen,
|
|
} => {
|
|
let (local_pts, local_pts_low) = points_to_ds(points);
|
|
let key_vertices = te
|
|
.key_vertices
|
|
.into_iter()
|
|
.map(|[x, y, z]| [x, y, z])
|
|
.collect();
|
|
let station_data = crate::scene::model::wire_model::encode_pattern_stations(
|
|
stations,
|
|
source_length,
|
|
&point_segments,
|
|
&station_pieces,
|
|
);
|
|
return vec![WireModel {
|
|
point_marker: None,
|
|
taper_widths: Vec::new(),
|
|
pattern_stations: station_data,
|
|
world_width: 0.0,
|
|
depth_override: None,
|
|
display_visible: true,
|
|
plot_visible: true,
|
|
fill_is_3d: false,
|
|
fill_is_2d_solid: false,
|
|
render_instance: None,
|
|
pick_tris: Vec::new(),
|
|
pick_tris_low: Vec::new(),
|
|
dash_from_start: false,
|
|
dash_align_end: None,
|
|
text_verts: Vec::new(),
|
|
name,
|
|
points: local_pts,
|
|
points_low: local_pts_low,
|
|
color,
|
|
selected,
|
|
pattern_length,
|
|
pattern,
|
|
line_weight_px,
|
|
snap_pts: te.snap_pts,
|
|
tangent_geoms: te.tangent_geoms,
|
|
aci: 0,
|
|
key_vertices,
|
|
plinegen,
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
fill_tris: Vec::new(),
|
|
fill_tris_low: Vec::new(),
|
|
}];
|
|
}
|
|
|
|
RenderObject::SegmentedLines(points) => {
|
|
let (local_pts, local_pts_low) = points_to_ds(points);
|
|
let snap_pts = te.snap_pts;
|
|
let key_vertices: Vec<[f64; 3]> = te
|
|
.key_vertices
|
|
.into_iter()
|
|
.map(|[x, y, z]| [x, y, z])
|
|
.collect();
|
|
// A wide polyline with PLINEGEN=0 arrives here: same shader-band
|
|
// treatment as the Contour arm, restarting the dash per segment.
|
|
let (pick_tris, pick_tris_low) = points_to_ds(te.pick_tris);
|
|
return vec![WireModel {
|
|
point_marker: None,
|
|
taper_widths: Vec::new(),
|
|
pattern_stations: Vec::new(),
|
|
world_width: polyline_band_width(entity, document.header.fill_mode),
|
|
depth_override: None,
|
|
display_visible: true,
|
|
plot_visible: true,
|
|
fill_is_3d: false,
|
|
fill_is_2d_solid: false,
|
|
render_instance: None,
|
|
pick_tris,
|
|
pick_tris_low,
|
|
dash_from_start: false,
|
|
dash_align_end: None,
|
|
text_verts: Vec::new(),
|
|
name,
|
|
points: local_pts,
|
|
points_low: local_pts_low,
|
|
color,
|
|
selected,
|
|
pattern_length,
|
|
pattern,
|
|
line_weight_px,
|
|
snap_pts,
|
|
tangent_geoms: te.tangent_geoms,
|
|
aci: 0,
|
|
key_vertices,
|
|
plinegen: false,
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
fill_tris: vec![],
|
|
fill_tris_low: Vec::new(),
|
|
}];
|
|
}
|
|
|
|
RenderObject::TaperedLines(points, widths) => {
|
|
// A wide polyline whose width varies: one continuous band wire
|
|
// carrying a per-point width; the shader interpolates each
|
|
// segment's two endpoint widths. `world_width` (the widest edge)
|
|
// stays as the constant fallback for PDF export + a hairline
|
|
// floor when zoomed out.
|
|
let (local_pts, local_pts_low) = points_to_ds(points);
|
|
let snap_pts = te.snap_pts;
|
|
let key_vertices: Vec<[f64; 3]> = te
|
|
.key_vertices
|
|
.into_iter()
|
|
.map(|[x, y, z]| [x, y, z])
|
|
.collect();
|
|
let (pick_tris, pick_tris_low) = points_to_ds(te.pick_tris);
|
|
let world_width = widths.iter().copied().fold(0.0f32, f32::max);
|
|
return vec![WireModel {
|
|
point_marker: None,
|
|
taper_widths: widths,
|
|
pattern_stations: Vec::new(),
|
|
world_width,
|
|
depth_override: None,
|
|
display_visible: true,
|
|
plot_visible: true,
|
|
fill_is_3d: false,
|
|
fill_is_2d_solid: false,
|
|
render_instance: None,
|
|
pick_tris,
|
|
pick_tris_low,
|
|
dash_from_start: false,
|
|
dash_align_end: None,
|
|
text_verts: Vec::new(),
|
|
name,
|
|
points: local_pts,
|
|
points_low: local_pts_low,
|
|
color,
|
|
selected,
|
|
pattern_length,
|
|
pattern,
|
|
line_weight_px,
|
|
snap_pts,
|
|
tangent_geoms: te.tangent_geoms,
|
|
aci: 0,
|
|
key_vertices,
|
|
plinegen: true,
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
fill_tris: vec![],
|
|
fill_tris_low: Vec::new(),
|
|
}];
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// ── Fallback for Viewport / Insert / Hatch / Ole2Frame ────────────────
|
|
let (mut points_f64, snap_pts, tangent_geoms, mut key_vertices) =
|
|
fallback_geometry(entity);
|
|
let clipped_viewport_polygon = match entity {
|
|
EntityType::Viewport(viewport) if !viewport.clip_boundary_handle.is_null() => {
|
|
let polygon = crate::scene::project::clip_boundary_polygon_for_document(
|
|
document,
|
|
viewport.clip_boundary_handle,
|
|
viewport.center.z as f32,
|
|
);
|
|
if polygon.len() >= 3 {
|
|
let polygon: Vec<[f64; 3]> = polygon
|
|
.into_iter()
|
|
.map(|point| {
|
|
[
|
|
point[0] as f64,
|
|
point[1] as f64,
|
|
point[2] as f64,
|
|
]
|
|
})
|
|
.collect();
|
|
points_f64 = polygon.clone();
|
|
points_f64.push(polygon[0]);
|
|
key_vertices = polygon.clone();
|
|
Some(polygon)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
_ => None,
|
|
};
|
|
// `points_f64` are absolute world coords; split into the double-single
|
|
// high/low pair so the outline reconstructs to f64 precision at UTM scale
|
|
// (a NaN separator stays NaN in both buffers).
|
|
let mut points: Vec<[f32; 3]> = Vec::with_capacity(points_f64.len());
|
|
let mut points_low: Vec<[f32; 3]> = Vec::with_capacity(points_f64.len());
|
|
for [x, y, z] in &points_f64 {
|
|
if !x.is_finite() || !y.is_finite() {
|
|
points.push([f32::NAN, f32::NAN, f32::NAN]);
|
|
points_low.push([0.0; 3]);
|
|
continue;
|
|
}
|
|
let (hx, lx) = split_ds(*x);
|
|
let (hy, ly) = split_ds(*y);
|
|
let (hz, lz) = split_ds(*z);
|
|
points.push([hx, hy, hz]);
|
|
points_low.push([lx, ly, lz]);
|
|
}
|
|
// fallback_geometry still emits offset-relative f32 snap points; widen to
|
|
// f64 for the WireModel's double-single-era snap buffer.
|
|
let snap_pts: Vec<(glam::DVec3, SnapHint)> =
|
|
snap_pts.into_iter().map(|(p, h)| (p.as_dvec3(), h)).collect();
|
|
// A paper-space viewport is a window, not a wireframe: give it an interior
|
|
// pick surface so a click anywhere inside the frame selects it. Ranked
|
|
// below edge and fill hits, so content drawn inside still wins the click.
|
|
// The sheet ("overall") viewport is the layout's own invisible camera
|
|
// frame covering the whole page — never pickable, or it would swallow
|
|
// every click over the real viewports beneath it. It is identified by the
|
|
// Layout object's viewport link (authoritative — DWG files carry id = 0
|
|
// and this file class centres the sheet viewport off-origin, so neither
|
|
// the id nor the geometry heuristic alone is reliable), with
|
|
// `is_content_viewport` as the fallback classifier.
|
|
let is_sheet_vp = |vp: &acadrust::entities::Viewport| {
|
|
let h = vp.common.handle;
|
|
document.objects.values().any(|obj| {
|
|
matches!(obj, acadrust::objects::ObjectType::Layout(l) if l.viewport == h)
|
|
}) || !crate::scene::Scene::is_content_viewport(vp)
|
|
};
|
|
let (pick_tris, pick_tris_low) = match entity {
|
|
EntityType::Viewport(vp) if !is_sheet_vp(vp) => {
|
|
if let Some(polygon) = clipped_viewport_polygon.as_ref() {
|
|
points_to_ds(crate::entities::mesh::triangulate_planar(polygon))
|
|
} else {
|
|
let (cx, cy, cz) = (vp.center.x, vp.center.y, vp.center.z);
|
|
let (hw, hh) = (vp.width / 2.0, vp.height / 2.0);
|
|
points_to_ds(crate::entities::common::quad_pick_tris(&[
|
|
[cx - hw, cy - hh, cz],
|
|
[cx + hw, cy - hh, cz],
|
|
[cx + hw, cy + hh, cz],
|
|
[cx - hw, cy + hh, cz],
|
|
]))
|
|
}
|
|
}
|
|
_ => (Vec::new(), Vec::new()),
|
|
};
|
|
vec![WireModel {
|
|
point_marker: None,
|
|
taper_widths: Vec::new(),
|
|
pattern_stations: Vec::new(),
|
|
world_width: 0.0,
|
|
depth_override: None,
|
|
display_visible: true,
|
|
plot_visible: true,
|
|
fill_is_3d: false,
|
|
fill_is_2d_solid: false,
|
|
render_instance: None,
|
|
pick_tris,
|
|
pick_tris_low,
|
|
dash_from_start: false,
|
|
dash_align_end: None,
|
|
text_verts: Vec::new(),
|
|
name,
|
|
points,
|
|
points_low,
|
|
color,
|
|
selected,
|
|
aci: 0,
|
|
pattern_length,
|
|
pattern,
|
|
line_weight_px,
|
|
snap_pts,
|
|
tangent_geoms,
|
|
key_vertices,
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
plinegen: true,
|
|
fill_tris: vec![],
|
|
fill_tris_low: Vec::new(),
|
|
}]
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
pub(crate) enum ArrowKind {
|
|
None,
|
|
Triangle { size: f32, filled: bool, size_mul: f32 },
|
|
Tick { size: f32 },
|
|
Open { size: f32, half_angle: f32 },
|
|
Dot { size: f32, filled: bool },
|
|
Origin { size: f32 },
|
|
Box_ { size: f32, filled: bool },
|
|
Datum { size: f32, filled: bool },
|
|
Custom {
|
|
size: f32,
|
|
lines: Vec<[f32; 3]>,
|
|
fill: Vec<[f32; 3]>,
|
|
},
|
|
}
|
|
|
|
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);
|
|
}
|
|
let Some(record) = doc.block_records.iter().find(|b| b.handle == handle) else {
|
|
return arrow_from_block_name(None, dimasz);
|
|
};
|
|
if let Some(arrow) = builtin_arrow_from_block_name(&record.name, dimasz) {
|
|
return arrow;
|
|
}
|
|
custom_arrow_from_block(doc, record, dimasz, defer_hatch)
|
|
.unwrap_or_else(|| arrow_from_block_name(None, dimasz))
|
|
}
|
|
|
|
fn arrow_from_block_name(name: Option<&str>, dimasz: f32) -> ArrowKind {
|
|
name.and_then(|name| builtin_arrow_from_block_name(name, dimasz))
|
|
.unwrap_or(ArrowKind::Triangle {
|
|
size: dimasz,
|
|
filled: true,
|
|
size_mul: 1.0,
|
|
})
|
|
}
|
|
|
|
fn builtin_arrow_from_block_name(name: &str, dimasz: f32) -> Option<ArrowKind> {
|
|
// Built-in arrow block names may carry a leading underscore. Normalize it
|
|
// before matching the canonical names.
|
|
let n = name
|
|
.trim()
|
|
.trim_start_matches('_')
|
|
.to_ascii_uppercase();
|
|
match n.as_str() {
|
|
"" | "CLOSEDFILLED" => Some(ArrowKind::Triangle {
|
|
size: dimasz,
|
|
filled: true,
|
|
size_mul: 1.0,
|
|
}),
|
|
"CLOSED" | "CLOSEDBLANK" => Some(ArrowKind::Triangle {
|
|
size: dimasz,
|
|
filled: false,
|
|
size_mul: 1.0,
|
|
}),
|
|
"SMALL" => Some(ArrowKind::Triangle {
|
|
size: dimasz,
|
|
filled: true,
|
|
size_mul: 0.5,
|
|
}),
|
|
"OPEN" => Some(ArrowKind::Open {
|
|
size: dimasz,
|
|
half_angle: 9.5_f32.to_radians(),
|
|
}),
|
|
"OPEN30" => Some(ArrowKind::Open {
|
|
size: dimasz,
|
|
half_angle: 15.0_f32.to_radians(),
|
|
}),
|
|
"OPEN90" => Some(ArrowKind::Open {
|
|
size: dimasz,
|
|
half_angle: 45.0_f32.to_radians(),
|
|
}),
|
|
"DOT" => Some(ArrowKind::Dot {
|
|
size: dimasz,
|
|
filled: true,
|
|
}),
|
|
"DOTSMALL" => Some(ArrowKind::Dot {
|
|
size: dimasz * 0.5,
|
|
filled: true,
|
|
}),
|
|
"DOTBLANK" => Some(ArrowKind::Dot {
|
|
size: dimasz,
|
|
filled: false,
|
|
}),
|
|
"DOTSMALLBLANK" => Some(ArrowKind::Dot {
|
|
size: dimasz * 0.5,
|
|
filled: false,
|
|
}),
|
|
"ORIGIN" | "ORIGIN2" | "ORIGININDICATOR" | "ORIGININDICATOR2" => {
|
|
Some(ArrowKind::Origin { size: dimasz })
|
|
}
|
|
// `ArrowKind::Tick` draws the stroke `size` to either side of the tip
|
|
// (total 2·size — its `size` is a half-length, matching DIMTSZ). For
|
|
// a block-selected tick DIMASZ is the full stroke length, so halve it.
|
|
"OBLIQUE" | "ARCHTICK" => Some(ArrowKind::Tick { size: dimasz * 0.5 }),
|
|
"BOXFILLED" => Some(ArrowKind::Box_ {
|
|
size: dimasz,
|
|
filled: true,
|
|
}),
|
|
"BOXBLANK" | "BOX" => Some(ArrowKind::Box_ {
|
|
size: dimasz,
|
|
filled: false,
|
|
}),
|
|
"DATUMFILLED" | "DATUMTRIANGLEFILLED" => Some(ArrowKind::Datum {
|
|
size: dimasz,
|
|
filled: true,
|
|
}),
|
|
"DATUMBLANK" | "DATUMTRIANGLE" => Some(ArrowKind::Datum {
|
|
size: dimasz,
|
|
filled: false,
|
|
}),
|
|
"NONE" => Some(ArrowKind::None),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
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()
|
|
|| record.is_paper_space()
|
|
|| record.flags.is_xref
|
|
|| record.flags.is_xref_overlay
|
|
|| record.flags.is_external
|
|
{
|
|
return None;
|
|
}
|
|
|
|
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 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 {
|
|
size: dimasz,
|
|
lines,
|
|
fill,
|
|
})
|
|
}
|
|
}
|
|
|
|
fn append_custom_arrow_leaf(
|
|
doc: &CadDocument,
|
|
entity: &EntityType,
|
|
lines: &mut Vec<[f32; 3]>,
|
|
fill: &mut Vec<[f32; 3]>,
|
|
) {
|
|
match entity {
|
|
EntityType::Block(_)
|
|
| EntityType::BlockEnd(_)
|
|
| EntityType::AttributeDefinition(_)
|
|
| EntityType::Dimension(_)
|
|
| EntityType::Leader(_)
|
|
| EntityType::MultiLeader(_)
|
|
| EntityType::Insert(_) => return,
|
|
EntityType::Hatch(hatch) => {
|
|
append_custom_hatch_geometry(
|
|
hatch,
|
|
acadrust::types::Vector3::ZERO,
|
|
lines,
|
|
fill,
|
|
);
|
|
return;
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
let wires = tessellate(
|
|
doc,
|
|
entity.common().handle,
|
|
entity,
|
|
false,
|
|
[1.0; 4],
|
|
0.0,
|
|
[0.0; 8],
|
|
1.0,
|
|
1.0,
|
|
None,
|
|
None,
|
|
[0.0, 0.0, 0.0, 1.0],
|
|
true,
|
|
);
|
|
for wire in wires {
|
|
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_geometry(
|
|
hatch: &acadrust::entities::Hatch,
|
|
base: acadrust::types::Vector3,
|
|
lines: &mut Vec<[f32; 3]>,
|
|
fill: &mut Vec<[f32; 3]>,
|
|
) {
|
|
use lyon_tessellation::math::point;
|
|
use lyon_tessellation::path::Path;
|
|
use lyon_tessellation::{
|
|
BuffersBuilder, FillOptions, FillRule, FillTessellator, FillVertex, VertexBuffers,
|
|
};
|
|
|
|
let Some(model) = crate::scene::Scene::hatch_model_from_dxf(hatch, [1.0; 4]) else {
|
|
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]>| {
|
|
if ring.len() >= 3 {
|
|
builder.begin(point(ring[0][0], ring[0][1]));
|
|
for p in &ring[1..] {
|
|
builder.line_to(point(p[0], p[1]));
|
|
}
|
|
builder.end(true);
|
|
}
|
|
ring.clear();
|
|
};
|
|
for &[x, y] in model.boundary.iter() {
|
|
if x.is_nan() || y.is_nan() {
|
|
finish_ring(&mut ring);
|
|
continue;
|
|
}
|
|
ring.push([
|
|
(model.world_origin[0] + x as f64 - base.x) as f32,
|
|
(model.world_origin[1] + y as f64 - base.y) as f32,
|
|
]);
|
|
}
|
|
finish_ring(&mut ring);
|
|
|
|
let mut geometry: VertexBuffers<[f32; 2], u32> = VertexBuffers::new();
|
|
let mut tessellator = FillTessellator::new();
|
|
if tessellator
|
|
.tessellate_path(
|
|
&builder.build(),
|
|
&FillOptions::default().with_fill_rule(FillRule::EvenOdd),
|
|
&mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| {
|
|
vertex.position().to_array()
|
|
}),
|
|
)
|
|
.is_err()
|
|
{
|
|
return;
|
|
}
|
|
let z = -base.z as f32;
|
|
fill.extend(
|
|
geometry
|
|
.indices
|
|
.iter()
|
|
.filter_map(|&index| geometry.vertices.get(index as usize))
|
|
.map(|&[x, y]| [x, y, z]),
|
|
);
|
|
}
|
|
|
|
fn append_custom_wire_points(
|
|
points: &[[f32; 3]],
|
|
points_low: &[[f32; 3]],
|
|
base: acadrust::types::Vector3,
|
|
out: &mut Vec<[f32; 3]>,
|
|
) {
|
|
if points.is_empty() {
|
|
return;
|
|
}
|
|
if !out.is_empty() && !out.last().is_some_and(|p| p[0].is_nan()) {
|
|
out.push([f32::NAN; 3]);
|
|
}
|
|
for (index, point) in points.iter().enumerate() {
|
|
if point[0].is_nan() {
|
|
if !out.last().is_some_and(|p| p[0].is_nan()) {
|
|
out.push([f32::NAN; 3]);
|
|
}
|
|
continue;
|
|
}
|
|
let low = points_low.get(index).copied().unwrap_or([0.0; 3]);
|
|
out.push([
|
|
(point[0] as f64 + low[0] as f64 - base.x) as f32,
|
|
(point[1] as f64 + low[1] as f64 - base.y) as f32,
|
|
(point[2] as f64 + low[2] as f64 - base.z) as f32,
|
|
]);
|
|
}
|
|
}
|
|
|
|
fn append_custom_fill_points(
|
|
points: &[[f32; 3]],
|
|
points_low: &[[f32; 3]],
|
|
base: acadrust::types::Vector3,
|
|
out: &mut Vec<[f32; 3]>,
|
|
) {
|
|
for (index, point) in points.iter().enumerate() {
|
|
let low = points_low.get(index).copied().unwrap_or([0.0; 3]);
|
|
out.push([
|
|
(point[0] as f64 + low[0] as f64 - base.x) as f32,
|
|
(point[1] as f64 + low[1] as f64 - base.y) as f32,
|
|
(point[2] as f64 + low[2] as f64 - base.z) as f32,
|
|
]);
|
|
}
|
|
}
|
|
|
|
pub(crate) struct DimGeom {
|
|
pub(crate) ext_lines: Vec<[f32; 3]>,
|
|
pub(crate) dim_lines: Vec<[f32; 3]>,
|
|
pub(crate) arrow_fill: Vec<[f32; 3]>,
|
|
}
|
|
|
|
impl DimGeom {
|
|
pub(crate) fn new() -> Self {
|
|
Self {
|
|
ext_lines: Vec::new(),
|
|
dim_lines: Vec::new(),
|
|
arrow_fill: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/// Convert an acadrust `Color` to RGBA, falling back to `inherited` for
|
|
/// `ByLayer` / `ByBlock` (assumes those are already resolved upstream).
|
|
pub(crate) fn color_or_inherit(c: &AcadColor, inherited: [f32; 4]) -> [f32; 4] {
|
|
match c.rgb() {
|
|
Some((r, g, b)) => [
|
|
r as f32 / 255.0,
|
|
g as f32 / 255.0,
|
|
b as f32 / 255.0,
|
|
inherited[3],
|
|
],
|
|
None => inherited,
|
|
}
|
|
}
|
|
|
|
|
|
// ── Entity Z helper ───────────────────────────────────────────────────────
|
|
|
|
/// Extract the Z elevation from a text/mtext entity.
|
|
pub(crate) fn entity_z(entity: &EntityType) -> f32 {
|
|
match entity {
|
|
EntityType::Text(t) => t.insertion_point.z as f32,
|
|
EntityType::MText(t) => t.insertion_point.z as f32,
|
|
_ => 0.0,
|
|
}
|
|
}
|
|
|
|
/// Shader band width, or zero while FILLMODE draws the kernel boundary.
|
|
fn polyline_band_width(entity: &EntityType, fill_mode: bool) -> f32 {
|
|
if !fill_mode {
|
|
return 0.0;
|
|
}
|
|
let w = match entity {
|
|
EntityType::LwPolyline(p) if p.thickness.abs() <= 1e-10 => {
|
|
let mut w = p.constant_width;
|
|
for v in &p.vertices {
|
|
w = w.max(v.start_width).max(v.end_width);
|
|
}
|
|
w
|
|
}
|
|
EntityType::Polyline2D(p) if p.thickness.abs() <= 1e-10 => {
|
|
let mut w = p.start_width.max(p.end_width);
|
|
for v in &p.vertices {
|
|
w = w.max(v.start_width).max(v.end_width);
|
|
}
|
|
w
|
|
}
|
|
_ => 0.0,
|
|
};
|
|
if w > 1e-9 {
|
|
w as f32
|
|
} else {
|
|
0.0
|
|
}
|
|
}
|
|
|
|
// ── Fallback geometry (Viewport, Insert, Hatch outline, Ole2Frame) ───────
|
|
//
|
|
// Per-entity blocks have moved to their respective `entities/*.rs` files
|
|
// (Viewport, Insert, Hatch, Ole2Frame) via the `FallbackTess` trait. This
|
|
// function stays as the dispatcher used by the main `tessellate()` path.
|
|
|
|
use crate::entities::traits::FallbackTess;
|
|
use crate::scene::convert::tess_util::FallbackGeometry as Geometry;
|
|
|
|
fn fallback_geometry(entity: &EntityType) -> Geometry {
|
|
match entity {
|
|
EntityType::Viewport(vp) => vp.fallback_geometry(),
|
|
EntityType::Insert(ins) => ins.fallback_geometry(),
|
|
EntityType::Hatch(h) => h.fallback_geometry(),
|
|
EntityType::Ole2Frame(ole) => ole.fallback_geometry(),
|
|
// Modeler solids render as meshes (solid3d_tess). Their wire path
|
|
// contributes only the pre-computed edge wires (empty for binary SAB)
|
|
// plus an insertion snap — never the placeholder segment below, which
|
|
// would otherwise draw a stray 1-unit line at the origin next to the
|
|
// solid.
|
|
EntityType::Solid3D(_)
|
|
| EntityType::Region(_)
|
|
| EntityType::Body(_)
|
|
| EntityType::Surface(_) => {
|
|
let mut snap = vec![];
|
|
if let Some(p) = crate::entities::solid3d::point_of_reference(entity) {
|
|
snap.push((
|
|
Vec3::new((p.x) as f32, (p.y) as f32, (p.z) as f32),
|
|
SnapHint::Insertion,
|
|
));
|
|
}
|
|
(vec![], snap, vec![], vec![])
|
|
}
|
|
_ => {
|
|
let s = 0.5_f64;
|
|
(vec![[-s, 0.0, 0.0], [s, 0.0, 0.0]], vec![], vec![], vec![])
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn push_tri(out: &mut Vec<[f32; 3]>, a: Vec3, b: Vec3, c: Vec3) {
|
|
out.push([a.x, a.y, a.z]);
|
|
out.push([b.x, b.y, b.z]);
|
|
out.push([c.x, c.y, c.z]);
|
|
}
|
|
|
|
pub(crate) fn append_arrow(g: &mut DimGeom, tip: Vec3, dir: Vec3, arrow: &ArrowKind) {
|
|
let dir = normalized_or(dir, Vec3::X);
|
|
let perp = Vec3::new(-dir.y, dir.x, 0.0);
|
|
match arrow {
|
|
ArrowKind::None => {}
|
|
ArrowKind::Triangle {
|
|
size,
|
|
filled,
|
|
size_mul,
|
|
} => {
|
|
let size = *size * *size_mul;
|
|
let base = tip + dir * size;
|
|
// ~1:6 length:half-width ratio (≈9.5° half-angle) matches
|
|
// the standard closed-filled block.
|
|
let half_w = size / 6.0;
|
|
let left = base + perp * half_w;
|
|
let right = base - perp * half_w;
|
|
add_segment(&mut g.dim_lines, tip, left);
|
|
add_segment(&mut g.dim_lines, left, right);
|
|
add_segment(&mut g.dim_lines, right, tip);
|
|
if *filled {
|
|
push_tri(&mut g.arrow_fill, tip, left, right);
|
|
}
|
|
}
|
|
ArrowKind::Tick { size } => {
|
|
// 45° oblique tick crossing the dim line at the tip; `size` is
|
|
// the half-length used by DIMTSZ.
|
|
let off = (dir + perp).normalize_or_zero() * *size;
|
|
add_segment(&mut g.dim_lines, tip - off, tip + off);
|
|
}
|
|
ArrowKind::Open { size, half_angle } => {
|
|
let base = tip + dir * *size;
|
|
let half_w = *size * half_angle.tan();
|
|
let left = base + perp * half_w;
|
|
let right = base - perp * half_w;
|
|
add_segment(&mut g.dim_lines, tip, left);
|
|
add_segment(&mut g.dim_lines, tip, right);
|
|
}
|
|
ArrowKind::Dot { size, filled } => {
|
|
let r = *size * 0.5;
|
|
const N: usize = 16;
|
|
let mut ring: Vec<Vec3> = Vec::with_capacity(N + 1);
|
|
for i in 0..=N {
|
|
let a = i as f32 * std::f32::consts::TAU / N as f32;
|
|
ring.push(tip + Vec3::new(a.cos() * r, a.sin() * r, 0.0));
|
|
}
|
|
add_polyline(&mut g.dim_lines, &ring);
|
|
if *filled {
|
|
for i in 0..N {
|
|
push_tri(&mut g.arrow_fill, tip, ring[i], ring[i + 1]);
|
|
}
|
|
}
|
|
}
|
|
ArrowKind::Origin { size } => {
|
|
// Small filled dot at the tip with a perpendicular tick crossing
|
|
// the dim line — matches "_ORIGIN" / "_ORIGIN2" blocks.
|
|
let r = *size * 0.25;
|
|
const N: usize = 12;
|
|
let mut ring: Vec<Vec3> = Vec::with_capacity(N + 1);
|
|
for i in 0..=N {
|
|
let a = i as f32 * std::f32::consts::TAU / N as f32;
|
|
ring.push(tip + Vec3::new(a.cos() * r, a.sin() * r, 0.0));
|
|
}
|
|
add_polyline(&mut g.dim_lines, &ring);
|
|
for i in 0..N {
|
|
push_tri(&mut g.arrow_fill, tip, ring[i], ring[i + 1]);
|
|
}
|
|
let half = *size * 0.5;
|
|
add_segment(&mut g.dim_lines, tip - perp * half, tip + perp * half);
|
|
}
|
|
ArrowKind::Box_ { size, filled } => {
|
|
let half = *size * 0.5;
|
|
let p1 = tip - dir * half - perp * half;
|
|
let p2 = tip + dir * half - perp * half;
|
|
let p3 = tip + dir * half + perp * half;
|
|
let p4 = tip - dir * half + perp * half;
|
|
add_segment(&mut g.dim_lines, p1, p2);
|
|
add_segment(&mut g.dim_lines, p2, p3);
|
|
add_segment(&mut g.dim_lines, p3, p4);
|
|
add_segment(&mut g.dim_lines, p4, p1);
|
|
if *filled {
|
|
push_tri(&mut g.arrow_fill, p1, p2, p3);
|
|
push_tri(&mut g.arrow_fill, p1, p3, p4);
|
|
}
|
|
}
|
|
ArrowKind::Datum { size, filled } => {
|
|
// Right-pointing triangle with the base perpendicular to the dim
|
|
// line at the tip and the apex along +dir.
|
|
let half = *size * 0.5;
|
|
let base_a = tip + perp * half;
|
|
let base_b = tip - perp * half;
|
|
let apex = tip + dir * *size;
|
|
add_segment(&mut g.dim_lines, base_a, apex);
|
|
add_segment(&mut g.dim_lines, apex, base_b);
|
|
add_segment(&mut g.dim_lines, base_b, base_a);
|
|
if *filled {
|
|
push_tri(&mut g.arrow_fill, base_a, apex, base_b);
|
|
}
|
|
}
|
|
ArrowKind::Custom { size, lines, fill } => {
|
|
let transform = |point: &[f32; 3]| {
|
|
tip - dir * (point[0] * *size) - perp * (point[1] * *size)
|
|
+ Vec3::Z * (point[2] * *size)
|
|
};
|
|
if !lines.is_empty()
|
|
&& !g.dim_lines.is_empty()
|
|
&& !g.dim_lines.last().is_some_and(|point| point[0].is_nan())
|
|
{
|
|
g.dim_lines.push([f32::NAN; 3]);
|
|
}
|
|
for point in lines {
|
|
if point[0].is_nan() {
|
|
if !g.dim_lines.last().is_some_and(|point| point[0].is_nan()) {
|
|
g.dim_lines.push([f32::NAN; 3]);
|
|
}
|
|
continue;
|
|
}
|
|
let point = transform(point);
|
|
g.dim_lines.push([point.x, point.y, point.z]);
|
|
}
|
|
for triangle in fill.chunks_exact(3) {
|
|
push_tri(
|
|
&mut g.arrow_fill,
|
|
transform(&triangle[0]),
|
|
transform(&triangle[1]),
|
|
transform(&triangle[2]),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn add_segment(points: &mut Vec<[f32; 3]>, a: Vec3, b: Vec3) {
|
|
if !points.is_empty() {
|
|
points.push([f32::NAN, f32::NAN, f32::NAN]);
|
|
}
|
|
points.push([a.x, a.y, a.z]);
|
|
points.push([b.x, b.y, b.z]);
|
|
}
|
|
|
|
pub(crate) fn add_polyline(points: &mut Vec<[f32; 3]>, polyline: &[Vec3]) {
|
|
if polyline.len() < 2 {
|
|
return;
|
|
}
|
|
if !points.is_empty() {
|
|
points.push([f32::NAN, f32::NAN, f32::NAN]);
|
|
}
|
|
points.extend(polyline.iter().map(|p| [p.x, p.y, p.z]));
|
|
}
|
|
|
|
/// Returns the text position of a dimension in DXF world-space (f64, no offset applied).
|
|
/// Used when building a synthetic Text entity so tessellate() can apply world_offset itself.
|
|
/// When the saved `text_middle_point` is zero (no explicit point was written),
|
|
/// computes a fallback from the dim geometry and applies DIMTAD/DIMGAP.
|
|
pub(crate) fn normalized_or(v: Vec3, fallback: Vec3) -> Vec3 {
|
|
if v.length_squared() <= 1e-12 {
|
|
fallback
|
|
} else {
|
|
v.normalize()
|
|
}
|
|
}
|