first commit

This commit is contained in:
Hakan Seven committed 2026-03-22 14:56:27 +03:00
1 parent d52c6440ff
commit d2fdac7f1e
274 files changed
+89330

No files matched your search

+699
View File
@@ -0,0 +1,699 @@
// Array dropdown — ribbon definition + interactive commands.
//
// ARRAYRECT (AR):
// Rectangular array: row/column counts and spacing collected via text input.
// 1. Row count → 2. Column count → 3. Row spacing → 4. Column spacing
// → Returns BatchCopy with a grid of Translate transforms.
//
// ARRAYPATH:
// Path array: copies placed at equal intervals along a curve/line.
// (pending geometry engine support — stub)
//
// ARRAYPOLAR:
// Polar array: copies rotated around a center point by a total angle.
// 1. Center point → 2. Item count (text) → 3. Total angle in degrees (text)
use acadrust::Handle;
use glam::Vec3;
use crate::command::{CadCommand, CmdResult, EntityTransform};
use crate::modules::home::defaults;
use crate::modules::IconKind;
use crate::scene::wire_model::WireModel;
// ── Dropdown constants ─────────────────────────────────────────────────────
pub const DROPDOWN_ID: &str = "array_type";
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../../assets/icons/array_rect.svg"));
pub const DROPDOWN_ITEMS: &[(&str, &str, IconKind)] = &[
(
"ARRAYRECT",
"Rectangular Array",
IconKind::Svg(include_bytes!("../../../../assets/icons/array_rect.svg")),
),
(
"ARRAYPATH",
"Path Array",
IconKind::Svg(include_bytes!("../../../../assets/icons/array_path.svg")),
),
(
"ARRAYPOLAR",
"Polar Array",
IconKind::Svg(include_bytes!("../../../../assets/icons/array_polar.svg")),
),
];
// ── Rectangular Array ──────────────────────────────────────────────────────
#[derive(Debug, Clone, Copy)]
enum RectStep {
Rows,
Cols { rows: u32 },
RowSp { rows: u32, cols: u32 },
ColSp { rows: u32, cols: u32, row_sp: f32 },
}
pub struct ArrayRectCommand {
handles: Vec<Handle>,
wire_models: Vec<WireModel>,
step: RectStep,
default_rows: u32,
default_cols: u32,
default_row_sp: f32,
default_col_sp: f32,
}
impl ArrayRectCommand {
pub fn new(handles: Vec<Handle>, wire_models: Vec<WireModel>) -> Self {
Self {
handles,
wire_models,
step: RectStep::Rows,
default_rows: defaults::get_array_rows() as u32,
default_cols: defaults::get_array_cols() as u32,
default_row_sp: defaults::get_array_row_sp(),
default_col_sp: defaults::get_array_col_sp(),
}
}
fn build_transforms(rows: u32, cols: u32, row_sp: f32, col_sp: f32) -> Vec<EntityTransform> {
let mut t = Vec::new();
for r in 0..rows {
for c in 0..cols {
if r == 0 && c == 0 {
continue;
}
t.push(EntityTransform::Translate(Vec3::new(
col_sp * c as f32,
row_sp * r as f32,
0.0,
)));
}
}
t
}
}
impl CadCommand for ArrayRectCommand {
fn name(&self) -> &'static str {
"ARRAYRECT"
}
fn prompt(&self) -> String {
match self.step {
RectStep::Rows => format!("ARRAYRECT Enter row count <{}>:", self.default_rows),
RectStep::Cols { rows } => format!(
"ARRAYRECT Enter column count <{}> [{rows} rows]:",
self.default_cols
),
RectStep::RowSp { rows, cols } => format!(
"ARRAYRECT Row spacing <{:.0}> [{rows}×{cols}]:",
self.default_row_sp
),
RectStep::ColSp { rows, cols, row_sp } => format!(
"ARRAYRECT Column spacing <{:.0}> [{rows}×{cols}, row={row_sp:.0}]:",
self.default_col_sp
),
}
}
fn wants_text_input(&self) -> bool {
true
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let t = text.trim().replace(',', ".");
let t = t.as_str();
match self.step {
RectStep::Rows => {
let rows = if t.is_empty() {
self.default_rows
} else {
let v = t.parse::<u32>().unwrap_or(self.default_rows).max(1);
defaults::set_array_rows(v as f32);
self.default_rows = v;
v
};
self.step = RectStep::Cols { rows };
None
}
RectStep::Cols { rows } => {
let cols = if t.is_empty() {
self.default_cols
} else {
let v = t.parse::<u32>().unwrap_or(self.default_cols).max(1);
defaults::set_array_cols(v as f32);
self.default_cols = v;
v
};
self.step = RectStep::RowSp { rows, cols };
None
}
RectStep::RowSp { rows, cols } => {
let row_sp = if t.is_empty() {
self.default_row_sp
} else {
let v = t.parse::<f32>().unwrap_or(self.default_row_sp);
defaults::set_array_row_sp(v);
self.default_row_sp = v;
v
};
self.step = RectStep::ColSp { rows, cols, row_sp };
None
}
RectStep::ColSp { rows, cols, row_sp } => {
let col_sp = if t.is_empty() {
self.default_col_sp
} else {
let v = t.parse::<f32>().unwrap_or(self.default_col_sp);
defaults::set_array_col_sp(v);
v
};
Some(CmdResult::BatchCopy(
self.handles.clone(),
Self::build_transforms(rows, cols, row_sp, col_sp),
))
}
}
}
fn on_preview_wires(&mut self, _pt: Vec3) -> Vec<WireModel> {
let (rows, cols, row_sp, col_sp) = match self.step {
RectStep::Rows => (
self.default_rows,
self.default_cols,
self.default_row_sp,
self.default_col_sp,
),
RectStep::Cols { rows } => (
rows,
self.default_cols,
self.default_row_sp,
self.default_col_sp,
),
RectStep::RowSp { rows, cols } => {
(rows, cols, self.default_row_sp, self.default_col_sp)
}
RectStep::ColSp { rows, cols, row_sp } => (rows, cols, row_sp, self.default_col_sp),
};
Self::build_transforms(rows, cols, row_sp, col_sp)
.iter()
.flat_map(|t| {
if let EntityTransform::Translate(delta) = t {
self.wire_models
.iter()
.map(|w| w.translated(*delta))
.collect::<Vec<_>>()
} else {
vec![]
}
})
.collect()
}
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
// Enter with empty input = use default for current step
self.on_text_input("").map_or(CmdResult::NeedPoint, |r| r)
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
// ── Polar Array ────────────────────────────────────────────────────────────
enum PolarStep {
Center,
Count { center: Vec3 },
Angle { center: Vec3, count: u32 },
}
pub struct ArrayPolarCommand {
handles: Vec<Handle>,
wire_models: Vec<WireModel>,
step: PolarStep,
default_count: u32,
default_angle: f32,
}
impl ArrayPolarCommand {
pub fn new(handles: Vec<Handle>, wire_models: Vec<WireModel>) -> Self {
Self {
handles,
wire_models,
step: PolarStep::Center,
default_count: defaults::get_array_p_count() as u32,
default_angle: defaults::get_array_p_angle(),
}
}
}
impl CadCommand for ArrayPolarCommand {
fn name(&self) -> &'static str {
"ARRAYPOLAR"
}
fn prompt(&self) -> String {
match &self.step {
PolarStep::Center => format!(
"ARRAYPOLAR Specify center point [{} objects]:",
self.handles.len()
),
PolarStep::Count { .. } => {
format!("ARRAYPOLAR Enter item count <{}>:", self.default_count)
}
PolarStep::Angle { count, .. } => format!(
"ARRAYPOLAR Enter total angle in degrees <{:.0}> [{count} items]:",
self.default_angle
),
}
}
fn wants_text_input(&self) -> bool {
matches!(self.step, PolarStep::Count { .. } | PolarStep::Angle { .. })
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let t = text.trim().replace(',', ".");
let t = t.as_str();
match &self.step {
PolarStep::Count { center } => {
let center = *center;
let count = if t.is_empty() {
self.default_count
} else {
let v = t.parse::<u32>().unwrap_or(self.default_count).max(2);
defaults::set_array_p_count(v as f32);
self.default_count = v;
v
};
self.step = PolarStep::Angle { center, count };
None
}
PolarStep::Angle { center, count } => {
let center = *center;
let count = *count;
let total_deg = if t.is_empty() {
self.default_angle
} else {
let v = t.parse::<f32>().unwrap_or(self.default_angle);
defaults::set_array_p_angle(v);
v
};
let step_rad = total_deg.to_radians() / count as f32;
let transforms = (1..count)
.map(|n| EntityTransform::Rotate {
center,
angle_rad: step_rad * n as f32,
})
.collect();
Some(CmdResult::BatchCopy(self.handles.clone(), transforms))
}
_ => None,
}
}
fn on_preview_wires(&mut self, pt: Vec3) -> Vec<WireModel> {
let (center, count, total_deg) = match &self.step {
PolarStep::Center => (pt, self.default_count, self.default_angle),
PolarStep::Count { center } => (*center, self.default_count, self.default_angle),
PolarStep::Angle { center, count } => (*center, *count, self.default_angle),
};
let step_rad = total_deg.to_radians() / count as f32;
let mut out: Vec<WireModel> = (1..count)
.flat_map(|n| {
let angle_rad = step_rad * n as f32;
self.wire_models
.iter()
.map(move |w| w.rotated(center, angle_rad))
})
.collect();
// Rubber-band from center to cursor while picking the center point.
if matches!(self.step, PolarStep::Center) {
out.push(WireModel::solid(
"rubber_band".into(),
vec![[center.x, center.y, center.z], [pt.x, pt.y, pt.z]],
WireModel::CYAN,
false,
));
}
out
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
if let PolarStep::Center = self.step {
self.step = PolarStep::Count { center: pt };
}
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
self.on_text_input("").map_or(CmdResult::NeedPoint, |r| r)
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
// ── Path Array ─────────────────────────────────────────────────────────────
//
// ARRAYPATH:
// Copies selected objects at equal arc-length intervals along a path entity.
// 1. Select path entity (Line, Arc, Circle, LwPolyline)
// 2. Enter item count (total, including the original at the path start)
// → Returns BatchCopy with Translate transforms derived from path samples.
use acadrust::EntityType;
use std::f32::consts::TAU as FTAU;
// ── Path geometry helpers ──────────────────────────────────────────────────
/// Tessellate an LwPolyline.
fn lw_dense_pts(p: &acadrust::entities::LwPolyline) -> Vec<Vec3> {
let z = p.elevation as f32;
let verts = &p.vertices;
if verts.is_empty() {
return vec![];
}
let n = verts.len();
let segs = if p.is_closed { n } else { n.saturating_sub(1) };
let mut out: Vec<Vec3> = vec![];
for i in 0..segs {
let v0 = &verts[i];
let v1 = &verts[(i + 1) % n];
let x0 = v0.location.x as f32;
let y0 = v0.location.y as f32;
let x1 = v1.location.x as f32;
let y1 = v1.location.y as f32;
if out.is_empty() {
out.push(Vec3::new(x0, y0, z));
}
let bulge = v0.bulge as f32;
if bulge.abs() < 1e-9 {
out.push(Vec3::new(x1, y1, z));
} else {
let dx = x1 - x0;
let dy = y1 - y0;
let d = (dx * dx + dy * dy).sqrt();
if d < 1e-9 {
out.push(Vec3::new(x1, y1, z));
continue;
}
let angle = 4.0 * bulge.atan();
let r = (d * 0.5) / (angle * 0.5).sin().abs();
let mx = (x0 + x1) * 0.5;
let my = (y0 + y1) * 0.5;
let inv = 1.0 / d;
let (px, py) = (-dy * inv, dx * inv);
let sign = if bulge > 0.0 { 1.0f32 } else { -1.0 };
let h_val = r - (r * r - d * d * 0.25).max(0.0).sqrt();
let (cx, cy) = (mx - sign * px * (r - h_val), my - sign * py * (r - h_val));
let a0 = (y0 - cy).atan2(x0 - cx);
let a1 = (y1 - cy).atan2(x1 - cx);
// Signed span: CCW for bulge > 0, CW (negative) for bulge < 0.
let span = if bulge > 0.0 {
let s = a1 - a0;
if s <= 0.0 {
s + FTAU
} else {
s
}
} else {
let s = a1 - a0;
if s >= 0.0 {
s - FTAU
} else {
s
}
};
let steps = ((r * span.abs() / 2.0).ceil() as usize).clamp(4, 64);
for j in 1..=steps {
let ang = a0 + span * (j as f32 / steps as f32);
out.push(Vec3::new(cx + r * ang.cos(), cy + r * ang.sin(), z));
}
}
}
out
}
/// Walk `pts` (ordered) and return `count` points at equal arc-length spacing.
fn subsample_equidistant(pts: &[Vec3], count: usize) -> Vec<Vec3> {
if count == 0 {
return vec![];
}
if pts.len() < 2 {
return vec![pts.first().copied().unwrap_or(Vec3::ZERO); count];
}
let mut cum = vec![0.0f32; pts.len()];
for i in 1..pts.len() {
cum[i] = cum[i - 1] + pts[i].distance(pts[i - 1]);
}
let total = *cum.last().unwrap();
if total < 1e-9 {
return vec![pts[0]; count];
}
let mut out = Vec::with_capacity(count);
let mut seg = 0usize;
for i in 0..count {
let target = if count > 1 {
total * i as f32 / (count - 1) as f32
} else {
0.0
};
while seg + 2 < pts.len() && cum[seg + 1] < target - 1e-9 {
seg += 1;
}
let seg_len = cum[seg + 1] - cum[seg];
let t = if seg_len > 1e-9 {
(target - cum[seg]) / seg_len
} else {
0.0
};
out.push(pts[seg].lerp(pts[seg + 1], t.clamp(0.0, 1.0)));
}
out
}
// ── State machine ──────────────────────────────────────────────────────────
enum PathStep {
SelectPath,
Count { path_entity: EntityType },
}
pub struct ArrayPathCommand {
handles: Vec<Handle>,
wire_models: Vec<WireModel>,
all_entities: Vec<EntityType>,
step: PathStep,
default_count: u32,
}
impl ArrayPathCommand {
pub fn new(
handles: Vec<Handle>,
wire_models: Vec<WireModel>,
all_entities: Vec<EntityType>,
) -> Self {
Self {
handles,
wire_models,
all_entities,
step: PathStep::SelectPath,
default_count: defaults::get_array_path_count() as u32,
}
}
/// Sample `count` evenly-spaced points along `entity`.
fn sample_path(entity: &EntityType, count: usize) -> Vec<Vec3> {
if count == 0 {
return vec![];
}
let fn_norm = |x: f32| -> f32 { ((x % FTAU) + FTAU) % FTAU };
match entity {
EntityType::Line(l) => {
let p0 = Vec3::new(l.start.x as f32, l.start.y as f32, 0.0);
let p1 = Vec3::new(l.end.x as f32, l.end.y as f32, 0.0);
let d = (count - 1).max(1) as f32;
(0..count).map(|i| p0.lerp(p1, i as f32 / d)).collect()
}
EntityType::Arc(a) => {
let (cx, cy, r) = (a.center.x as f32, a.center.y as f32, a.radius as f32);
let a0 = a.start_angle.to_radians() as f32;
let a1 = a.end_angle.to_radians() as f32;
let span = {
let s = fn_norm(a1) - fn_norm(a0);
if s <= 0.0 {
s + FTAU
} else {
s
}
};
let d = (count - 1).max(1) as f32;
(0..count)
.map(|i| {
let ang = fn_norm(a0) + span * (i as f32 / d);
Vec3::new(cx + r * ang.cos(), cy + r * ang.sin(), 0.0)
})
.collect()
}
EntityType::Circle(c) => {
let (cx, cy, r) = (c.center.x as f32, c.center.y as f32, c.radius as f32);
(0..count)
.map(|i| {
let ang = i as f32 / count as f32 * FTAU;
Vec3::new(cx + r * ang.cos(), cy + r * ang.sin(), 0.0)
})
.collect()
}
EntityType::LwPolyline(p) => subsample_equidistant(&lw_dense_pts(p), count),
_ => vec![Vec3::ZERO; count],
}
}
/// Build Translate transforms: delta from pts[0] to each subsequent point.
fn build_transforms(pts: &[Vec3]) -> Vec<EntityTransform> {
if pts.len() < 2 {
return vec![];
}
let origin = pts[0];
pts[1..]
.iter()
.map(|&p| EntityTransform::Translate(p - origin))
.collect()
}
}
impl CadCommand for ArrayPathCommand {
fn name(&self) -> &'static str {
"ARRAYPATH"
}
fn prompt(&self) -> String {
match &self.step {
PathStep::SelectPath => format!(
"ARRAYPATH Select path entity [{} objects]:",
self.handles.len()
),
PathStep::Count { .. } => {
format!("ARRAYPATH Enter item count <{}>:", self.default_count)
}
}
}
fn needs_entity_pick(&self) -> bool {
matches!(self.step, PathStep::SelectPath)
}
fn wants_text_input(&self) -> bool {
matches!(self.step, PathStep::Count { .. })
}
fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult {
if handle.is_null() || self.handles.contains(&handle) {
return CmdResult::NeedPoint;
}
if let Some(entity) = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
.cloned()
{
self.step = PathStep::Count {
path_entity: entity,
};
}
CmdResult::NeedPoint
}
fn on_hover_entity(&mut self, handle: Handle, _pt: Vec3) -> Vec<WireModel> {
if handle.is_null() || self.handles.contains(&handle) {
return vec![];
}
if !matches!(self.step, PathStep::SelectPath) {
return vec![];
}
if let Some(entity) = self
.all_entities
.iter()
.find(|e| e.common().handle == handle)
{
let pts = Self::sample_path(entity, 64);
if pts.len() >= 2 {
return vec![WireModel::solid(
"arraypath_hover".into(),
pts.iter().map(|p| [p.x, p.y, p.z]).collect(),
WireModel::CYAN,
false,
)];
}
}
vec![]
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let PathStep::Count { path_entity } = &self.step else {
return None;
};
let t = text.trim().replace(',', ".");
let count = if t.is_empty() {
self.default_count
} else {
let v = t.parse::<u32>().unwrap_or(self.default_count).max(2);
defaults::set_array_path_count(v as f32);
self.default_count = v;
v
};
let pts = Self::sample_path(path_entity, count as usize);
let transforms = Self::build_transforms(&pts);
if transforms.is_empty() {
return Some(CmdResult::Cancel);
}
Some(CmdResult::BatchCopy(self.handles.clone(), transforms))
}
fn on_preview_wires(&mut self, _pt: Vec3) -> Vec<WireModel> {
let PathStep::Count { path_entity } = &self.step else {
return vec![];
};
let pts = Self::sample_path(path_entity, self.default_count as usize);
let transforms = Self::build_transforms(&pts);
transforms
.iter()
.flat_map(|t| {
if let EntityTransform::Translate(delta) = t {
self.wire_models
.iter()
.map(|w| w.translated(*delta))
.collect::<Vec<_>>()
} else {
vec![]
}
})
.collect()
}
fn on_point(&mut self, _pt: Vec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
self.on_text_input("").map_or(CmdResult::NeedPoint, |r| r)
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
}