feat(text): SDF for tolerance + complex-linetype text; per-group model; pick cycle
Extend SDF text to the remaining text kinds and make the Text arm decide SDF per group instead of per entity type. - Text arm: a stroke group renders as SDF glyph quads when it carries a GlyphRun (strokes suppressed); a run-less group keeps its strokes. TEXT / MTEXT / dim / block / mleader are unchanged (all run-groups). A composite Text object can now mix geometry + text: its run-less groups stay strokes and its run-groups become SDF. The glyph-bounds AABB is hoisted so both the bins-empty and bins-non-empty paths stamp it on the text wire. - Tolerance: refactored to emit the feature-control frame (border, separators, dividers) as run-less stroke groups and each cell's text as a run-group, so the frame stays crisp geometry while the values/symbols render as SDF text. - Complex linetypes: embedded LtSeg::Text renders as SDF glyph quads on an extra text wire (glyph-bounds AABB) instead of stroke polylines. - click_hits_all: include SDF text boxes so selection cycling steps through text too, ranked after real geometry. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
96181563fb
commit
d9a2c8245c
4 changed files with 263 additions and 89 deletions
|
|
@ -3,7 +3,7 @@ use acadrust::entities::Tolerance;
|
||||||
use crate::command::EntityTransform;
|
use crate::command::EntityTransform;
|
||||||
use crate::entities::common::{edit_prop as edit, ro_prop as ro, square_grip};
|
use crate::entities::common::{edit_prop as edit, ro_prop as ro, square_grip};
|
||||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
|
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
|
||||||
use crate::scene::convert::acad_to_truck::{TextStroke, TruckEntity, TruckObject};
|
use crate::scene::convert::acad_to_truck::{GlyphRun, TextStroke, TruckEntity, TruckObject};
|
||||||
use crate::scene::model::object::{GripApply, GripDef, PropSection};
|
use crate::scene::model::object::{GripApply, GripDef, PropSection};
|
||||||
use crate::scene::model::wire_model::SnapHint;
|
use crate::scene::model::wire_model::SnapHint;
|
||||||
use crate::scene::text::lff;
|
use crate::scene::text::lff;
|
||||||
|
|
@ -103,25 +103,34 @@ fn gdt_char_to_ascii(c: char) -> &'static str {
|
||||||
|
|
||||||
// ── Feature-control frame builder ─────────────────────────────────────────────
|
// ── Feature-control frame builder ─────────────────────────────────────────────
|
||||||
|
|
||||||
/// Tessellate a Tolerance entity into CXF-style polyline output.
|
/// One text cell of a feature-control frame, ready to become a `TextStroke`
|
||||||
|
/// with a `GlyphRun` so the cell text can render as SDF glyph quads (or, when
|
||||||
|
/// SDF is off, from `strokes`). `origin` is relative to the tolerance insertion
|
||||||
|
/// point (already rotated); `strokes` are the glyph polylines rotated about the
|
||||||
|
/// origin (no origin translation — the wire-builder adds the origin).
|
||||||
|
struct TolCell {
|
||||||
|
text: String,
|
||||||
|
origin: [f32; 2],
|
||||||
|
strokes: Vec<Vec<[f32; 2]>>,
|
||||||
|
height: f32,
|
||||||
|
rotation: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tessellate a Tolerance entity's feature-control frame.
|
||||||
///
|
///
|
||||||
/// Returns (`box_lines`, `text_strokes`) where:
|
/// Returns (`box_strokes`, `cells`):
|
||||||
/// - `box_lines` — 3-D line segments forming the outer border and dividers
|
/// - `box_strokes` — the outer border, row separators and column dividers as
|
||||||
/// - `text_strokes` — 2-D polylines from the CXF tessellator
|
/// 2-D polylines (rotated; run-less, so they always render as strokes)
|
||||||
///
|
/// - `cells` — one [`TolCell`] per non-empty cell, carrying its text + a
|
||||||
/// Since TruckObject has either Lines (3-D) or Text (2-D CXF), we render the
|
/// `GlyphRun` so the cell renders as SDF text (frame stays geometry).
|
||||||
/// box frame via a separate Lines path (added by the caller) and the text cells
|
fn tessellate_tolerance(tol: &Tolerance) -> (Vec<Vec<[f32; 2]>>, Vec<TolCell>) {
|
||||||
/// via the Text path. For simplicity this function packs everything into a
|
|
||||||
/// single Vec<Vec<[f32;2]>> by projecting the box lines to 2-D and using NaN
|
|
||||||
/// polylines as separators — exactly what the Text wire-builder does.
|
|
||||||
fn tessellate_tolerance(tol: &Tolerance) -> Vec<Vec<[f32; 2]>> {
|
|
||||||
if tol.text.is_empty() {
|
if tol.text.is_empty() {
|
||||||
return vec![];
|
return (vec![], vec![]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let rows = parse_gdt_rows(&tol.text);
|
let rows = parse_gdt_rows(&tol.text);
|
||||||
if rows.is_empty() {
|
if rows.is_empty() {
|
||||||
return vec![];
|
return (vec![], vec![]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Metrics ──────────────────────────────────────────────────────────
|
// ── Metrics ──────────────────────────────────────────────────────────
|
||||||
|
|
@ -162,10 +171,11 @@ fn tessellate_tolerance(tol: &Tolerance) -> Vec<Vec<[f32; 2]>> {
|
||||||
// Rotate only; origin is kept as f64 and applied later with full precision.
|
// Rotate only; origin is kept as f64 and applied later with full precision.
|
||||||
let rot = |x: f32, y: f32| -> [f32; 2] { [x * ca - y * sa, x * sa + y * ca] };
|
let rot = |x: f32, y: f32| -> [f32; 2] { [x * ca - y * sa, x * sa + y * ca] };
|
||||||
|
|
||||||
let mut out: Vec<Vec<[f32; 2]>> = Vec::new();
|
let mut box_out: Vec<Vec<[f32; 2]>> = Vec::new();
|
||||||
|
let mut cells: Vec<TolCell> = Vec::new();
|
||||||
|
|
||||||
// ── Outer border ──────────────────────────────────────────────────────
|
// ── Outer border ──────────────────────────────────────────────────────
|
||||||
out.push(vec![
|
box_out.push(vec![
|
||||||
rot(0.0, 0.0),
|
rot(0.0, 0.0),
|
||||||
rot(total_w, 0.0),
|
rot(total_w, 0.0),
|
||||||
rot(total_w, total_h),
|
rot(total_w, total_h),
|
||||||
|
|
@ -176,7 +186,7 @@ fn tessellate_tolerance(tol: &Tolerance) -> Vec<Vec<[f32; 2]>> {
|
||||||
// ── Row separators ─────────────────────────────────────────────────────
|
// ── Row separators ─────────────────────────────────────────────────────
|
||||||
for ri in 1..rows.len() {
|
for ri in 1..rows.len() {
|
||||||
let y = cell_h * ri as f32;
|
let y = cell_h * ri as f32;
|
||||||
out.push(vec![rot(0.0, y), rot(total_w, y)]);
|
box_out.push(vec![rot(0.0, y), rot(total_w, y)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Column dividers ────────────────────────────────────────────────────
|
// ── Column dividers ────────────────────────────────────────────────────
|
||||||
|
|
@ -187,12 +197,16 @@ fn tessellate_tolerance(tol: &Tolerance) -> Vec<Vec<[f32; 2]>> {
|
||||||
if ci + 1 < rows[ri].len() {
|
if ci + 1 < rows[ri].len() {
|
||||||
let y0 = cell_h * ri as f32;
|
let y0 = cell_h * ri as f32;
|
||||||
let y1 = y0 + cell_h;
|
let y1 = y0 + cell_h;
|
||||||
out.push(vec![rot(x_cursor, y0), rot(x_cursor, y1)]);
|
box_out.push(vec![rot(x_cursor, y0), rot(x_cursor, y1)]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Text content per cell ─────────────────────────────────────────────
|
// ── Text content per cell ─────────────────────────────────────────────
|
||||||
|
// Each cell becomes a TolCell: its origin is the (rotated) cell position
|
||||||
|
// relative to the insertion point; its `strokes` are the glyph polylines
|
||||||
|
// rotated about that origin (used only when SDF is off). The GlyphRun the
|
||||||
|
// caller attaches lets the cell render as SDF text.
|
||||||
for (ri, row) in rows.iter().enumerate() {
|
for (ri, row) in rows.iter().enumerate() {
|
||||||
let row_y = cell_h * ri as f32 + gap;
|
let row_y = cell_h * ri as f32 + gap;
|
||||||
let mut cell_x = 0.0_f32;
|
let mut cell_x = 0.0_f32;
|
||||||
|
|
@ -205,24 +219,28 @@ fn tessellate_tolerance(tol: &Tolerance) -> Vec<Vec<[f32; 2]>> {
|
||||||
if !cell.is_empty() {
|
if !cell.is_empty() {
|
||||||
let text_w = cell.len() as f32 * char_w;
|
let text_w = cell.len() as f32 * char_w;
|
||||||
let tx = cell_x + (cw - text_w) * 0.5;
|
let tx = cell_x + (cw - text_w) * 0.5;
|
||||||
// Tessellate text in local frame then transform
|
|
||||||
let (local_strokes, _) =
|
let (local_strokes, _) =
|
||||||
lff::tessellate_text_ex([0.0, 0.0], h, 0.0, 1.0, 0.0, "txt", cell);
|
lff::tessellate_text_ex([0.0, 0.0], h, 0.0, 1.0, 0.0, "txt", cell);
|
||||||
for polyline in local_strokes {
|
// Glyph polylines rotated about the cell origin (no origin
|
||||||
let transformed: Vec<[f32; 2]> = polyline
|
// translation — the wire-builder adds `origin`).
|
||||||
.into_iter()
|
let strokes: Vec<Vec<[f32; 2]>> = local_strokes
|
||||||
.map(|[px, py]| rot(px + tx, py + row_y))
|
.into_iter()
|
||||||
.collect();
|
.map(|pl| pl.into_iter().map(|[px, py]| rot(px, py)).collect())
|
||||||
if !transformed.is_empty() {
|
.filter(|pl: &Vec<[f32; 2]>| !pl.is_empty())
|
||||||
out.push(transformed);
|
.collect();
|
||||||
}
|
cells.push(TolCell {
|
||||||
}
|
text: cell.clone(),
|
||||||
|
origin: rot(tx, row_y),
|
||||||
|
strokes,
|
||||||
|
height: h,
|
||||||
|
rotation: angle,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
cell_x += cw;
|
cell_x += cw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
out
|
(box_out, cells)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── TruckConvertible ──────────────────────────────────────────────────────────
|
// ── TruckConvertible ──────────────────────────────────────────────────────────
|
||||||
|
|
@ -240,17 +258,46 @@ impl TruckConvertible for Tolerance {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Build the feature-control frame in local space; origin stored as f64.
|
// Build the feature-control frame in local space; origin stored as f64.
|
||||||
let strokes = tessellate_tolerance(self);
|
let (box_strokes, cells) = tessellate_tolerance(self);
|
||||||
let origin = [self.insertion_point.x, self.insertion_point.y];
|
let ins = [self.insertion_point.x, self.insertion_point.y];
|
||||||
|
|
||||||
Some(TruckEntity {
|
// Frame geometry first (run-less → always strokes; also the anchor
|
||||||
object: TruckObject::Text(vec![TextStroke {
|
// group so its origin = the insertion point), then one run-group per
|
||||||
strokes,
|
// text cell so the cell text renders as SDF glyphs (or strokes when
|
||||||
origin,
|
// SDF is off).
|
||||||
|
let mut groups: Vec<TextStroke> = Vec::with_capacity(1 + cells.len());
|
||||||
|
if !box_strokes.is_empty() {
|
||||||
|
groups.push(TextStroke {
|
||||||
|
strokes: box_strokes,
|
||||||
|
origin: ins,
|
||||||
color: None,
|
color: None,
|
||||||
fill_tris: vec![],
|
fill_tris: vec![],
|
||||||
run: None,
|
run: None,
|
||||||
}]),
|
});
|
||||||
|
}
|
||||||
|
for cell in cells {
|
||||||
|
groups.push(TextStroke {
|
||||||
|
strokes: cell.strokes,
|
||||||
|
origin: [ins[0] + cell.origin[0] as f64, ins[1] + cell.origin[1] as f64],
|
||||||
|
color: None,
|
||||||
|
fill_tris: vec![],
|
||||||
|
run: Some(GlyphRun {
|
||||||
|
text: cell.text,
|
||||||
|
font: "txt".to_string(),
|
||||||
|
height: cell.height,
|
||||||
|
rotation: cell.rotation,
|
||||||
|
width_factor: 1.0,
|
||||||
|
oblique: 0.0,
|
||||||
|
tracking: 0.0,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if groups.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(TruckEntity {
|
||||||
|
object: TruckObject::Text(groups),
|
||||||
snap_pts: vec![(snap_pt, SnapHint::Insertion)],
|
snap_pts: vec![(snap_pt, SnapHint::Insertion)],
|
||||||
tangent_geoms: vec![],
|
tangent_geoms: vec![],
|
||||||
key_vertices: vec![],
|
key_vertices: vec![],
|
||||||
|
|
|
||||||
|
|
@ -202,17 +202,16 @@ pub fn tessellate(
|
||||||
};
|
};
|
||||||
|
|
||||||
let anno = anno_scale as f64;
|
let anno = anno_scale as f64;
|
||||||
// With SDF text on, glyphs render as textured quads carried on
|
// SDF text is decided PER GROUP by whether the group carries a
|
||||||
// this entity's own wire (built below) — skip the stroke
|
// `GlyphRun`: a run-group renders as textured quads (built
|
||||||
// geometry so text is not double-drawn. `bins` stays empty, so
|
// below) so its strokes are suppressed; a run-less group keeps
|
||||||
// the empty-wire path emits the insertion snap point and the
|
// its strokes. TEXT / MTEXT / dim / block / mleader text are all
|
||||||
// glyph quads. Applies to every TEXT/MTEXT the tessellator
|
// run-groups → fully SDF; a composite object that packs geometry
|
||||||
// sees, whether a top-level entity, a dimension's text, or a
|
// and text into one Text object (a tolerance frame: box lines =
|
||||||
// block-internal string — each owns its glyphs, no central
|
// run-less, cell text = run-groups) keeps the geometry as
|
||||||
// collector, no re-explosion.
|
// strokes and draws only the text as SDF.
|
||||||
let sdf_text = crate::scene::text::sdf_atlas::sdf_text_enabled()
|
let sdf_on = crate::scene::text::sdf_atlas::sdf_text_enabled();
|
||||||
&& matches!(entity, EntityType::Text(_) | EntityType::MText(_));
|
for group in stroke_groups.iter().filter(|g| !(sdf_on && g.run.is_some())) {
|
||||||
for group in stroke_groups.iter().filter(|_| !sdf_text) {
|
|
||||||
let lx_v = group.origin[0];
|
let lx_v = group.origin[0];
|
||||||
let ly_v = group.origin[1];
|
let ly_v = group.origin[1];
|
||||||
let slx_v = (lx_v - ref_lx_v) * anno + ref_lx_v;
|
let slx_v = (lx_v - ref_lx_v) * anno + ref_lx_v;
|
||||||
|
|
@ -255,7 +254,7 @@ pub fn tessellate(
|
||||||
// tess memo, hit-materialisation and (for block content) the
|
// tess memo, hit-materialisation and (for block content) the
|
||||||
// block-expand transform — no separate document-wide collector.
|
// block-expand transform — no separate document-wide collector.
|
||||||
let mut sdf_verts: Vec<crate::scene::pipeline::text_gpu::TextVertex> = Vec::new();
|
let mut sdf_verts: Vec<crate::scene::pipeline::text_gpu::TextVertex> = Vec::new();
|
||||||
if sdf_text {
|
if sdf_on {
|
||||||
if let Ok(mut atlas) = crate::scene::text::sdf_atlas::text_atlas().lock() {
|
if let Ok(mut atlas) = crate::scene::text::sdf_atlas::text_atlas().lock() {
|
||||||
// Selection tints the whole run; otherwise inline `\C`
|
// Selection tints the whole run; otherwise inline `\C`
|
||||||
// colours (bin key) win, falling back to entity colour.
|
// colours (bin key) win, falling back to entity colour.
|
||||||
|
|
@ -302,42 +301,43 @@ pub fn tessellate(
|
||||||
.map(|[x, y, z]| [x, y, z])
|
.map(|[x, y, z]| [x, y, z])
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
// 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 derive the AABB from
|
||||||
|
// `sdf_verts` (accumulate in f64, reconstruct high+low, then cast
|
||||||
|
// to f32 once) and stop the generic stamp clobbering it (tess.rs
|
||||||
|
// guards on `text_verts`). Computed here so both the bins-empty
|
||||||
|
// and the bins-non-empty (tolerance box + text) paths can stamp
|
||||||
|
// it on the SDF text wire.
|
||||||
|
let text_aabb = if sdf_on && !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
|
// Empty input (no glyphs) → emit a single empty wire so the
|
||||||
// entity still has a hit-test target via snap_pts. With SDF on
|
// entity still has a hit-test target via snap_pts. With SDF on
|
||||||
// this is the normal path (strokes suppressed) and the wire
|
// this is the normal path (strokes suppressed) and the wire
|
||||||
// also carries the glyph quads built above.
|
// also carries the glyph quads built above.
|
||||||
if bins.is_empty() {
|
if bins.is_empty() {
|
||||||
let mut wires: Vec<WireModel> = Vec::new();
|
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
|
// MTEXT background fill / mask: an opaque rectangle behind
|
||||||
// the glyphs, emitted first so it renders under the text.
|
// the glyphs, emitted first so it renders under the text.
|
||||||
// Flag 0x01 → the entity's background-fill colour; 0x02 →
|
// Flag 0x01 → the entity's background-fill colour; 0x02 →
|
||||||
// the drawing-window (canvas) colour, which masks geometry
|
// the drawing-window (canvas) colour, which masks geometry
|
||||||
// behind the text like a wipeout. Box = glyph bounds padded
|
// behind the text like a wipeout. Box = glyph bounds padded
|
||||||
// by (background_scale - 1) × text height.
|
// by (background_scale - 1) × text height.
|
||||||
if sdf_text && text_aabb != WireModel::UNBOUNDED_AABB {
|
if sdf_on && text_aabb != WireModel::UNBOUNDED_AABB {
|
||||||
if let EntityType::MText(m) = entity {
|
if let EntityType::MText(m) = entity {
|
||||||
let has_fill = m.background_fill_flags & 0x03 != 0;
|
let has_fill = m.background_fill_flags & 0x03 != 0;
|
||||||
let has_frame = m.background_fill_flags & 0x10 != 0;
|
let has_frame = m.background_fill_flags & 0x10 != 0;
|
||||||
|
|
@ -429,7 +429,7 @@ pub fn tessellate(
|
||||||
// bounds as a separate outline wire so the text box is
|
// bounds as a separate outline wire so the text box is
|
||||||
// visible for testing. The empty text wire below is left
|
// visible for testing. The empty text wire below is left
|
||||||
// untouched (still the SDF + pick target).
|
// untouched (still the SDF + pick target).
|
||||||
if sdf_text
|
if sdf_on
|
||||||
&& !sdf_verts.is_empty()
|
&& !sdf_verts.is_empty()
|
||||||
&& crate::scene::text::sdf_atlas::text_box_debug()
|
&& crate::scene::text::sdf_atlas::text_box_debug()
|
||||||
{
|
{
|
||||||
|
|
@ -561,6 +561,35 @@ pub fn tessellate(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
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,
|
||||||
|
vp_scissor: None,
|
||||||
|
fill_tris: vec![],
|
||||||
|
fill_tris_low: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if out.is_empty() {
|
if out.is_empty() {
|
||||||
out.push(WireModel {
|
out.push(WireModel {
|
||||||
text_verts: Vec::new(),
|
text_verts: Vec::new(),
|
||||||
|
|
|
||||||
|
|
@ -215,6 +215,38 @@ pub fn click_hits_all<'a>(
|
||||||
hits.push((best_for_wire, &wire.name));
|
hits.push((best_for_wire, &wire.name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// SDF text: include a text whose box contains the cursor (an empty-stroke
|
||||||
|
// wire carrying glyph quads) so selection cycling steps through text too —
|
||||||
|
// the same discriminator/fallback `click_hit` uses. Ranked after real
|
||||||
|
// geometry (distance = the click threshold, above every proximity hit).
|
||||||
|
if crate::scene::text::sdf_atlas::sdf_text_enabled() {
|
||||||
|
for wire in wires {
|
||||||
|
if wire.text_verts.is_empty()
|
||||||
|
|| !wire.points.is_empty()
|
||||||
|
|| !wire.fill_tris.is_empty()
|
||||||
|
|| wire.aabb == WireModel::UNBOUNDED_AABB
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let [minx, miny, maxx, maxy] = wire.aabb;
|
||||||
|
let (mut sx0, mut sy0, mut sx1, mut sy1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
|
||||||
|
for (cx, cy) in [(minx, miny), (maxx, miny), (maxx, maxy), (minx, maxy)] {
|
||||||
|
let s = world_to_screen(
|
||||||
|
glam::DVec3::new(cx as f64, cy as f64, 0.0),
|
||||||
|
view_rot,
|
||||||
|
eye,
|
||||||
|
bounds,
|
||||||
|
);
|
||||||
|
sx0 = sx0.min(s.x);
|
||||||
|
sx1 = sx1.max(s.x);
|
||||||
|
sy0 = sy0.min(s.y);
|
||||||
|
sy1 = sy1.max(s.y);
|
||||||
|
}
|
||||||
|
if cursor.x >= sx0 && cursor.x <= sx1 && cursor.y >= sy0 && cursor.y <= sy1 {
|
||||||
|
hits.push((CLICK_THRESHOLD_PX, &wire.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
hits.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
|
hits.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
hits.into_iter().map(|(_, name)| name).collect()
|
hits.into_iter().map(|(_, name)| name).collect()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,11 @@ pub fn apply_along(
|
||||||
|
|
||||||
let mut strokes: Vec<Vec<[f32; 3]>> = Vec::new();
|
let mut strokes: Vec<Vec<[f32; 3]>> = Vec::new();
|
||||||
let mut cur_stroke: Vec<[f32; 3]> = Vec::new();
|
let mut cur_stroke: Vec<[f32; 3]> = Vec::new();
|
||||||
|
// When SDF text is on, embedded linetype text (LtSeg::Text) renders as glyph
|
||||||
|
// quads collected here instead of stroke polylines; emitted as one extra
|
||||||
|
// text-carrying WireModel at the end.
|
||||||
|
let sdf_on = crate::scene::text::sdf_atlas::sdf_text_enabled();
|
||||||
|
let mut text_verts: Vec<crate::scene::pipeline::text_gpu::TextVertex> = Vec::new();
|
||||||
|
|
||||||
let mut elem_idx: usize = 0;
|
let mut elem_idx: usize = 0;
|
||||||
let mut elem_consumed: f32 = 0.0;
|
let mut elem_consumed: f32 = 0.0;
|
||||||
|
|
@ -175,20 +180,45 @@ pub fn apply_along(
|
||||||
let insert = offset_pt(insert, fwd, perp, *x, *y);
|
let insert = offset_pt(insert, fwd, perp, *x, *y);
|
||||||
let fwd_angle = fwd[1].atan2(fwd[0]) + rot_deg.to_radians();
|
let fwd_angle = fwd[1].atan2(fwd[0]) + rot_deg.to_radians();
|
||||||
let resolved = resolve_dxf_special_chars(text);
|
let resolved = resolve_dxf_special_chars(text);
|
||||||
let (text_strokes, _) = lff::tessellate_text_ex(
|
if sdf_on {
|
||||||
[insert[0], insert[1]],
|
// SDF: glyph quads at the insert point (rotation baked in
|
||||||
*tx_scale,
|
// by layout_glyph_quads), collected for the text wire.
|
||||||
fwd_angle,
|
if let Ok(mut atlas) = crate::scene::text::sdf_atlas::text_atlas().lock() {
|
||||||
1.0,
|
let quads = crate::scene::text::glyph_quads::layout_glyph_quads(
|
||||||
0.0,
|
&mut atlas,
|
||||||
style,
|
*tx_scale,
|
||||||
&resolved,
|
fwd_angle,
|
||||||
);
|
1.0,
|
||||||
for stroke in &text_strokes {
|
0.0,
|
||||||
if stroke.len() >= 2 {
|
0.0,
|
||||||
let pts: Vec<[f32; 3]> =
|
style,
|
||||||
stroke.iter().map(|&[sx, sy]| [sx, sy, insert[2]]).collect();
|
&resolved,
|
||||||
strokes.push(pts);
|
);
|
||||||
|
crate::scene::pipeline::text_gpu::push_glyph_vertices(
|
||||||
|
&mut text_verts,
|
||||||
|
&quads,
|
||||||
|
[insert[0] as f64, insert[1] as f64, insert[2] as f64],
|
||||||
|
1.0,
|
||||||
|
color,
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let (text_strokes, _) = lff::tessellate_text_ex(
|
||||||
|
[insert[0], insert[1]],
|
||||||
|
*tx_scale,
|
||||||
|
fwd_angle,
|
||||||
|
1.0,
|
||||||
|
0.0,
|
||||||
|
style,
|
||||||
|
&resolved,
|
||||||
|
);
|
||||||
|
for stroke in &text_strokes {
|
||||||
|
if stroke.len() >= 2 {
|
||||||
|
let pts: Vec<[f32; 3]> =
|
||||||
|
stroke.iter().map(|&[sx, sy]| [sx, sy, insert[2]]).collect();
|
||||||
|
strokes.push(pts);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -204,7 +234,7 @@ pub fn apply_along(
|
||||||
|
|
||||||
flush(&mut cur_stroke, &mut strokes);
|
flush(&mut cur_stroke, &mut strokes);
|
||||||
|
|
||||||
strokes
|
let mut out: Vec<WireModel> = strokes
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|s| s.len() >= 2)
|
.filter(|s| s.len() >= 2)
|
||||||
.map(|pts| WireModel {
|
.map(|pts| WireModel {
|
||||||
|
|
@ -227,7 +257,43 @@ pub fn apply_along(
|
||||||
fill_tris: vec![],
|
fill_tris: vec![],
|
||||||
fill_tris_low: Vec::new(),
|
fill_tris_low: Vec::new(),
|
||||||
})
|
})
|
||||||
.collect()
|
.collect();
|
||||||
|
|
||||||
|
// Embedded linetype text (SDF): one extra wire carrying the glyph quads,
|
||||||
|
// with a glyph-bounds AABB (f64 accumulate → f32) so it stays precise at
|
||||||
|
// UTM scale. Empty points so it doesn't add stroke geometry.
|
||||||
|
if !text_verts.is_empty() {
|
||||||
|
let (mut nx, mut ny, mut xx, mut xy) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
|
||||||
|
for v in &text_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);
|
||||||
|
}
|
||||||
|
out.push(WireModel {
|
||||||
|
text_verts,
|
||||||
|
name: name.to_string(),
|
||||||
|
points: Vec::new(),
|
||||||
|
points_low: Vec::new(),
|
||||||
|
color,
|
||||||
|
selected,
|
||||||
|
pattern_length: 0.0,
|
||||||
|
pattern: [0.0; 8],
|
||||||
|
line_weight_px,
|
||||||
|
snap_pts: vec![],
|
||||||
|
tangent_geoms: vec![],
|
||||||
|
aci: 0,
|
||||||
|
key_vertices: vec![],
|
||||||
|
aabb: [nx as f32, ny as f32, xx as f32, xy as f32],
|
||||||
|
plinegen: true,
|
||||||
|
vp_scissor: None,
|
||||||
|
fill_tris: vec![],
|
||||||
|
fill_tris_low: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue