diff --git a/src/scene/block_cache.rs b/src/scene/block_cache.rs index 634840279..58731026c 100644 --- a/src/scene/block_cache.rs +++ b/src/scene/block_cache.rs @@ -627,11 +627,13 @@ pub fn expand_insert( Some(batches.finalize(&name, selected)) } -/// Emit a greeked rectangle for a text LocalWire — same color, OBB-sized -/// fill (rotated to match the entity), no per-glyph stroke geometry. -/// Mirrors the top-level text greek in scene/mod.rs (including the 0.45 -/// dim pre-boost the face3d pipeline applies to fill_tris). Falls back to -/// the axis-aligned `local_aabb` when no OBB was precomputed. +/// Emit a greeked filled rect (2 triangles) for a text LocalWire. Stays in +/// the text's own color — the face3d pipeline skips its 0.45 dim for wires +/// whose `points` are empty, so the rect lands at full intensity. Clamped +/// to a single line's height so multi-line MText doesn't blow the box up +/// to the full block. Mirrors `text_greek_obb_tris` in scene/mod.rs. +/// Falls back to the axis-aligned `local_aabb` when no OBB was cached; +/// xref fade still applies via the shared color resolution. fn emit_greeked_text( lw: &LocalWire, local_aabb: [f32; 4], @@ -642,35 +644,57 @@ fn emit_greeked_text( ) { let [ox, oy, oz] = ctx.world_offset; let [lo_x, lo_y, lo_z] = defn_lo; - let tris: [[f32; 3]; 6] = if let Some(obb) = lw.text_obb_local { - // Re-add the defn's `local_offset` in f64 before composing with - // `accum_xform` (the OBB corners are stored in offset frame f32 so - // they don't lose precision for distant text). Then subtract host - // `world_offset` so the fill_tris share the batch's f32 space. - let xf = |p: [f32; 3]| -> [f32; 3] { - let w = accum_xform.apply(Vector3::new( - p[0] as f64 + lo_x, - p[1] as f64 + lo_y, - p[2] as f64 + lo_z, - )); - [(w.x - ox) as f32, (w.y - oy) as f32, (w.z - oz) as f32] - }; - let p00 = xf(obb[0]); - let p10 = xf(obb[1]); - let p11 = xf(obb[2]); - let p01 = xf(obb[3]); - [p00, p10, p11, p00, p11, p01] + // Re-add the defn's `local_offset` in f64 before composing with + // `accum_xform` so the rect corners share the batch's f32 space + // without losing precision for distant text. + let xf = |p: [f32; 3]| -> [f32; 3] { + let w = accum_xform.apply(Vector3::new( + p[0] as f64 + lo_x, + p[1] as f64 + lo_y, + p[2] as f64 + lo_z, + )); + [(w.x - ox) as f32, (w.y - oy) as f32, (w.z - oz) as f32] + }; + + let corners: Option<[[f32; 3]; 4]> = if let (Some(obb), Some(h_local)) = + (lw.text_obb_local, lw.text_height_local) + { + if h_local <= 0.0 { + None + } else { + // OBB[3] - OBB[0] = up direction in local frame, full-block + // height for MText; normalize and rescale to a single line. + let dx = obb[3][0] - obb[0][0]; + let dy = obb[3][1] - obb[0][1]; + let dz = obb[3][2] - obb[0][2]; + let dlen = (dx * dx + dy * dy + dz * dz).sqrt(); + if dlen < 1e-9 { + None + } else { + let nx = dx / dlen * h_local; + let ny = dy / dlen * h_local; + let nz = dz / dlen * h_local; + let bl = obb[0]; + let br = obb[1]; + let tl = [bl[0] + nx, bl[1] + ny, bl[2] + nz]; + let tr = [br[0] + nx, br[1] + ny, br[2] + nz]; + Some([xf(bl), xf(br), xf(tr), xf(tl)]) + } + } } else { + // OBB missing — fall back to the axis-aligned local AABB. Already + // a tight per-entity rect (no multi-line block to clamp). let [x0, y0, x1, y1] = local_aabb; let z = 0.0_f32; - [ - [x0, y0, z], - [x1, y0, z], - [x1, y1, z], - [x0, y0, z], - [x1, y1, z], - [x0, y1, z], - ] + Some([ + xf([x0, y0, z]), + xf([x1, y0, z]), + xf([x1, y1, z]), + xf([x0, y1, z]), + ]) + }; + let Some([bl, br, tr, tl]) = corners else { + return; }; let final_color = if ctx.selected { @@ -680,29 +704,19 @@ fn emit_greeked_text( } else { lw.color }; - // Apply the same xref fade as `emit_wire` BEFORE the face3d 0.45 boost, - // so the final on-screen color (boost × 0.45 dim) lands at the faded value. let final_color = if ctx.is_xref && !ctx.selected { fade_toward_bg(final_color, ctx.bg_color) } else { final_color }; - let boost = 1.0 / 0.45_f32; - let [r, g, b, a] = final_color; - let greek_color = [ - (r * boost).min(1.0), - (g * boost).min(1.0), - (b * boost).min(1.0), - a, - ]; - let key = style_key(greek_color, 0.0, [0.0; 8], 1.0, lw.aci, true); + let key = style_key(final_color, 0.0, [0.0; 8], 1.0, lw.aci, true, true); let entry = out .by_style .entry(key) - .or_insert_with(|| BatchEntry::new(greek_color, 0.0, [0.0; 8], 1.0, lw.aci, true)); - for p in tris { - entry.fill_tris.push(p); + .or_insert_with(|| BatchEntry::new(final_color, 0.0, [0.0; 8], 1.0, lw.aci, true, true)); + + for p in [bl, br, tr, bl, tr, tl] { if p[0] < entry.min_x { entry.min_x = p[0]; } @@ -715,6 +729,7 @@ fn emit_greeked_text( if p[1] > entry.max_y { entry.max_y = p[1]; } + entry.fill_tris.push(p); } } @@ -767,11 +782,11 @@ fn emit_text_baseline( final_color }; - let key = style_key(final_color, 0.0, [0.0; 8], 1.0, lw.aci, true); + let key = style_key(final_color, 0.0, [0.0; 8], 1.0, lw.aci, true, false); let entry = out .by_style .entry(key) - .or_insert_with(|| BatchEntry::new(final_color, 0.0, [0.0; 8], 1.0, lw.aci, true)); + .or_insert_with(|| BatchEntry::new(final_color, 0.0, [0.0; 8], 1.0, lw.aci, true, false)); let needs_sep = !entry.points.is_empty() && !entry.points.last().map(|p| p[0].is_nan()).unwrap_or(false); @@ -844,6 +859,11 @@ struct StyleKey { line_weight_px: u32, aci: u8, plinegen: bool, + /// Marks batches that emit only `fill_tris` with no wire `points`. The + /// face3d pipeline uses `wire.points.is_empty()` as the "skip dim" + /// discriminator, so greek fills must stay in their own batches even + /// when their color/style would otherwise collide with regular wires. + is_fill_only: bool, } #[derive(Default, Debug)] @@ -880,7 +900,20 @@ struct Batches { } impl BatchEntry { - fn new(color: [f32; 4], pat_len: f32, pat: [f32; 8], lw_px: f32, aci: u8, plinegen: bool) -> Self { + fn new( + color: [f32; 4], + pat_len: f32, + pat: [f32; 8], + lw_px: f32, + aci: u8, + plinegen: bool, + _is_fill_only: bool, + ) -> Self { + // `is_fill_only` is part of the StyleKey hash so greek fills never + // share a batch with regular wires (otherwise the finalized + // WireModel would have both `points` and `fill_tris`, defeating + // the face3d-dim discriminator). It isn't stored on the entry + // itself — the empty `points` field is enough at finalize time. Self { color, pattern_length: pat_len, @@ -937,6 +970,7 @@ fn style_key( lw_px: f32, aci: u8, plinegen: bool, + is_fill_only: bool, ) -> StyleKey { StyleKey { color: [ @@ -959,6 +993,7 @@ fn style_key( line_weight_px: lw_px.to_bits(), aci, plinegen, + is_fill_only, } } @@ -1164,6 +1199,7 @@ fn emit_wire( final_lw_px, lw.aci, lw.plinegen, + false, ); // If the open batch for this style would exceed wgpu's per-buffer limit @@ -1183,6 +1219,7 @@ fn emit_wire( final_lw_px, lw.aci, lw.plinegen, + false, ) }); diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 57bc5844c..0e540a0d0 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -4195,22 +4195,13 @@ fn tessellate_entity( } if h_px < 5.0 && aabb != WireModel::UNBOUNDED_AABB { let fill_tris = text_greek_obb_tris(e, anno_scale, world_offset); - // The face3d pipeline dims fill_tris colors to 45% to fake - // ambient occlusion on PolyfaceMesh / 3DFACE solids. Greeked - // text doesn't want that shading — pre-boost the color so it - // emerges close to the original text color after the dim. - let boost = 1.0 / 0.45; - let [r, g, b, a] = entity_color; - let greek_color = [ - (r * boost).min(1.0), - (g * boost).min(1.0), - (b * boost).min(1.0), - a, - ]; + if fill_tris.is_empty() { + return vec![]; + } return vec![WireModel { name: h.value().to_string(), points: vec![], - color: greek_color, + color: entity_color, selected: sel, aci, pattern_length: 0.0, @@ -4401,8 +4392,11 @@ fn text_baseline_points( vec![cast(corners[0]), cast(corners[1])] } -/// 6-vertex (2-triangle) fill for a greeked top-level Text / MText, in -/// world-offset-subtracted f32 space. +/// 6-vertex filled rect (2 triangles) for a greeked top-level Text / MText. +/// Clamped to a single line's height — multi-line MText's OBB spans the +/// whole block and using its full height would emit a misleadingly tall +/// box. The face3d pipeline skips its 0.45 dim for wires with empty +/// `points`, so these tris render at the literal text color. fn text_greek_obb_tris( e: &EntityType, anno_scale: f32, @@ -4411,13 +4405,35 @@ fn text_greek_obb_tris( let Some(corners) = text_obb_corners_native(e, anno_scale) else { return vec![]; }; + let line_h = match e { + EntityType::Text(t) => (t.height * anno_scale as f64) as f32, + EntityType::MText(m) => (m.height * anno_scale as f64) as f32, + _ => return vec![], + }; + if line_h <= 0.0 { + return vec![]; + } let [ox, oy, oz] = world_offset; let cast = |p: [f64; 3]| -> [f32; 3] { [(p[0] - ox) as f32, (p[1] - oy) as f32, (p[2] - oz) as f32] }; - let [p00, p10, p11, p01] = corners; - let (p00, p10, p11, p01) = (cast(p00), cast(p10), cast(p11), cast(p01)); - vec![p00, p10, p11, p00, p11, p01] + let bl = cast(corners[0]); + let br = cast(corners[1]); + let full_tl = cast(corners[3]); + + // Up direction = (bl → full_tl) normalized, then scale to a single + // line. Replaces the full-block top corners so MText with N lines + // still emits a single-line rect. + let (ux, uy, uz) = (full_tl[0] - bl[0], full_tl[1] - bl[1], full_tl[2] - bl[2]); + let ulen = (ux * ux + uy * uy + uz * uz).sqrt(); + if ulen < 1e-9 { + return vec![]; + } + let (nx, ny, nz) = (ux / ulen, uy / ulen, uz / ulen); + let tl = [bl[0] + nx * line_h, bl[1] + ny * line_h, bl[2] + nz * line_h]; + let tr = [br[0] + nx * line_h, br[1] + ny * line_h, br[2] + nz * line_h]; + + vec![bl, br, tr, bl, tr, tl] } fn entity_aabb(e: &acadrust::EntityType, world_offset: [f64; 3]) -> [f32; 4] { diff --git a/src/scene/pipeline/face3d_gpu.rs b/src/scene/pipeline/face3d_gpu.rs index 8658d2a80..8be56c95b 100644 --- a/src/scene/pipeline/face3d_gpu.rs +++ b/src/scene/pipeline/face3d_gpu.rs @@ -85,13 +85,22 @@ impl Face3DGpu { vertices.push(v(3)); } - // PolyfaceMesh / PolygonMesh pre-triangulated fills. + // PolyfaceMesh / PolygonMesh / unlit fills (text greek, MultiLeader + // background). Wires whose `points` are empty carry pure 2-D fills + // that should render at their literal color — applying the 0.45 + // AO-style dim to them would wash out user-picked colors. Wires + // with both fill_tris and points (mesh edges + faces) keep the dim + // so PolyfaceMesh / PolygonMesh still look 3-D-shaded. for wire in all_wires { if wire.fill_tris.is_empty() { continue; } let [r, g, b, a] = wire.color; - let fill_color = [r * 0.45, g * 0.45, b * 0.45, a]; + let fill_color = if wire.points.is_empty() { + [r, g, b, a] + } else { + [r * 0.45, g * 0.45, b * 0.45, a] + }; for &position in &wire.fill_tris { vertices.push(Face3DVertex { position, diff --git a/src/scene/tessellate.rs b/src/scene/tessellate.rs index bff76d80a..031a0eb24 100644 --- a/src/scene/tessellate.rs +++ b/src/scene/tessellate.rs @@ -909,45 +909,49 @@ pub fn tessellate_multileader( }); } } else if lod_mode == 1 { - // Greeked rect — tight text bbox (no frame padding). - let block_top = v_offset + height; - let block_bottom = v_offset - (n_lines - 1.0) * line_h; - let block_left = -max_line_w * h_anchor; - let block_right = max_line_w * (1.0 - h_anchor); - let c00 = to_wcs(block_left, block_bottom); - let c10 = to_wcs(block_right, block_bottom); - let c11 = to_wcs(block_right, block_top); - let c01 = to_wcs(block_left, block_top); - // Pre-boost the color so the face3d 0.45 fill_tris dim lands at - // the original text color (matches scene/mod.rs greek path). - let boost = 1.0 / 0.45_f32; - let [r, g, b, a] = text_color; - let greek_color = [ - (r * boost).min(1.0), - (g * boost).min(1.0), - (b * boost).min(1.0), - a, - ]; - wires.push(WireModel { - name: name.clone(), - points: vec![], - color: greek_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: vec![c00, c10, c11, c00, c11, c01], - }); + // 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(lines.len() * 6); + for (i, _) in lines.iter().enumerate() { + let li = i as f32; + let line_y_bottom = -li * line_h + v_offset; + let line_y_top = line_y_bottom + height; + let line_w = line_widths[i]; + 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 { // Build per-line stroke points in WCS. let mut text_points: Vec<[f32; 3]> = Vec::new();