cad-editor/src/modules/annotate/table_cmd.rs

240 lines
7.5 KiB
Rust
Raw Normal View History

// TABLE command — create an empty table entity.
//
// Workflow:
// 1. Text: Enter number of columns (default 3)
// 2. Text: Enter number of rows (default 4, includes header row)
// 3. Point: Click insertion point
//
// Creates a Table entity with uniform row height (0.5) and column width (2.0).
use acadrust::entities::TableBuilder;
use acadrust::types::Vector3;
use acadrust::EntityType;
use glam::DVec3;
use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
2026-08-02 22:09:25 +03:00
use crate::t;
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/table.svg"));
pub fn tool() -> ToolDef {
ToolDef {
id: "TABLE",
label: "Table",
icon: ICON,
event: ModuleEvent::Command("TABLE".to_string()),
}
}
const DEFAULT_COLS: usize = 3;
const DEFAULT_ROWS: usize = 4;
const COL_WIDTH: f64 = 2.0;
const ROW_HEIGHT: f64 = 0.5;
enum Step {
Columns,
Rows { cols: usize },
Insertion { cols: usize, rows: usize },
}
pub struct TableCommand {
step: Step,
style_handle: Option<acadrust::Handle>,
column_width: f64,
row_height: f64,
preview_scale: f64,
plane: WorkingPlane,
}
impl TableCommand {
pub fn new() -> Self {
2026-05-12 10:36:15 +03:00
Self {
step: Step::Columns,
style_handle: None,
column_width: COL_WIDTH,
row_height: ROW_HEIGHT,
preview_scale: 1.0,
plane: WorkingPlane::default(),
}
}
pub fn with_style(
style_handle: acadrust::Handle,
style: &acadrust::objects::TableStyle,
annotation_multiplier: f64,
) -> Self {
let text_height = style
.data_row_style
.text_height
.max(style.header_row_style.text_height)
.max(style.title_row_style.text_height)
.max(1.0e-6);
let row_height = text_height * 1.5 + style.vertical_margin * 2.0;
let column_width = text_height * 8.0 + style.horizontal_margin * 2.0;
Self {
step: Step::Columns,
style_handle: Some(style_handle),
column_width,
row_height,
preview_scale: if style.annotative {
annotation_multiplier
} else {
1.0
},
plane: WorkingPlane::default(),
2026-05-12 10:36:15 +03:00
}
}
}
impl CadCommand for TableCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
2026-05-12 10:36:15 +03:00
fn name(&self) -> &'static str {
"TABLE"
}
fn prompt(&self) -> String {
match &self.step {
2026-08-02 22:09:25 +03:00
Step::Columns => t!(
"TABLE Enter number of columns [%{cols}]:",
cols = DEFAULT_COLS
)
.into_owned(),
Step::Rows { cols } => t!(
"TABLE Enter number of rows (incl. header) [%{rows}] (%{cols} cols):",
rows = DEFAULT_ROWS,
cols = cols
)
.into_owned(),
Step::Insertion { cols, rows } => t!(
"TABLE Specify insertion point [%{cols}×%{rows}]:",
cols = cols,
rows = rows
)
.into_owned(),
}
}
fn wants_text_input(&self) -> bool {
!matches!(self.step, Step::Insertion { .. })
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let t = text.trim();
match &self.step {
Step::Columns => {
let cols = if t.is_empty() {
DEFAULT_COLS
} else {
t.parse::<usize>().ok().filter(|&n| n >= 1)?
};
self.step = Step::Rows { cols };
Some(CmdResult::NeedPoint)
}
Step::Rows { cols } => {
let cols = *cols;
let rows = if t.is_empty() {
DEFAULT_ROWS
} else {
t.parse::<usize>().ok().filter(|&n| n >= 1)?
};
self.step = Step::Insertion { cols, rows };
Some(CmdResult::NeedPoint)
}
Step::Insertion { .. } => None,
}
}
fn on_enter(&mut self) -> CmdResult {
match &self.step {
Step::Columns => {
// Accept default.
self.step = Step::Rows { cols: DEFAULT_COLS };
CmdResult::NeedPoint
}
Step::Rows { cols } => {
let cols = *cols;
2026-05-12 10:36:15 +03:00
self.step = Step::Insertion {
cols,
rows: DEFAULT_ROWS,
};
CmdResult::NeedPoint
}
Step::Insertion { .. } => CmdResult::Cancel,
}
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
if let Step::Insertion { cols, rows } = self.step {
let point = self.plane.to_local(pt);
let ins = Vector3::new(point.x, point.y, point.z);
let mut table = TableBuilder::new(rows, cols)
.at(ins)
.row_height(self.row_height)
.column_width(self.column_width)
.build();
table.table_style_handle = self.style_handle;
CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Table(table)))
} else {
CmdResult::NeedPoint
}
}
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
if let Step::Insertion { cols, rows } = self.step {
// Preview: outline of the table bounding box.
let w = (cols as f32) * (self.column_width * self.preview_scale) as f32;
let h = (rows as f32) * (self.row_height * self.preview_scale) as f32;
let point = self.plane.to_local(pt);
let corners = [
point,
point + DVec3::X * w as f64,
point + DVec3::new(w as f64, -(h as f64), 0.0),
point - DVec3::Y * h as f64,
]
.map(|corner| self.plane.to_world(corner).as_vec3().to_array());
let [p0, p1, p2, p3] = corners;
Some(WireModel {
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
fix(pick): hit-test lineweight width and thickness walls Selection tested a flat 8 px radius around each wire's centreline and nothing else, so two things the user can see were not pickable. A wire renders as a band `line_weight_px` wide, so pick now widens to that rendered half-width via `pick_tolerance_px`, mirroring the wire shader's `select(0.5, half_width, lwdisplay_enable)`. Behaviour is unchanged for every standard weight (the widest, 2.11 mm, renders 7.97 px half-width — just under the 8 px threshold); this keeps the two sides from drifting apart if the display boost in `lineweight_to_px` ever changes. Entities extruded by a DXF thickness draw their wall as four edges with nothing in between, so the interior was a hole the cursor fell through. WireModel gains `pick_tris`: triangles hit-testing treats as solid and the renderer never uploads. Kept apart from `fill_tris` because that channel reaches the GPU, and a wall that rendered shaded would change how every thickness drawing looks. Ranked below `fill_tris` — those are drawn, these are not, so where they overlap the drawn face is what the user means. Walls are dropped when a wire is projected into a floating viewport: `points` are reprojected and clipped into paper coords there, so cloned model-space triangles would hit-test at a model-scale offset. Extruded entities stay selectable by their edges, which do get projected. The wall loop runs on every hover that misses everything else, so it rejects on the wire's AABB first — an extruded circle is ~128 triangles and a drawing full of thickness would otherwise project every wall on each mouse move. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 14:58:55 +03:00
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
feat(mline): GPU-dash multiline elements with shared A-type alignment MLINE dashed elements were fully CPU-expanded (apply_along) — one wire per dash — to keep the parallel lines in phase. Move pure-dash elements onto the GPU dash shader and end-align every element to the multiline's centre-line so they share one interior phase. - wire_distances now accumulates arc-length from the double-single (high+low) points, so the dash phase stays precise at UTM coordinates. f32-high-only deltas quantised ~0.1 there and drifted the parallel lines apart — the original reason MLINE dashes were CPU-side. - New WireModel.dash_align_end carries a shared begin/end dash length. The shader uses it as align_end for every parallel element (the interior phase depends on align_end, not the wire's own length) while align_total stays the element's own length, so each still ends on a dash at its own endpoint. - MLINE tessellation derives that length once from the centre-line, routes pure dash/space/dot elements to the GPU pattern and text-bearing elements to apply_along. - apply_along gained an "A"-type mode (shared reference length): a solid begin/end dash and a phased interior, so its dashes — and embedded text — line up with the GPU-dashed sibling elements instead of tiling from start. Pure-dash MLINEs now cost one WireModel + pattern per element instead of N segments; single (non-MLINE) entities keep the from-start tiling unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 02:59:11 +03:00
dash_align_end: None,
text_verts: Vec::new(),
name: "table_preview".into(),
points: vec![
p0, p1, p1, p2, p2, p3, p3, p0,
],
points_low: Vec::new(),
color: WireModel::CYAN,
selected: false,
pattern_length: 0.0,
pattern: [0.0; 8],
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
aci: 0,
2026-05-12 10:36:15 +03:00
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
} else {
None
}
}
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["TABLE"] }); // TableCommand