refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
163725ca22
commit
aadb64860d
12 changed files with 3046 additions and 2862 deletions
|
|
@ -850,3 +850,664 @@ impl Transformable for MultiLeader {
|
|||
apply_transform(self, t);
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-entity tessellation entry for `MultiLeader`. Returns multiple
|
||||
/// `WireModel`s (leader/dogleg + arrow fill, optional block content,
|
||||
/// text strokes, frame, background fill) so each piece can carry its
|
||||
/// own colour.
|
||||
pub trait MultiLeaderTess {
|
||||
fn tessellate(
|
||||
&self,
|
||||
document: &acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
selected: bool,
|
||||
entity_color: [f32; 4],
|
||||
line_weight_px: f32,
|
||||
world_offset: [f64; 3],
|
||||
anno_scale: f32,
|
||||
world_per_pixel: Option<f32>,
|
||||
) -> Vec<crate::scene::wire_model::WireModel>;
|
||||
}
|
||||
|
||||
impl MultiLeaderTess for MultiLeader {
|
||||
fn tessellate(
|
||||
&self,
|
||||
document: &acadrust::CadDocument,
|
||||
handle: acadrust::Handle,
|
||||
selected: bool,
|
||||
entity_color: [f32; 4],
|
||||
line_weight_px: f32,
|
||||
world_offset: [f64; 3],
|
||||
anno_scale: f32,
|
||||
world_per_pixel: Option<f32>,
|
||||
) -> Vec<crate::scene::wire_model::WireModel> {
|
||||
use crate::scene::tessellate::{
|
||||
append_arrow, arrow_from_block, color_or_inherit, tessellate, ArrowKind, DimGeom,
|
||||
};
|
||||
use crate::scene::wire_model::{SnapHint, TangentGeom, WireModel};
|
||||
use glam::Vec3;
|
||||
let ml = self;
|
||||
// line_color falls back to the entity colour when the MultiLeader's own
|
||||
// colour is ByBlock/ByLayer; otherwise the leader uses its stored hue.
|
||||
let line_color = if selected {
|
||||
WireModel::SELECTED
|
||||
} else {
|
||||
color_or_inherit(&ml.line_color, entity_color)
|
||||
};
|
||||
// ml.line_weight is the leader-line weight override. Negative codes
|
||||
// (ByLayer/ByBlock/Default) fall through to the entity's already-resolved
|
||||
// pixel width.
|
||||
let leader_lw_px = match ml.line_weight {
|
||||
acadrust::types::LineWeight::Value(v) if v >= 0 => {
|
||||
(v as f32 / 100.0) * (96.0 / 25.4)
|
||||
}
|
||||
_ => line_weight_px,
|
||||
};
|
||||
// ml.line_type_handle — resolve via line_types table by handle and apply
|
||||
// the resulting dash pattern to the leader wire.
|
||||
let lt_scale = document.header.linetype_scale as f32 * ml.common.linetype_scale as f32;
|
||||
let (leader_pat_len, leader_pat) = match ml.line_type_handle {
|
||||
Some(h) if !h.is_null() => {
|
||||
let name = document
|
||||
.line_types
|
||||
.iter()
|
||||
.find(|lt| lt.handle == h)
|
||||
.map(|lt| lt.name.clone());
|
||||
match name {
|
||||
Some(n) => crate::scene::render::resolve_pattern(&document.line_types, &n, lt_scale),
|
||||
None => (0.0, [0.0; 8]),
|
||||
}
|
||||
}
|
||||
_ => (0.0, [0.0; 8]),
|
||||
};
|
||||
|
||||
let name = handle.value().to_string();
|
||||
let nan = [f32::NAN; 3];
|
||||
let [ox, oy, oz] = world_offset;
|
||||
let p3 = |v: &acadrust::types::Vector3| -> [f32; 3] {
|
||||
[(v.x - ox) as f32, (v.y - oy) as f32, (v.z - oz) as f32]
|
||||
};
|
||||
|
||||
// ── Scaling ──────────────────────────────────────────────────────────────
|
||||
// ml.scale_factor is always applied; anno_scale is only applied when the
|
||||
// multileader is marked annotative.
|
||||
let effective_scale = (ml.scale_factor as f32)
|
||||
* if ml.enable_annotation_scale {
|
||||
anno_scale
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
|
||||
let arrow_size = ml.arrowhead_size as f32 * effective_scale;
|
||||
let draw_arrow = arrow_size > 0.0;
|
||||
let invisible = ml.path_type == MultiLeaderPathType::Invisible;
|
||||
// arrowhead_handle resolves through the block records to a named arrow
|
||||
// block (matches DIMLDRBLK on Dimension). Null handle / unknown name →
|
||||
// ClosedFilled triangle.
|
||||
let arrow_kind = match ml.arrowhead_handle {
|
||||
Some(h) if !h.is_null() => arrow_from_block(document, h, arrow_size.max(0.001)),
|
||||
_ => ArrowKind::Triangle {
|
||||
size: arrow_size.max(0.001),
|
||||
filled: true,
|
||||
size_mul: 1.0,
|
||||
},
|
||||
};
|
||||
|
||||
// ── Leader / arrow / dogleg points ───────────────────────────────────────
|
||||
let mut points: Vec<[f32; 3]> = Vec::new();
|
||||
let mut key_verts: Vec<[f32; 3]> = Vec::new();
|
||||
let mut snap_pts: Vec<(Vec3, SnapHint)> = Vec::new();
|
||||
let mut tangents: Vec<TangentGeom> = Vec::new();
|
||||
let mut arrow_fill: Vec<[f32; 3]> = Vec::new();
|
||||
let mut first = true;
|
||||
|
||||
for root in &ml.context.leader_roots {
|
||||
let cp = &root.connection_point;
|
||||
let cp_f = p3(cp);
|
||||
snap_pts.push((Vec3::from(cp_f), SnapHint::Node));
|
||||
|
||||
for line in &root.lines {
|
||||
if line.points.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !invisible {
|
||||
if !first {
|
||||
points.push(nan);
|
||||
}
|
||||
first = false;
|
||||
|
||||
let mut ctrl: Vec<[f32; 3]> = line.points.iter().map(|p| p3(p)).collect();
|
||||
let last_f = *ctrl.last().unwrap_or(&cp_f);
|
||||
let dist = ((last_f[0] - cp_f[0]).powi(2) + (last_f[1] - cp_f[1]).powi(2)).sqrt();
|
||||
if dist > 1e-9 {
|
||||
ctrl.push(cp_f);
|
||||
}
|
||||
for &c in &ctrl {
|
||||
key_verts.push(c);
|
||||
snap_pts.push((Vec3::from(c), SnapHint::Node));
|
||||
}
|
||||
|
||||
if ml.path_type == MultiLeaderPathType::Spline && ctrl.len() >= 2 {
|
||||
let ctrl_f64: Vec<[f64; 3]> = ctrl
|
||||
.iter()
|
||||
.map(|c| [c[0] as f64, c[1] as f64, c[2] as f64])
|
||||
.collect();
|
||||
for pt in catmull_rom_pts(&ctrl_f64, 8) {
|
||||
points.push([pt[0] as f32, pt[1] as f32, pt[2] as f32]);
|
||||
}
|
||||
} else {
|
||||
for &c in &ctrl {
|
||||
points.push(c);
|
||||
}
|
||||
}
|
||||
for i in 0..ctrl.len().saturating_sub(1) {
|
||||
tangents.push(TangentGeom::Line {
|
||||
p1: ctrl[i],
|
||||
p2: ctrl[i + 1],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if draw_arrow {
|
||||
let tip = &line.points[0];
|
||||
let tip_f = p3(tip);
|
||||
let next = if line.points.len() >= 2 {
|
||||
line.points[1]
|
||||
} else {
|
||||
*cp
|
||||
};
|
||||
let dx = (next.x - tip.x) as f32;
|
||||
let dy = (next.y - tip.y) as f32;
|
||||
let dl = (dx * dx + dy * dy).sqrt().max(1e-9);
|
||||
let dir = Vec3::new(dx / dl, dy / dl, 0.0);
|
||||
let tip_v = Vec3::new(tip_f[0], tip_f[1], tip_f[2]);
|
||||
// Reuse the dim/leader arrow emitter so MultiLeader's arrow
|
||||
// matches the block referenced by arrowhead_handle.
|
||||
let mut arrow_geom = DimGeom::new();
|
||||
append_arrow(&mut arrow_geom, tip_v, dir, &arrow_kind);
|
||||
if !arrow_geom.dim_lines.is_empty() {
|
||||
points.push(nan);
|
||||
points.extend(arrow_geom.dim_lines);
|
||||
}
|
||||
arrow_fill.extend(arrow_geom.arrow_fill);
|
||||
}
|
||||
}
|
||||
|
||||
if ml.enable_landing && ml.enable_dogleg && ml.dogleg_length > 0.0 {
|
||||
let dir = &root.direction;
|
||||
let dl = (dir.x * dir.x + dir.y * dir.y).sqrt().max(1e-9);
|
||||
let d = ml.dogleg_length * effective_scale as f64;
|
||||
let landing_end = [
|
||||
(cp.x + dir.x / dl * d - ox) as f32,
|
||||
(cp.y + dir.y / dl * d - oy) as f32,
|
||||
cp_f[2],
|
||||
];
|
||||
points.push(nan);
|
||||
points.push(cp_f);
|
||||
points.push(landing_end);
|
||||
// extend_leader_to_text: continue past the landing to the text
|
||||
// insertion point along the same direction.
|
||||
if ml.extend_leader_to_text
|
||||
&& ml.content_type == LeaderContentType::MText
|
||||
&& !ml.context.text_string.is_empty()
|
||||
{
|
||||
let tx = (ml.context.text_location.x - ox) as f32;
|
||||
let ty = (ml.context.text_location.y - oy) as f32;
|
||||
points.push(landing_end);
|
||||
points.push([tx, ty, cp_f[2]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The leader/arrow/dogleg wire goes out as a single WireModel. Text, frame,
|
||||
// and background fill (each with their own color) are appended as separate
|
||||
// WireModels so the renderer respects per-piece coloring.
|
||||
let mut wires: Vec<WireModel> = Vec::new();
|
||||
wires.push(WireModel {
|
||||
name: name.clone(),
|
||||
points,
|
||||
color: line_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: leader_pat_len,
|
||||
pattern: leader_pat,
|
||||
line_weight_px: leader_lw_px,
|
||||
snap_pts,
|
||||
tangent_geoms: tangents,
|
||||
key_vertices: key_verts,
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: arrow_fill,
|
||||
});
|
||||
|
||||
// ── Block content ───────────────────────────────────────────────────────
|
||||
// When content_type == Block, the MultiLeader displays a block reference
|
||||
// at block_content_location with the recorded rotation/scale. Synthesize
|
||||
// an Insert and explode it through the standard tessellator. The block
|
||||
// resolves via block_content_handle (handle → block_record name).
|
||||
if ml.content_type == LeaderContentType::Block && ml.context.has_block_contents {
|
||||
let block_name = match ml.block_content_handle {
|
||||
Some(h) if !h.is_null() => document
|
||||
.block_records
|
||||
.iter()
|
||||
.find(|br| br.handle == h)
|
||||
.map(|br| br.name.clone()),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(block_name) = block_name {
|
||||
let block_color = if selected {
|
||||
line_color
|
||||
} else {
|
||||
color_or_inherit(&ml.block_content_color, entity_color)
|
||||
};
|
||||
let mut synth_ins =
|
||||
acadrust::entities::Insert::new(block_name, ml.context.block_content_location);
|
||||
synth_ins.set_x_scale(ml.block_scale.x);
|
||||
synth_ins.set_y_scale(ml.block_scale.y);
|
||||
synth_ins.set_z_scale(ml.block_scale.z);
|
||||
synth_ins.rotation = ml.block_rotation;
|
||||
synth_ins.common.layer = ml.common.layer.clone();
|
||||
// block_connection_type (BlockExtents vs BasePoint) chooses the
|
||||
// anchor when *creating* the multileader; at render time the
|
||||
// file's stored leader endpoints already encode that choice.
|
||||
let _ = ml.block_connection_type;
|
||||
for sub in synth_ins.explode_from_document(document) {
|
||||
let normalized =
|
||||
crate::modules::home::modify::explode::normalize_insert_entity(sub);
|
||||
let mut sub_wires = tessellate(
|
||||
document,
|
||||
handle,
|
||||
&normalized,
|
||||
selected,
|
||||
block_color,
|
||||
leader_pat_len,
|
||||
leader_pat,
|
||||
leader_lw_px,
|
||||
world_offset,
|
||||
1.0,
|
||||
);
|
||||
for w in &mut sub_wires {
|
||||
w.name = name.clone();
|
||||
}
|
||||
wires.extend(sub_wires);
|
||||
}
|
||||
// Block attributes attached to the multileader — render each as
|
||||
// its own attribute entity at WCS location like INSERT does.
|
||||
for ba in &ml.block_attributes {
|
||||
let _ = ba; // BlockAttribute carries only the value override
|
||||
// string; we'd need the AttributeDefinition handle
|
||||
// to materialise it as ATTRIB geometry. Skipped
|
||||
// until that wiring exists.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Text strokes / frame / background fill ──────────────────────────────
|
||||
// Strip inline format codes, split / word-wrap into lines, then place each
|
||||
// line according to text_attachment_point (horizontal) and
|
||||
// text_left_attachment (vertical), with text_rotation/text_direction applied.
|
||||
if ml.content_type == LeaderContentType::MText && !ml.context.text_string.is_empty() {
|
||||
let ctx = &ml.context;
|
||||
// `ctx.text_height` (when > 0) is the already-resolved WCS text height
|
||||
// stored in the per-instance context — style × scale_factor × anno
|
||||
// scale are all already baked in. Multiplying by `effective_scale`
|
||||
// again would double-scale (e.g., a context height of 100 in a file
|
||||
// with scale_factor=20 was rendering at 2000 units — 20× too big).
|
||||
// Only the fallback path (file omits the context value) needs
|
||||
// scale_factor + annotation scale applied.
|
||||
let height = if ctx.text_height > 0.0 {
|
||||
ctx.text_height as f32
|
||||
} else {
|
||||
ml.text_height as f32 * effective_scale
|
||||
};
|
||||
|
||||
let ins = &ctx.text_location;
|
||||
// Subtract world_offset in f64 before casting to f32: drawings often
|
||||
// sit at large absolute coordinates and casting first then subtracting
|
||||
// throws away the precision needed for the rotated sub-glyph offsets.
|
||||
let local_ins_x = (ins.x - ox) as f32;
|
||||
let local_ins_y = (ins.y - oy) as f32;
|
||||
let z = (ins.z - oz) as f32;
|
||||
|
||||
// Rotation: prefer text_direction (transforms survive rotations /
|
||||
// mirrors when acadrust updates it) and fall back to text_rotation.
|
||||
// ml.text_angle_type then constrains the final angle:
|
||||
// - Horizontal: always 0
|
||||
// - ParallelToLastLeaderLine: keep stored direction
|
||||
// - Optimized: clamp to (-π/2, π/2] (flip upside-down)
|
||||
// text_direction_negative finally adds π so reading goes the other way.
|
||||
let td = ctx.text_direction;
|
||||
let mut rot = if td.x.abs() > 1e-9 || td.y.abs() > 1e-9 {
|
||||
(td.y as f32).atan2(td.x as f32)
|
||||
} else {
|
||||
ctx.text_rotation as f32
|
||||
};
|
||||
match ml.text_angle_type {
|
||||
acadrust::entities::multileader::TextAngleType::Horizontal => rot = 0.0,
|
||||
acadrust::entities::multileader::TextAngleType::Optimized => {
|
||||
let pi = std::f32::consts::PI;
|
||||
if rot > pi / 2.0 {
|
||||
rot -= pi;
|
||||
} else if rot <= -pi / 2.0 {
|
||||
rot += pi;
|
||||
}
|
||||
}
|
||||
acadrust::entities::multileader::TextAngleType::ParallelToLastLeaderLine => {}
|
||||
}
|
||||
if ml.text_direction_negative {
|
||||
rot += std::f32::consts::PI;
|
||||
}
|
||||
|
||||
// Resolve text style via handle when available, falling back to STANDARD.
|
||||
let style_name = ctx
|
||||
.text_style_handle
|
||||
.as_ref()
|
||||
.and_then(|h| {
|
||||
document
|
||||
.text_styles
|
||||
.iter()
|
||||
.find(|s| s.handle == *h)
|
||||
.map(|s| s.name.clone())
|
||||
})
|
||||
.unwrap_or_else(|| "STANDARD".to_string());
|
||||
let style = resolve_text_style(&style_name, document);
|
||||
let mut rot = rot;
|
||||
if style.is_upside_down {
|
||||
rot += std::f32::consts::PI;
|
||||
}
|
||||
let (cos_r, sin_r) = (rot.cos(), rot.sin());
|
||||
|
||||
// ml.text_alignment overrides the per-context attachment_point when
|
||||
// not Left (which is the default zero). Otherwise we honour the
|
||||
// context's stored attachment_point (Left/Center/Right).
|
||||
use acadrust::entities::multileader::{TextAlignmentType, TextAttachmentDirectionType};
|
||||
let h_anchor = match (ml.text_alignment, ctx.text_attachment_point) {
|
||||
(TextAlignmentType::Center, _) => 0.5_f32,
|
||||
(TextAlignmentType::Right, _) => 1.0,
|
||||
(TextAlignmentType::Left, TextAttachmentPointType::Center) => 0.5,
|
||||
(TextAlignmentType::Left, TextAttachmentPointType::Right) => 1.0,
|
||||
_ => 0.0,
|
||||
};
|
||||
// Pick the vertical-anchor attachment based on text_attachment_direction:
|
||||
// Horizontal — leader attaches left/right; use ml.text_left_attachment
|
||||
// (matches the file's stored ctx.text_left_attachment).
|
||||
// Vertical — leader attaches top/bottom; use ml.text_top_attachment
|
||||
// or ml.text_bottom_attachment depending on which side
|
||||
// the leader is coming from (chosen via the first
|
||||
// root.direction.y sign).
|
||||
let leader_from_top = ml
|
||||
.context
|
||||
.leader_roots
|
||||
.first()
|
||||
.map(|r| r.direction.y < 0.0)
|
||||
.unwrap_or(true);
|
||||
let vertical_attach = match ml.text_attachment_direction {
|
||||
TextAttachmentDirectionType::Vertical => {
|
||||
if leader_from_top {
|
||||
ml.text_top_attachment
|
||||
} else {
|
||||
ml.text_bottom_attachment
|
||||
}
|
||||
}
|
||||
TextAttachmentDirectionType::Horizontal => ctx.text_left_attachment,
|
||||
};
|
||||
let v_anchor = mleader_v_anchor(vertical_attach);
|
||||
|
||||
// Shared MText pipeline — every inline format code (`\f`, `\C`/`\c`,
|
||||
// `\H`, `\W`, `\Q`, `\T`, `\A`, `\p…`, decorations, stacked
|
||||
// fractions, …) reaches the stroke output. Stroke origins are
|
||||
// already in offset-relative space because we pass local_ins_x/y.
|
||||
let layout = layout_mtext(&MTextRenderOpts {
|
||||
value: &ctx.text_string,
|
||||
insertion: [local_ins_x as f64, local_ins_y as f64, z as f64],
|
||||
height,
|
||||
rect_w: ctx.text_width as f32,
|
||||
rotation: rot,
|
||||
style: &style,
|
||||
attach_h_anchor: h_anchor,
|
||||
v_anchor,
|
||||
line_spacing_factor: ctx.line_spacing_factor as f32,
|
||||
vertical_text: false,
|
||||
});
|
||||
let line_widths = &layout.line_widths;
|
||||
let max_line_w = line_widths.iter().cloned().fold(0.0_f32, f32::max);
|
||||
let line_h = layout.line_height;
|
||||
let v_offset = layout.v_offset;
|
||||
let n_lines = layout.line_count.max(1) as f32;
|
||||
|
||||
// Resolve text color (falls back to entity color for ByLayer / ByBlock).
|
||||
let text_color = if selected {
|
||||
line_color
|
||||
} else {
|
||||
color_or_inherit(&ctx.text_color, entity_color)
|
||||
};
|
||||
|
||||
// Same LOD ladder used for top-level Text / MText (see scene/mod.rs):
|
||||
// h_px < 1 → baseline line (skip glyphs)
|
||||
// 1 ≤ h < 5 → greeked rect in text color (skip glyphs)
|
||||
// h_px ≥ 5 → full per-glyph stroke tessellation
|
||||
let lod_h_px = world_per_pixel.map(|wpp| height / wpp);
|
||||
let lod_mode = match lod_h_px {
|
||||
Some(h) if h < 1.0 => 0,
|
||||
Some(h) if h < 5.0 => 1,
|
||||
_ => 2,
|
||||
};
|
||||
|
||||
// Helper: map a (local_x, local_y) in the text's pre-rotation frame
|
||||
// (origin at the insertion point) into WCS render space.
|
||||
let to_wcs = |lx: f32, ly: f32| -> [f32; 3] {
|
||||
[
|
||||
local_ins_x + lx * cos_r - ly * sin_r,
|
||||
local_ins_y + lx * sin_r + ly * cos_r,
|
||||
z,
|
||||
]
|
||||
};
|
||||
|
||||
if lod_mode == 0 {
|
||||
// Baseline of the top line only.
|
||||
let line_w = line_widths.first().copied().unwrap_or(0.0);
|
||||
let len_px = world_per_pixel
|
||||
.map(|wpp| line_w / wpp)
|
||||
.unwrap_or(f32::INFINITY);
|
||||
if len_px >= 2.0 {
|
||||
let line_y_local = v_offset;
|
||||
let p0 = to_wcs(-line_w * h_anchor, line_y_local);
|
||||
let p1 = to_wcs(line_w * (1.0 - h_anchor), line_y_local);
|
||||
wires.push(WireModel {
|
||||
name: name.clone(),
|
||||
points: vec![p0, p1],
|
||||
color: text_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px,
|
||||
snap_pts: vec![(
|
||||
Vec3::new(local_ins_x, local_ins_y, z),
|
||||
SnapHint::Node,
|
||||
)],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
});
|
||||
}
|
||||
} else if lod_mode == 1 {
|
||||
// One filled rect per line — keeps the visual "text lives here
|
||||
// per row" hint that multi-line MText carries, in the text's
|
||||
// own color. Empty `points` opts out of the face3d 0.45 dim so
|
||||
// the fill renders at full intensity.
|
||||
let mut greek_tris: Vec<[f32; 3]> = Vec::with_capacity(line_widths.len() * 6);
|
||||
for (i, &line_w) in line_widths.iter().enumerate() {
|
||||
let li = i as f32;
|
||||
let line_y_bottom = -li * line_h + v_offset;
|
||||
let line_y_top = line_y_bottom + height;
|
||||
if line_w <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
let left = -line_w * h_anchor;
|
||||
let right = line_w * (1.0 - h_anchor);
|
||||
let bl = to_wcs(left, line_y_bottom);
|
||||
let br = to_wcs(right, line_y_bottom);
|
||||
let tr = to_wcs(right, line_y_top);
|
||||
let tl = to_wcs(left, line_y_top);
|
||||
greek_tris.extend_from_slice(&[bl, br, tr, bl, tr, tl]);
|
||||
}
|
||||
if !greek_tris.is_empty() {
|
||||
wires.push(WireModel {
|
||||
name: name.clone(),
|
||||
points: vec![],
|
||||
color: text_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px: 1.0,
|
||||
snap_pts: vec![(
|
||||
Vec3::new(local_ins_x, local_ins_y, z),
|
||||
SnapHint::Node,
|
||||
)],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: greek_tris,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Pre-tessellated by `layout_mtext`. Each TextStroke is in
|
||||
// local glyph space with its world origin (already offset-
|
||||
// relative because we passed local_ins_x/y) stored as f64.
|
||||
let mut text_points: Vec<[f32; 3]> = Vec::new();
|
||||
for ts in &layout.strokes {
|
||||
let ox = ts.origin[0] as f32;
|
||||
let oy = ts.origin[1] as f32;
|
||||
for stroke in &ts.strokes {
|
||||
if stroke.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
text_points.push(nan);
|
||||
for &[x, y] in stroke {
|
||||
text_points.push([x + ox, y + oy, z]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !text_points.is_empty() {
|
||||
wires.push(WireModel {
|
||||
name: name.clone(),
|
||||
points: text_points,
|
||||
color: text_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px,
|
||||
snap_pts: vec![(
|
||||
Vec3::new(local_ins_x, local_ins_y, z),
|
||||
SnapHint::Node,
|
||||
)],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Text frame / background-fill rectangle in local frame, then rotated to WCS.
|
||||
if ml.text_frame || ctx.background_fill_enabled {
|
||||
// Visual gap so the frame/fill doesn't touch glyph caps.
|
||||
let pad = height * 0.25;
|
||||
let block_top = v_offset + height + pad;
|
||||
let block_bottom = v_offset - (n_lines - 1.0) * line_h - pad;
|
||||
let block_left = -max_line_w * h_anchor - pad;
|
||||
let block_right = max_line_w * (1.0 - h_anchor) + pad;
|
||||
let local_corners: [[f32; 2]; 4] = [
|
||||
[block_left, block_bottom],
|
||||
[block_right, block_bottom],
|
||||
[block_right, block_top],
|
||||
[block_left, block_top],
|
||||
];
|
||||
let wcs_corners: [[f32; 3]; 4] = std::array::from_fn(|i| {
|
||||
let lx = local_corners[i][0];
|
||||
let ly = local_corners[i][1];
|
||||
let wx = local_ins_x + lx * cos_r - ly * sin_r;
|
||||
let wy = local_ins_y + lx * sin_r + ly * cos_r;
|
||||
[wx, wy, z]
|
||||
});
|
||||
|
||||
// Background fill — emit two triangles; renders under the text strokes.
|
||||
if ctx.background_fill_enabled {
|
||||
let fill_color = if selected {
|
||||
line_color
|
||||
} else {
|
||||
color_or_inherit(&ctx.background_fill_color, entity_color)
|
||||
};
|
||||
let fill_tris: Vec<[f32; 3]> = vec![
|
||||
wcs_corners[0],
|
||||
wcs_corners[1],
|
||||
wcs_corners[2],
|
||||
wcs_corners[0],
|
||||
wcs_corners[2],
|
||||
wcs_corners[3],
|
||||
];
|
||||
wires.push(WireModel {
|
||||
name: name.clone(),
|
||||
points: vec![],
|
||||
color: fill_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px: 1.0,
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris,
|
||||
});
|
||||
}
|
||||
|
||||
// Text frame — closed rectangle, matches text color.
|
||||
if ml.text_frame {
|
||||
let frame_points: Vec<[f32; 3]> = vec![
|
||||
wcs_corners[0],
|
||||
wcs_corners[1],
|
||||
wcs_corners[2],
|
||||
wcs_corners[3],
|
||||
wcs_corners[0],
|
||||
];
|
||||
wires.push(WireModel {
|
||||
name,
|
||||
points: frame_points,
|
||||
color: text_color,
|
||||
selected,
|
||||
aci: 0,
|
||||
pattern_length: 0.0,
|
||||
pattern: [0.0; 8],
|
||||
line_weight_px,
|
||||
snap_pts: vec![],
|
||||
tangent_geoms: vec![],
|
||||
key_vertices: vec![],
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
vp_scissor: None,
|
||||
fill_tris: vec![],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wires
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue