Feat: Phase 5.4 — Table entity render (grid wireframe + cell text)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-04-02 23:11:09 +03:00
commit 313e7226d2
3 changed files with 184 additions and 0 deletions

View file

@ -20,6 +20,7 @@ pub mod ray;
pub mod spline;
pub mod text;
mod text_support;
pub mod table;
pub mod tolerance;
pub mod traits;
pub mod viewport;

174
src/entities/table.rs Normal file
View file

@ -0,0 +1,174 @@
use acadrust::entities::Table;
use glam::Vec3;
use crate::command::EntityTransform;
use crate::entities::common::{ro_prop as ro, square_grip};
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible};
use crate::scene::acad_to_truck::{TruckEntity, TruckObject};
use crate::scene::object::{GripApply, GripDef, PropSection};
use crate::scene::wire_model::SnapHint;
use crate::scene::{cxf, transform};
fn v3(v: &acadrust::types::Vector3) -> Vec3 {
Vec3::new(v.x as f32, v.y as f32, v.z as f32)
}
impl TruckConvertible for Table {
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
if self.rows.is_empty() || self.columns.is_empty() {
return None;
}
let origin = v3(&self.insertion_point);
let h_raw = v3(&self.horizontal_direction);
let h = if h_raw.length_squared() > 1e-10 { h_raw.normalize() } else { Vec3::X };
// Perpendicular "down" direction in the drawing plane (tables grow downward)
let v_down = Vec3::new(h.y, -h.x, 0.0);
let col_offsets: Vec<f32> = {
let mut off = 0.0f32;
let mut v = vec![0.0f32];
for col in &self.columns {
off += col.width as f32;
v.push(off);
}
v
};
let total_w = *col_offsets.last().unwrap_or(&0.0);
let row_offsets: Vec<f32> = {
let mut off = 0.0f32;
let mut v = vec![0.0f32];
for row in &self.rows {
off += row.height as f32;
v.push(off);
}
v
};
let total_h = *row_offsets.last().unwrap_or(&0.0);
let mut pts: Vec<[f32; 3]> = Vec::new();
let mut add_seg = |a: Vec3, b: Vec3| {
if !pts.is_empty() {
pts.push([f32::NAN; 3]);
}
pts.push([a.x, a.y, a.z]);
pts.push([b.x, b.y, b.z]);
};
for &ry in &row_offsets {
let left = origin + h * 0.0 + v_down * ry;
let right = origin + h * total_w + v_down * ry;
add_seg(left, right);
}
for &cx in &col_offsets {
let top = origin + h * cx + v_down * 0.0;
let bottom = origin + h * cx + v_down * total_h;
add_seg(top, bottom);
}
// Cell text — lifted into Lines points via 2D strokes
let text_height = 0.18_f32;
for (ri, row) in self.rows.iter().enumerate() {
let row_top = row_offsets[ri];
let row_bot = row_offsets.get(ri + 1).copied().unwrap_or(row_top + row.height as f32);
let row_mid = (row_top + row_bot) * 0.5;
for (ci, cell) in row.cells.iter().enumerate() {
let text = cell.text_value();
if text.is_empty() {
continue;
}
let col_left = col_offsets[ci];
let col_mid = col_left + self.columns.get(ci).map(|c| c.width as f32 * 0.5).unwrap_or(0.5);
let cell_center = origin + h * col_mid + v_down * row_mid;
let strokes = cxf::tessellate_text_ex(
[cell_center.x, cell_center.y],
text_height,
0.0,
1.0,
0.0,
"txt",
text,
);
for stroke in strokes {
if !pts.is_empty() {
pts.push([f32::NAN; 3]);
}
for [x, y] in stroke {
pts.push([x, y, origin.z]);
}
}
}
}
Some(TruckEntity {
object: TruckObject::Lines(pts),
snap_pts: vec![(origin, SnapHint::Insertion)],
tangent_geoms: vec![],
key_vertices: vec![],
})
}
}
impl Grippable for Table {
fn grips(&self) -> Vec<GripDef> {
vec![square_grip(0, v3(&self.insertion_point))]
}
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
if grip_id == 0 {
match apply {
GripApply::Translate(d) => {
self.insertion_point.x += d.x as f64;
self.insertion_point.y += d.y as f64;
self.insertion_point.z += d.z as f64;
}
GripApply::Absolute(p) => {
self.insertion_point.x = p.x as f64;
self.insertion_point.y = p.y as f64;
self.insertion_point.z = p.z as f64;
}
}
}
}
}
impl PropertyEditable for Table {
fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection {
PropSection {
title: "Table".into(),
props: vec![
ro("Rows", "tbl_rows", self.rows.len().to_string()),
ro("Columns", "tbl_cols", self.columns.len().to_string()),
ro("Insert X","tbl_ix", format!("{:.4}", self.insertion_point.x)),
ro("Insert Y","tbl_iy", format!("{:.4}", self.insertion_point.y)),
ro("Insert Z","tbl_iz", format!("{:.4}", self.insertion_point.z)),
],
}
}
fn apply_geom_prop(&mut self, _field: &str, _value: &str) {}
}
impl Transformable for Table {
fn apply_transform(&mut self, t: &EntityTransform) {
transform::apply_standard_entity_transform(self, t, |entity, p1, p2| {
transform::reflect_xy_point(
&mut entity.insertion_point.x,
&mut entity.insertion_point.y,
p1,
p2,
);
// Reflect the horizontal direction by reflecting a tip point
let mut tip_x = entity.insertion_point.x + entity.horizontal_direction.x;
let mut tip_y = entity.insertion_point.y + entity.horizontal_direction.y;
transform::reflect_xy_point(&mut tip_x, &mut tip_y, p1, p2);
entity.horizontal_direction.x = tip_x - entity.insertion_point.x;
entity.horizontal_direction.y = tip_y - entity.insertion_point.y;
});
}
}

View file

@ -55,6 +55,7 @@ impl EntityTypeOps for EntityType {
EntityType::Face3D(f) => TruckConvertible::to_truck(f, document),
EntityType::PolygonMesh(pm) => TruckConvertible::to_truck(pm, document),
EntityType::PolyfaceMesh(pfm) => TruckConvertible::to_truck(pfm, document),
EntityType::Table(tbl) => TruckConvertible::to_truck(tbl, document),
EntityType::Text(text) => TruckConvertible::to_truck(text, document),
EntityType::MText(text) => TruckConvertible::to_truck(text, document),
EntityType::Leader(leader) => TruckConvertible::to_truck(leader, document),
@ -84,6 +85,7 @@ impl EntityTypeOps for EntityType {
EntityType::Face3D(f) => Grippable::grips(f),
EntityType::PolygonMesh(pm) => Grippable::grips(pm),
EntityType::PolyfaceMesh(pfm) => Grippable::grips(pfm),
EntityType::Table(tbl) => Grippable::grips(tbl),
EntityType::Point(pt) => Grippable::grips(pt),
EntityType::Spline(spline) => Grippable::grips(spline),
EntityType::Text(text) => Grippable::grips(text),
@ -173,6 +175,10 @@ impl EntityTypeOps for EntityType {
pfm,
text_style_names,
)),
EntityType::Table(tbl) => Some(PropertyEditable::geometry_properties(
tbl,
text_style_names,
)),
EntityType::Hatch(hatch) => Some(PropertyEditable::geometry_properties(
hatch,
text_style_names,
@ -237,6 +243,7 @@ impl EntityTypeOps for EntityType {
EntityType::Face3D(f) => PropertyEditable::apply_geom_prop(f, field, value),
EntityType::PolygonMesh(pm) => PropertyEditable::apply_geom_prop(pm, field, value),
EntityType::PolyfaceMesh(pfm) => PropertyEditable::apply_geom_prop(pfm, field, value),
EntityType::Table(tbl) => PropertyEditable::apply_geom_prop(tbl, field, value),
EntityType::Hatch(hatch) => PropertyEditable::apply_geom_prop(hatch, field, value),
EntityType::Point(pt) => PropertyEditable::apply_geom_prop(pt, field, value),
EntityType::Spline(spline) => PropertyEditable::apply_geom_prop(spline, field, value),
@ -272,6 +279,7 @@ impl EntityTypeOps for EntityType {
EntityType::Face3D(f) => Grippable::apply_grip(f, grip_id, apply),
EntityType::PolygonMesh(pm) => Grippable::apply_grip(pm, grip_id, apply),
EntityType::PolyfaceMesh(pfm) => Grippable::apply_grip(pfm, grip_id, apply),
EntityType::Table(tbl) => Grippable::apply_grip(tbl, grip_id, apply),
EntityType::Point(pt) => Grippable::apply_grip(pt, grip_id, apply),
EntityType::Spline(spline) => Grippable::apply_grip(spline, grip_id, apply),
EntityType::Text(text) => Grippable::apply_grip(text, grip_id, apply),
@ -307,6 +315,7 @@ impl EntityTypeOps for EntityType {
EntityType::Face3D(f) => Transformable::apply_transform(f, t),
EntityType::PolygonMesh(pm) => Transformable::apply_transform(pm, t),
EntityType::PolyfaceMesh(pfm) => Transformable::apply_transform(pfm, t),
EntityType::Table(tbl) => Transformable::apply_transform(tbl, t),
EntityType::MText(text) => Transformable::apply_transform(text, t),
EntityType::Point(pt) => Transformable::apply_transform(pt, t),
EntityType::Spline(spline) => Transformable::apply_transform(spline, t),