forked from 1b-agent/opencadstudio-upstream
Render the MTEXT background (never drawn before — the flags were only shown read-only in the panel) and make it editable. - Text arm emits an opaque fill_tris rect behind the glyphs: flag 0x01 uses the entity background colour, 0x02 uses the canvas colour (a wipeout-style mask). Box = glyph bounds padded by (background_scale - 1) × text height. - Text frame (flag 0x10) draws a closed rectangle in the text colour around the same box. - Threaded `bg_color` into `tessellate()` (+ all callers and the MultiLeaderTess trait) so the mask can use the drawing-window colour. - Properties: "Background mask" is a Off / Fill / Mask dropdown, "Background color" is its own colour picker (independent of the entity colour — new PropBgColor* messages; picking a colour switches to Fill mode), and "Text frame" is a checkbox toggle. Edits re-tessellate so the change shows live. Renders via the face3d fill path, so the fill shows with FILLMODE on (same as MLEADER background fill); the frame is a stroke and always shows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1273 lines
53 KiB
Rust
1273 lines
53 KiB
Rust
// Tessellation — convert acadrust EntityType to GPU-ready WireModel or MeshModel.
|
||
//
|
||
// Flow:
|
||
// EntityType
|
||
// ↓ acad_to_truck::convert()
|
||
// TruckEntity { object: TruckObject, snap_pts, tangent_geoms, key_vertices }
|
||
// ↓ truck_tess::tessellate_*()
|
||
// TruckTessResult::Lines → WireModel
|
||
// TruckTessResult::Point → WireModel (small cross)
|
||
// TruckTessResult::Mesh → MeshModel
|
||
// TruckObject::Text → one WireModel per glyph stroke (elevation from entity Z)
|
||
//
|
||
// Entities not handled by acad_to_truck (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_truck::{convert, TruckObject};
|
||
use crate::scene::convert::truck_tess::{
|
||
tessellate_edge, tessellate_vertex, tessellate_wire, TruckTessResult,
|
||
};
|
||
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])
|
||
}
|
||
|
||
/// 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.
|
||
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)
|
||
}
|
||
|
||
// ── 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,
|
||
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],
|
||
) -> Vec<WireModel> {
|
||
let color = if selected {
|
||
WireModel::SELECTED
|
||
} else {
|
||
entity_color
|
||
};
|
||
let name = handle.value().to_string();
|
||
|
||
// Determine effective annotation scale for this entity.
|
||
//
|
||
// AutoCAD's R2007+ "annotative" system marks objects via extension-
|
||
// dictionary records or "AcAnnoPO" / "AcAnnotativeData" xdata. Only
|
||
// entities so marked should be auto-scaled by the viewport's
|
||
// paper-scale; everything else is treated as manually pre-scaled
|
||
// (old DXF/DWG convention with $DIMSCALE and oversized text).
|
||
//
|
||
// Default: NOT annotative (anno_scale = 1.0). Opt-in via explicit
|
||
// xdata marker. Files that mark every entity annotative are rare; the
|
||
// pre-R2007 manual-scale convention is far more common in field data.
|
||
let anno_scale = {
|
||
let xdata = &entity.common().extended_data;
|
||
let is_annotative = xdata.get_record("AcAnnoPO").is_some()
|
||
|| xdata.get_record("AcAnnotativeData").is_some();
|
||
if is_annotative {
|
||
anno_scale
|
||
} else {
|
||
1.0
|
||
}
|
||
};
|
||
|
||
// 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,
|
||
)];
|
||
}
|
||
|
||
// ── Try the truck path first ───────────────────────────────────────────
|
||
// Relative-PDSIZE points size their glyph from the current zoom so they
|
||
// stay a roughly constant on-screen size; otherwise the header-driven path.
|
||
let te = crate::entities::point::relative_truck(entity, document, world_per_pixel)
|
||
.or_else(|| 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.
|
||
TruckObject::Text(stroke_groups) => {
|
||
let entity_zf = entity_z(entity) as f64;
|
||
let elev_v = entity_zf;
|
||
|
||
// anno_scale anchors at the first group's origin so multi-line
|
||
// MText lines spread apart correctly as they grow.
|
||
let ref_origin = 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)
|
||
struct TextBin {
|
||
color: Option<[f32; 3]>,
|
||
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]>, bins: &mut Vec<TextBin>, firsts: &mut Vec<bool>| -> usize {
|
||
if let Some(i) = bins.iter().position(|b| b.color == key) {
|
||
i
|
||
} else {
|
||
bins.push(TextBin {
|
||
color: key,
|
||
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;
|
||
// With SDF text on, glyphs render as textured quads carried on
|
||
// this entity's own wire (built below) — skip the stroke
|
||
// geometry so text is not double-drawn. `bins` stays empty, so
|
||
// the empty-wire path emits the insertion snap point and the
|
||
// glyph quads. Applies to every TEXT/MTEXT the tessellator
|
||
// sees, whether a top-level entity, a dimension's text, or a
|
||
// block-internal string — each owns its glyphs, no central
|
||
// collector, no re-explosion.
|
||
let sdf_text = crate::scene::text::sdf_atlas::sdf_text_enabled()
|
||
&& matches!(entity, EntityType::Text(_) | EntityType::MText(_));
|
||
for group in stroke_groups.iter().filter(|_| !sdf_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 bi = find_or_make(bin_key, &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 ──────────────────────────────────────
|
||
// When SDF text is on, build this 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 sdf_text {
|
||
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 };
|
||
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 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.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();
|
||
|
||
// 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();
|
||
// Pick box straight from the rendered glyph quads — the true
|
||
// text extent. entity_aabb is unreliable for MTEXT (its box
|
||
// sits beside the laid-out glyphs), so we derive the AABB
|
||
// from `sdf_verts` and stop the generic stamp from
|
||
// clobbering it (tess.rs guards on `text_verts`).
|
||
let text_aabb = if sdf_text && !sdf_verts.is_empty() {
|
||
// Accumulate in f64 (reconstruct high + low) so the box
|
||
// stays accurate at UTM scale, then cast the corners to
|
||
// f32 once for storage.
|
||
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
|
||
};
|
||
// MTEXT background fill / mask: an opaque rectangle behind
|
||
// the glyphs, emitted first so it renders under the text.
|
||
// Flag 0x01 → the entity's background-fill colour; 0x02 →
|
||
// the drawing-window (canvas) colour, which masks geometry
|
||
// behind the text like a wipeout. Box = glyph bounds padded
|
||
// by (background_scale - 1) × text height.
|
||
if sdf_text && 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 {
|
||
// Padded box shared by the fill and the frame.
|
||
let th = (m.height * anno) as f32;
|
||
let pad = ((m.background_scale as f32) - 1.0).max(0.0) * th;
|
||
let [bnx, bny, bxx, bxy] = text_aabb;
|
||
let (l, b, r, t) = (
|
||
(bnx - pad) as f64,
|
||
(bny - pad) as f64,
|
||
(bxx + pad) as f64,
|
||
(bxy + pad) as f64,
|
||
);
|
||
// Fill / mask — two triangles behind the glyphs.
|
||
if has_fill {
|
||
let fill_color = if m.background_fill_flags & 0x01 != 0 {
|
||
color_or_inherit(&m.background_color, bg_color)
|
||
} else {
|
||
bg_color
|
||
};
|
||
let corners = [[l, b], [r, b], [r, t], [l, t]];
|
||
let mut ft = Vec::with_capacity(6);
|
||
let mut ftl = Vec::with_capacity(6);
|
||
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 {
|
||
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,
|
||
vp_scissor: None,
|
||
fill_tris: ft,
|
||
fill_tris_low: ftl,
|
||
});
|
||
}
|
||
// Text frame — a closed rectangle in the text
|
||
// colour around the same box.
|
||
if has_frame {
|
||
let loop_xy =
|
||
[[l, b], [r, b], [r, t], [l, t], [l, b]];
|
||
let mut fp = Vec::with_capacity(5);
|
||
let mut fpl = Vec::with_capacity(5);
|
||
for &[x, y] in &loop_xy {
|
||
let (h, lo) = split_ds_xyz(x, y, elev_v);
|
||
fp.push(h);
|
||
fpl.push(lo);
|
||
}
|
||
wires.push(WireModel {
|
||
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,
|
||
vp_scissor: None,
|
||
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_text
|
||
&& !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 {
|
||
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,
|
||
vp_scissor: None,
|
||
fill_tris: vec![],
|
||
fill_tris_low: Vec::new(),
|
||
});
|
||
}
|
||
wires.push(WireModel {
|
||
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,
|
||
vp_scissor: None,
|
||
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,
|
||
};
|
||
|
||
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 {
|
||
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,
|
||
snap_pts: snap,
|
||
tangent_geoms: tangents,
|
||
aci: 0,
|
||
key_vertices: keys,
|
||
aabb: WireModel::UNBOUNDED_AABB,
|
||
plinegen: true,
|
||
vp_scissor: None,
|
||
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 {
|
||
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,
|
||
vp_scissor: None,
|
||
fill_tris: bin.fill_tris,
|
||
fill_tris_low: bin.fill_tris_low,
|
||
});
|
||
}
|
||
}
|
||
|
||
if out.is_empty() {
|
||
out.push(WireModel {
|
||
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,
|
||
vp_scissor: None,
|
||
fill_tris: vec![],
|
||
fill_tris_low: Vec::new(),
|
||
});
|
||
}
|
||
return out;
|
||
}
|
||
|
||
// ── Standard topology objects ─────────────────────────────────
|
||
TruckObject::Point(v) => {
|
||
let result = tessellate_vertex(&v);
|
||
match result {
|
||
TruckTessResult::Point([x, y, z], [xl, yl, zl]) => {
|
||
// 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 {
|
||
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,
|
||
vp_scissor: None,
|
||
fill_tris: vec![],
|
||
fill_tris_low: Vec::new(),
|
||
}];
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
TruckObject::Curve(e) => {
|
||
if let TruckTessResult::Lines(points, points_low) =
|
||
tessellate_edge(&e)
|
||
{
|
||
let snap_pts = te.snap_pts;
|
||
let key_vertices: Vec<[f64; 3]> = te
|
||
.key_vertices
|
||
.into_iter()
|
||
.map(|[x, y, z]| [x, y, z])
|
||
.collect();
|
||
return vec![WireModel {
|
||
text_verts: Vec::new(),
|
||
name,
|
||
points,
|
||
points_low,
|
||
color,
|
||
selected,
|
||
pattern_length,
|
||
pattern,
|
||
line_weight_px,
|
||
snap_pts,
|
||
tangent_geoms: te.tangent_geoms,
|
||
aci: 0,
|
||
key_vertices,
|
||
aabb: WireModel::UNBOUNDED_AABB,
|
||
plinegen: true,
|
||
vp_scissor: None,
|
||
fill_tris: vec![],
|
||
fill_tris_low: Vec::new(),
|
||
}];
|
||
}
|
||
}
|
||
|
||
TruckObject::Contour(w) => {
|
||
if let TruckTessResult::Lines(points, points_low) =
|
||
tessellate_wire(&w)
|
||
{
|
||
let snap_pts = te.snap_pts;
|
||
let key_vertices: Vec<[f64; 3]> = te
|
||
.key_vertices
|
||
.into_iter()
|
||
.map(|[x, y, z]| [x, y, z])
|
||
.collect();
|
||
return vec![WireModel {
|
||
text_verts: Vec::new(),
|
||
name,
|
||
points,
|
||
points_low,
|
||
color,
|
||
selected,
|
||
pattern_length,
|
||
pattern,
|
||
line_weight_px,
|
||
snap_pts,
|
||
tangent_geoms: te.tangent_geoms,
|
||
aci: 0,
|
||
key_vertices,
|
||
aabb: WireModel::UNBOUNDED_AABB,
|
||
plinegen: true,
|
||
vp_scissor: None,
|
||
fill_tris: vec![],
|
||
fill_tris_low: Vec::new(),
|
||
}];
|
||
}
|
||
}
|
||
|
||
TruckObject::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);
|
||
let mut out = Vec::new();
|
||
let mut is_first = true;
|
||
|
||
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())
|
||
};
|
||
out.push(WireModel {
|
||
text_verts: Vec::new(),
|
||
name: name.clone(),
|
||
points: local_pts,
|
||
points_low: local_pts_low,
|
||
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,
|
||
vp_scissor: None,
|
||
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 {
|
||
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,
|
||
vp_scissor: None,
|
||
fill_tris,
|
||
fill_tris_low,
|
||
});
|
||
}
|
||
|
||
if out.is_empty() {
|
||
out.push(WireModel {
|
||
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,
|
||
vp_scissor: None,
|
||
fill_tris: vec![],
|
||
fill_tris_low: Vec::new(),
|
||
});
|
||
}
|
||
|
||
return out;
|
||
}
|
||
|
||
TruckObject::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();
|
||
return vec![WireModel {
|
||
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,
|
||
vp_scissor: None,
|
||
aabb: WireModel::UNBOUNDED_AABB,
|
||
fill_tris: vec![],
|
||
fill_tris_low: Vec::new(),
|
||
}];
|
||
}
|
||
|
||
TruckObject::Volume(_) => {
|
||
// Solid3D / Region / Body → mesh tessellation lives in
|
||
// `solid3d_tess`. As a wire fallback, render the pre-computed
|
||
// edge wires stored in the entity when present (e.g. from
|
||
// SOLVIEW output or when the SAT kernel cannot parse the
|
||
// ACIS data).
|
||
let wire_pts = solid_wire_fallback(entity);
|
||
let mut wm = WireModel::solid_f64(name, wire_pts, color, selected);
|
||
// Add insertion snap at point_of_reference.
|
||
if let Some(p) = crate::entities::solid3d::point_of_reference(entity) {
|
||
let sp = glam::DVec3::new(p.x, p.y, p.z);
|
||
wm.snap_pts.push((sp, SnapHint::Insertion));
|
||
}
|
||
return vec![wm];
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Fallback for Viewport / Insert / Hatch / Ole2Frame ────────────────
|
||
let (points_f64, snap_pts, tangent_geoms, key_vertices) =
|
||
fallback_geometry(entity);
|
||
// `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();
|
||
vec![WireModel {
|
||
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,
|
||
vp_scissor: None,
|
||
fill_tris: vec![],
|
||
fill_tris_low: Vec::new(),
|
||
}]
|
||
}
|
||
|
||
|
||
|
||
#[derive(Clone, Copy)]
|
||
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 },
|
||
}
|
||
|
||
pub(crate) fn arrow_from_block(
|
||
doc: &CadDocument,
|
||
handle: acadrust::types::Handle,
|
||
dimasz: f32,
|
||
) -> ArrowKind {
|
||
let name = if handle.is_null() {
|
||
None
|
||
} else {
|
||
doc.block_records
|
||
.iter()
|
||
.find(|b| b.handle == handle)
|
||
.map(|b| b.name.as_str())
|
||
};
|
||
arrow_from_block_name(name, dimasz)
|
||
}
|
||
|
||
fn arrow_from_block_name(name: Option<&str>, dimasz: f32) -> ArrowKind {
|
||
// AutoCAD's standard arrow blocks are prefixed with "_" (e.g. "_OPEN").
|
||
// Strip the prefix, upper-case, and switch on canonical names. Unknown
|
||
// / missing names default to ClosedFilled.
|
||
let n = name
|
||
.map(|s| s.trim().trim_start_matches('_').to_ascii_uppercase())
|
||
.unwrap_or_default();
|
||
match n.as_str() {
|
||
"" | "CLOSEDFILLED" => ArrowKind::Triangle {
|
||
size: dimasz,
|
||
filled: true,
|
||
size_mul: 1.0,
|
||
},
|
||
"CLOSED" | "CLOSEDBLANK" => ArrowKind::Triangle {
|
||
size: dimasz,
|
||
filled: false,
|
||
size_mul: 1.0,
|
||
},
|
||
"SMALL" => ArrowKind::Triangle {
|
||
size: dimasz,
|
||
filled: true,
|
||
size_mul: 0.5,
|
||
},
|
||
"OPEN" => ArrowKind::Open {
|
||
size: dimasz,
|
||
half_angle: 9.5_f32.to_radians(),
|
||
},
|
||
"OPEN30" => ArrowKind::Open {
|
||
size: dimasz,
|
||
half_angle: 15.0_f32.to_radians(),
|
||
},
|
||
"OPEN90" => ArrowKind::Open {
|
||
size: dimasz,
|
||
half_angle: 45.0_f32.to_radians(),
|
||
},
|
||
"DOT" => ArrowKind::Dot {
|
||
size: dimasz,
|
||
filled: true,
|
||
},
|
||
"DOTSMALL" => ArrowKind::Dot {
|
||
size: dimasz * 0.5,
|
||
filled: true,
|
||
},
|
||
"DOTBLANK" => ArrowKind::Dot {
|
||
size: dimasz,
|
||
filled: false,
|
||
},
|
||
"DOTSMALLBLANK" => ArrowKind::Dot {
|
||
size: dimasz * 0.5,
|
||
filled: false,
|
||
},
|
||
"ORIGIN" | "ORIGIN2" | "ORIGININDICATOR" | "ORIGININDICATOR2" => {
|
||
ArrowKind::Origin { size: dimasz }
|
||
}
|
||
"OBLIQUE" | "ARCHTICK" => ArrowKind::Tick { size: dimasz },
|
||
"BOXFILLED" => ArrowKind::Box_ {
|
||
size: dimasz,
|
||
filled: true,
|
||
},
|
||
"BOXBLANK" | "BOX" => ArrowKind::Box_ {
|
||
size: dimasz,
|
||
filled: false,
|
||
},
|
||
"DATUMFILLED" | "DATUMTRIANGLEFILLED" => ArrowKind::Datum {
|
||
size: dimasz,
|
||
filled: true,
|
||
},
|
||
"DATUMBLANK" | "DATUMTRIANGLE" => ArrowKind::Datum {
|
||
size: dimasz,
|
||
filled: false,
|
||
},
|
||
"NONE" => ArrowKind::None,
|
||
// INTEGRAL and other complex glyphs aren't reproduced here; fall through.
|
||
_ => ArrowKind::Triangle {
|
||
size: dimasz,
|
||
filled: true,
|
||
size_mul: 1.0,
|
||
},
|
||
}
|
||
}
|
||
|
||
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,
|
||
}
|
||
}
|
||
|
||
// ── 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 pts = solid_wire_fallback(entity);
|
||
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,
|
||
));
|
||
}
|
||
(pts, snap, vec![], vec![])
|
||
}
|
||
_ => {
|
||
let s = 0.5_f64;
|
||
(vec![[-s, 0.0, 0.0], [s, 0.0, 0.0]], vec![], vec![], vec![])
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Extract pre-computed edge-wire points from Solid3D / Region / Body entities.
|
||
///
|
||
/// AutoCAD stores explicit wire geometry (from SOLVIEW / 3DPLOT) alongside the
|
||
/// ACIS data. We use this as a visible fallback when the SAT tessellator
|
||
/// produces no mesh (e.g. binary SAB data or unsupported geometry).
|
||
fn solid_wire_fallback(entity: &EntityType) -> Vec<[f64; 3]> {
|
||
let Some(wires) = crate::entities::solid3d::fallback_wires(entity) else {
|
||
return vec![];
|
||
};
|
||
if wires.is_empty() {
|
||
return vec![];
|
||
}
|
||
|
||
let mut pts: Vec<[f64; 3]> = Vec::new();
|
||
for wire in wires {
|
||
if wire.points.len() < 2 {
|
||
continue;
|
||
}
|
||
for v in &wire.points {
|
||
pts.push([v.x, v.y, v.z]);
|
||
}
|
||
// NaN sentinel separates distinct wire segments.
|
||
pts.push([f64::NAN, f64::NAN, f64::NAN]);
|
||
}
|
||
pts
|
||
}
|
||
|
||
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
|
||
// AutoCAD's standard ClosedFilled 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 (matches AutoCAD's DIMTSZ semantics).
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
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 (i.e. AutoCAD never wrote one),
|
||
/// 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()
|
||
}
|
||
}
|
||
|