2026-08-09 01:44:04 +03:00
|
|
|
use acadrust::kernel::space::polygon;
|
2026-08-03 22:31:20 +03:00
|
|
|
use acadrust::{EntityType, Handle};
|
feat(commands): integrate standard CAD commands and fix behavior gaps
Broad pass over the command set: integrate missing commands, wire
previously-unreachable ribbon/menu buttons, and correct name/behaviour
mismatches. Every UI command token now resolves (real handler where the
infrastructure exists, an honest "not yet implemented" status otherwise).
New commands:
- Draw: 3DPOLY, 2D solid (SO/SOLID2D), HELIX, TRACE, SKETCH, DIMCENTER
(DCE/CENTERMARK)
- Modify/inquiry: REVERSE, MEASUREGEOM (MEA), DBLIST
- Data/manage: BASE, OVERKILL (exact-duplicate removal), AUDIT (read-only
integrity report), SETVAR (system-variable get/set), SCRIPT (.scr runner),
FINDNONPURGEABLE
Behaviour fixes:
- LINE and SPLINE gain Close/Undo; AREA computes area/perimeter in f64
(correct at large coordinates)
- ATTEDIT/ATE unified so the alias and the ribbon button open the
interactive editor; -ATTEDIT is the command-line form
- Linear/aligned/radius/diameter dimensions gain a text override (Text) and
text angle (Angle) option
- INSERT gains Scale/Rotate options; COPY gains an Array option
- LINETYPE SET sets the current entity linetype; SETBYLAYER resets overrides
- UNDO accepts a step count
Integrations: standard dimension/block aliases (DLI/DRA/DAN, BMAKE,
EXPORTPDF, DDIM), drafting-aid and screen toggles (GRID/SNAP/POLAR/OSNAP/
DSETTINGS/CLEANSCREEN/QUICKPROPERTIES), viewport tiling
(CASCADE/HORIZONTAL/VERTICAL/VPJOIN), and customization entry points
(CUI/ALIASEDIT).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 19:27:35 +03:00
|
|
|
use glam::DVec3;
|
2026-04-02 23:36:53 +03:00
|
|
|
|
2026-08-04 00:01:12 +03:00
|
|
|
use crate::command::{
|
|
|
|
|
AreaPreviewRegion, AreaPreviewSource, CadCommand, CmdOption, CmdResult, SelectionEntity,
|
|
|
|
|
};
|
2026-08-09 01:44:04 +03:00
|
|
|
use crate::entities::curve::entity_curve;
|
2026-08-03 22:31:20 +03:00
|
|
|
use crate::entities::traits::EntityTypeOps;
|
2026-06-16 11:33:46 +03:00
|
|
|
use crate::scene::model::wire_model::WireModel;
|
2026-04-02 23:36:53 +03:00
|
|
|
|
2026-08-03 22:31:20 +03:00
|
|
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
|
|
|
enum AreaMode {
|
|
|
|
|
Single,
|
|
|
|
|
Add,
|
|
|
|
|
Subtract,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy)]
|
|
|
|
|
struct AreaMeasurement {
|
|
|
|
|
area: f64,
|
|
|
|
|
perimeter: Option<f64>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 23:36:53 +03:00
|
|
|
pub struct AreaCommand {
|
2026-08-03 22:31:20 +03:00
|
|
|
mode: AreaMode,
|
feat(commands): integrate standard CAD commands and fix behavior gaps
Broad pass over the command set: integrate missing commands, wire
previously-unreachable ribbon/menu buttons, and correct name/behaviour
mismatches. Every UI command token now resolves (real handler where the
infrastructure exists, an honest "not yet implemented" status otherwise).
New commands:
- Draw: 3DPOLY, 2D solid (SO/SOLID2D), HELIX, TRACE, SKETCH, DIMCENTER
(DCE/CENTERMARK)
- Modify/inquiry: REVERSE, MEASUREGEOM (MEA), DBLIST
- Data/manage: BASE, OVERKILL (exact-duplicate removal), AUDIT (read-only
integrity report), SETVAR (system-variable get/set), SCRIPT (.scr runner),
FINDNONPURGEABLE
Behaviour fixes:
- LINE and SPLINE gain Close/Undo; AREA computes area/perimeter in f64
(correct at large coordinates)
- ATTEDIT/ATE unified so the alias and the ribbon button open the
interactive editor; -ATTEDIT is the command-line form
- Linear/aligned/radius/diameter dimensions gain a text override (Text) and
text angle (Angle) option
- INSERT gains Scale/Rotate options; COPY gains an Array option
- LINETYPE SET sets the current entity linetype; SETBYLAYER resets overrides
- UNDO accepts a step count
Integrations: standard dimension/block aliases (DLI/DRA/DAN, BMAKE,
EXPORTPDF, DDIM), drafting-aid and screen toggles (GRID/SNAP/POLAR/OSNAP/
DSETTINGS/CLEANSCREEN/QUICKPROPERTIES), viewport tiling
(CASCADE/HORIZONTAL/VERTICAL/VPJOIN), and customization entry points
(CUI/ALIASEDIT).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 19:27:35 +03:00
|
|
|
points: Vec<DVec3>,
|
2026-08-04 00:01:12 +03:00
|
|
|
objects_gathering: bool,
|
|
|
|
|
selected_entities: Vec<SelectionEntity>,
|
|
|
|
|
preview_regions: Vec<AreaPreviewRegion>,
|
2026-08-03 22:31:20 +03:00
|
|
|
total_area: f64,
|
|
|
|
|
total_perimeter: f64,
|
2026-04-02 23:36:53 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl AreaCommand {
|
|
|
|
|
pub fn new() -> Self {
|
2026-08-03 22:31:20 +03:00
|
|
|
Self {
|
|
|
|
|
mode: AreaMode::Single,
|
|
|
|
|
points: Vec::new(),
|
2026-08-04 00:01:12 +03:00
|
|
|
objects_gathering: false,
|
|
|
|
|
selected_entities: Vec::new(),
|
|
|
|
|
preview_regions: Vec::new(),
|
2026-08-03 22:31:20 +03:00
|
|
|
total_area: 0.0,
|
|
|
|
|
total_perimeter: 0.0,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn option(label: String, keyword: &str) -> CmdOption {
|
|
|
|
|
CmdOption { label, keyword: keyword.to_string() }
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-09 01:44:04 +03:00
|
|
|
/// Area and perimeter of a ring of picked points.
|
|
|
|
|
///
|
|
|
|
|
/// Measured in space rather than in projection — a picked sequence need
|
|
|
|
|
/// not lie in a coordinate plane, and flattening it to XY first would
|
|
|
|
|
/// report its shadow.
|
2026-08-03 22:31:20 +03:00
|
|
|
fn point_measurement(points: &[DVec3], close_perimeter: bool) -> AreaMeasurement {
|
|
|
|
|
if points.len() < 2 {
|
|
|
|
|
return AreaMeasurement { area: 0.0, perimeter: Some(0.0) };
|
|
|
|
|
}
|
2026-08-09 01:44:04 +03:00
|
|
|
let ring: Vec<[f64; 3]> = points.iter().map(|p| [p.x, p.y, p.z]).collect();
|
|
|
|
|
let perimeter = if close_perimeter {
|
|
|
|
|
polygon::perimeter(&ring)
|
|
|
|
|
} else {
|
|
|
|
|
polygon::chain_length(&ring)
|
|
|
|
|
};
|
|
|
|
|
AreaMeasurement {
|
|
|
|
|
area: polygon::area(&ring),
|
|
|
|
|
perimeter: Some(perimeter),
|
2026-08-03 22:31:20 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-09 01:44:04 +03:00
|
|
|
/// Area and perimeter of an entity whose geometry is a planar curve.
|
|
|
|
|
///
|
|
|
|
|
/// Both exact: the kernel measures the curve rather than a polygon
|
|
|
|
|
/// through some of its points, so a bulged polyline counts each arc's own
|
|
|
|
|
/// bite and length, and a spline is integrated rather than sampled.
|
|
|
|
|
fn curve_measurement(entity: &EntityType) -> Option<AreaMeasurement> {
|
|
|
|
|
let curve = entity_curve(entity)?;
|
|
|
|
|
let perimeter = curve.length();
|
|
|
|
|
Some(AreaMeasurement {
|
|
|
|
|
area: curve.curve.enclosed_area().abs(),
|
|
|
|
|
perimeter: perimeter.is_finite().then_some(perimeter),
|
|
|
|
|
})
|
2026-08-03 22:31:20 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn entity_measurement(entity: &EntityType) -> Option<AreaMeasurement> {
|
2026-08-09 01:44:04 +03:00
|
|
|
// Anything the converter recognises is measured from its own curve.
|
|
|
|
|
if let Some(measurement) = Self::curve_measurement(entity) {
|
|
|
|
|
return Some(measurement);
|
|
|
|
|
}
|
2026-08-03 22:31:20 +03:00
|
|
|
match entity {
|
2026-08-09 01:44:04 +03:00
|
|
|
// What is left is the genuinely spatial: a 3D polyline, and a
|
|
|
|
|
// spline whose points wander off any plane. Both are measured as
|
|
|
|
|
// rings of points in space.
|
2026-08-03 22:31:20 +03:00
|
|
|
EntityType::Polyline(polyline) => {
|
|
|
|
|
let points = polyline
|
|
|
|
|
.vertices
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|vertex| {
|
|
|
|
|
DVec3::new(vertex.location.x, vertex.location.y, vertex.location.z)
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
Some(Self::point_measurement(&points, polyline.is_closed()))
|
|
|
|
|
}
|
|
|
|
|
EntityType::Polyline3D(polyline) => {
|
|
|
|
|
let points = polyline
|
|
|
|
|
.vertices
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|vertex| {
|
|
|
|
|
DVec3::new(vertex.position.x, vertex.position.y, vertex.position.z)
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
Some(Self::point_measurement(&points, polyline.is_closed()))
|
|
|
|
|
}
|
|
|
|
|
EntityType::Spline(spline) => {
|
|
|
|
|
let points = crate::entities::spline::measurement_polyline(spline)
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|point| DVec3::new(point[0], point[1], point[2]))
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
Some(Self::point_measurement(
|
|
|
|
|
&points,
|
|
|
|
|
spline.flags.closed || spline.flags.periodic,
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
_ => entity.mass_props().map(|props| AreaMeasurement {
|
|
|
|
|
area: props.area,
|
|
|
|
|
perimeter: Some(props.perimeter),
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 00:01:12 +03:00
|
|
|
fn selection_entity_measurement(entity: &SelectionEntity) -> Option<AreaMeasurement> {
|
|
|
|
|
Self::entity_measurement(&entity.entity).or_else(|| {
|
|
|
|
|
entity.surface_area.map(|area| AreaMeasurement {
|
|
|
|
|
area,
|
|
|
|
|
perimeter: None,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn selection_measurement(&self) -> Option<(AreaMeasurement, Vec<Handle>)> {
|
|
|
|
|
let measured = self
|
|
|
|
|
.selected_entities
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|entity| {
|
|
|
|
|
Self::selection_entity_measurement(entity)
|
|
|
|
|
.map(|measurement| (entity.handle, measurement))
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
if measured.is_empty() {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let area = measured.iter().map(|(_, measurement)| measurement.area).sum();
|
|
|
|
|
let perimeter = measured
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|(_, measurement)| measurement.perimeter)
|
|
|
|
|
.collect::<Option<Vec<_>>>()
|
|
|
|
|
.map(|values| values.into_iter().sum());
|
|
|
|
|
let handles = measured.into_iter().map(|(handle, _)| handle).collect();
|
|
|
|
|
Some((AreaMeasurement { area, perimeter }, handles))
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-03 22:31:20 +03:00
|
|
|
fn result_message(measurement: AreaMeasurement) -> String {
|
|
|
|
|
match measurement.perimeter {
|
|
|
|
|
Some(perimeter) => crate::tr!(
|
|
|
|
|
"area-result",
|
|
|
|
|
area = format!("{:.4}", measurement.area),
|
|
|
|
|
perimeter = format!("{perimeter:.4}"),
|
|
|
|
|
),
|
|
|
|
|
None => crate::tr!("area-result-area-only", area = format!("{:.4}", measurement.area)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn running_result_message(&self, measurement: AreaMeasurement) -> String {
|
|
|
|
|
match measurement.perimeter {
|
|
|
|
|
Some(perimeter) => crate::tr!(
|
|
|
|
|
"area-running-result",
|
|
|
|
|
area = format!("{:.4}", measurement.area),
|
|
|
|
|
perimeter = format!("{perimeter:.4}"),
|
|
|
|
|
total_area = format!("{:.4}", self.total_area),
|
|
|
|
|
total_perimeter = format!("{:.4}", self.total_perimeter),
|
|
|
|
|
),
|
|
|
|
|
None => crate::tr!(
|
|
|
|
|
"area-running-result-area-only",
|
|
|
|
|
area = format!("{:.4}", measurement.area),
|
|
|
|
|
total_area = format!("{:.4}", self.total_area),
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 00:01:12 +03:00
|
|
|
fn finish_measurement(
|
|
|
|
|
&mut self,
|
|
|
|
|
measurement: AreaMeasurement,
|
|
|
|
|
deselect: bool,
|
|
|
|
|
) -> CmdResult {
|
2026-08-03 22:31:20 +03:00
|
|
|
if self.mode == AreaMode::Single {
|
|
|
|
|
return CmdResult::Measurement(Self::result_message(measurement));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let sign = if self.mode == AreaMode::Add { 1.0 } else { -1.0 };
|
|
|
|
|
self.total_area += sign * measurement.area;
|
|
|
|
|
if let Some(perimeter) = measurement.perimeter {
|
|
|
|
|
self.total_perimeter += sign * perimeter;
|
|
|
|
|
}
|
|
|
|
|
self.points.clear();
|
2026-08-04 00:01:12 +03:00
|
|
|
self.objects_gathering = false;
|
|
|
|
|
self.selected_entities.clear();
|
|
|
|
|
let message = self.running_result_message(measurement);
|
|
|
|
|
if deselect {
|
|
|
|
|
CmdResult::ReportMeasurementAndDeselect(message)
|
|
|
|
|
} else {
|
|
|
|
|
CmdResult::ReportMeasurement(message)
|
|
|
|
|
}
|
2026-04-02 23:36:53 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl CadCommand for AreaCommand {
|
|
|
|
|
fn name(&self) -> &'static str {
|
|
|
|
|
"AREA"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn prompt(&self) -> String {
|
2026-08-04 00:01:12 +03:00
|
|
|
if self.objects_gathering {
|
|
|
|
|
return crate::tr!(
|
|
|
|
|
"area-prompt-objects",
|
|
|
|
|
count = self.selected_entities.len()
|
|
|
|
|
);
|
2026-04-02 23:36:53 +03:00
|
|
|
}
|
2026-08-03 22:31:20 +03:00
|
|
|
if !self.points.is_empty() {
|
|
|
|
|
return crate::tr!("area-prompt-next", count = self.points.len());
|
|
|
|
|
}
|
|
|
|
|
match self.mode {
|
|
|
|
|
AreaMode::Single => crate::tr!("area-prompt-first"),
|
|
|
|
|
AreaMode::Add => crate::tr!("area-prompt-add"),
|
|
|
|
|
AreaMode::Subtract => crate::tr!("area-prompt-subtract"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn options(&self) -> Vec<CmdOption> {
|
2026-08-04 00:01:12 +03:00
|
|
|
if self.objects_gathering {
|
|
|
|
|
return vec![Self::option(crate::tr!("area-option-back"), "BACK")];
|
|
|
|
|
}
|
|
|
|
|
if !self.points.is_empty() {
|
2026-08-03 22:31:20 +03:00
|
|
|
return Vec::new();
|
|
|
|
|
}
|
2026-08-04 00:01:12 +03:00
|
|
|
let objects = || Self::option(crate::tr!("area-option-objects"), "OBJECTS");
|
2026-08-03 22:31:20 +03:00
|
|
|
let add = || Self::option(crate::tr!("area-option-add"), "ADD");
|
|
|
|
|
let subtract = || Self::option(crate::tr!("area-option-subtract"), "SUBTRACT");
|
|
|
|
|
match self.mode {
|
2026-08-04 00:01:12 +03:00
|
|
|
AreaMode::Single => vec![objects(), add(), subtract()],
|
|
|
|
|
AreaMode::Add => vec![objects(), subtract()],
|
|
|
|
|
AreaMode::Subtract => vec![objects(), add()],
|
2026-08-03 22:31:20 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn wants_text_input(&self) -> bool {
|
2026-08-04 00:01:12 +03:00
|
|
|
!self.objects_gathering
|
2026-08-03 22:31:20 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn point_step_accepts_keywords(&self) -> bool {
|
2026-08-04 00:01:12 +03:00
|
|
|
!self.objects_gathering
|
2026-08-03 22:31:20 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
2026-08-04 00:01:12 +03:00
|
|
|
let input = text.trim().to_ascii_uppercase();
|
|
|
|
|
if self.objects_gathering {
|
|
|
|
|
if matches!(input.as_str(), "B" | "BACK") {
|
|
|
|
|
if let Some((measurement, handles)) = self.selection_measurement() {
|
|
|
|
|
if self.mode != AreaMode::Single {
|
|
|
|
|
self.preview_regions.push(AreaPreviewRegion {
|
|
|
|
|
source: AreaPreviewSource::Handles(handles),
|
|
|
|
|
subtract: self.mode == AreaMode::Subtract,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return Some(self.finish_measurement(measurement, true));
|
|
|
|
|
}
|
|
|
|
|
self.objects_gathering = false;
|
|
|
|
|
self.selected_entities.clear();
|
|
|
|
|
return Some(CmdResult::DeselectAndContinue);
|
|
|
|
|
}
|
2026-08-03 22:31:20 +03:00
|
|
|
return Some(CmdResult::NeedPoint);
|
|
|
|
|
}
|
2026-08-04 00:01:12 +03:00
|
|
|
if !self.points.is_empty() {
|
|
|
|
|
return Some(CmdResult::NeedPoint);
|
|
|
|
|
}
|
|
|
|
|
match input.as_str() {
|
|
|
|
|
"O" | "OBJECT" | "OBJECTS" => self.objects_gathering = true,
|
2026-08-03 22:31:20 +03:00
|
|
|
"A" | "ADD" => self.mode = AreaMode::Add,
|
|
|
|
|
"S" | "SUBTRACT" => self.mode = AreaMode::Subtract,
|
|
|
|
|
_ => return Some(CmdResult::NeedPoint),
|
|
|
|
|
}
|
|
|
|
|
Some(CmdResult::NeedPoint)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 00:01:12 +03:00
|
|
|
fn is_selection_gathering(&self) -> bool {
|
|
|
|
|
self.objects_gathering
|
2026-08-03 22:31:20 +03:00
|
|
|
}
|
|
|
|
|
|
2026-08-04 00:01:12 +03:00
|
|
|
fn selection_forces_add(&self) -> bool {
|
|
|
|
|
self.objects_gathering
|
2026-08-03 22:31:20 +03:00
|
|
|
}
|
|
|
|
|
|
2026-08-04 00:01:12 +03:00
|
|
|
fn inject_selection_entities(&mut self, entities: Vec<SelectionEntity>) {
|
|
|
|
|
self.selected_entities = entities;
|
2026-08-03 22:31:20 +03:00
|
|
|
}
|
|
|
|
|
|
2026-08-04 00:01:12 +03:00
|
|
|
fn on_selection_complete(&mut self, _handles: Vec<Handle>) -> CmdResult {
|
|
|
|
|
CmdResult::NeedPoint
|
2026-08-03 22:31:20 +03:00
|
|
|
}
|
|
|
|
|
|
2026-08-04 00:01:12 +03:00
|
|
|
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
|
|
|
|
if self.objects_gathering {
|
2026-08-03 22:31:20 +03:00
|
|
|
return CmdResult::NeedPoint;
|
|
|
|
|
}
|
|
|
|
|
self.points.push(point);
|
2026-04-02 23:36:53 +03:00
|
|
|
CmdResult::NeedPoint
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn on_enter(&mut self) -> CmdResult {
|
2026-08-04 00:01:12 +03:00
|
|
|
if self.objects_gathering {
|
|
|
|
|
let Some((measurement, handles)) = self.selection_measurement() else {
|
|
|
|
|
self.selected_entities.clear();
|
|
|
|
|
return CmdResult::ReportMeasurementAndDeselect(crate::tr!(
|
|
|
|
|
"area-objects-not-measurable"
|
|
|
|
|
));
|
|
|
|
|
};
|
|
|
|
|
if self.mode != AreaMode::Single {
|
|
|
|
|
self.preview_regions.push(AreaPreviewRegion {
|
|
|
|
|
source: AreaPreviewSource::Handles(handles),
|
|
|
|
|
subtract: self.mode == AreaMode::Subtract,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return self.finish_measurement(measurement, true);
|
2026-04-02 23:36:53 +03:00
|
|
|
}
|
|
|
|
|
if self.points.is_empty() {
|
2026-08-04 00:01:12 +03:00
|
|
|
self.objects_gathering = true;
|
2026-08-03 22:31:20 +03:00
|
|
|
return CmdResult::NeedPoint;
|
|
|
|
|
}
|
|
|
|
|
if self.points.len() < 3 {
|
|
|
|
|
return CmdResult::Cancel;
|
|
|
|
|
}
|
|
|
|
|
let measurement = Self::point_measurement(&self.points, true);
|
2026-08-04 00:01:12 +03:00
|
|
|
if self.mode != AreaMode::Single {
|
|
|
|
|
self.preview_regions.push(AreaPreviewRegion {
|
|
|
|
|
source: AreaPreviewSource::Boundary(
|
|
|
|
|
self.points.iter().map(|point| [point.x, point.y]).collect(),
|
|
|
|
|
),
|
|
|
|
|
subtract: self.mode == AreaMode::Subtract,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
self.finish_measurement(measurement, false)
|
2026-08-03 22:31:20 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
|
2026-08-04 00:01:12 +03:00
|
|
|
if self.objects_gathering || self.points.is_empty() {
|
2026-04-02 23:36:53 +03:00
|
|
|
return None;
|
|
|
|
|
}
|
2026-08-03 22:31:20 +03:00
|
|
|
let to_render = |p: DVec3| [p.x as f32, p.y as f32, p.z as f32];
|
|
|
|
|
let mut points = self.points.iter().map(|point| to_render(*point)).collect::<Vec<_>>();
|
|
|
|
|
points.push(to_render(point));
|
|
|
|
|
points.push(to_render(self.points[0]));
|
2026-04-02 23:36:53 +03:00
|
|
|
Some(WireModel {
|
2026-07-20 16:36:06 +03:00
|
|
|
taper_widths: Vec::new(),
|
2026-07-20 14:50:27 +03:00
|
|
|
world_width: 0.0,
|
2026-07-21 15:53:28 +03:00
|
|
|
depth_override: None,
|
2026-07-18 15:15:24 +03:00
|
|
|
fill_is_3d: false,
|
2026-08-04 21:23:25 +03:00
|
|
|
fill_is_2d_solid: false,
|
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(),
|
2026-07-17 01:49:18 +03:00
|
|
|
dash_from_start: false,
|
2026-07-17 02:59:11 +03:00
|
|
|
dash_align_end: None,
|
refactor(text): per-entity SDF text — all text kinds, selection/hover highlight
Rearchitect SDF text from a document-wide collector to per-entity
production during tessellation, so each entity owns its glyph quads and
composite/nested text is covered for free (no re-explosion, no per-frame
document walk).
- WireModel gains `text_verts`: glyph quads ride on the wire, so they are
cached by the tess memo, cloned on hit, and transformed by the block
expand loop exactly like `points`.
- Text arm (tessellate.rs) builds the quads when SDF is on and suppresses
the strokes; render gathers text_verts from the viewport wire set
(`gather_text_verts`, cached on wire content id) — the old collector is
removed. Covers model, dimension, table, block-internal and paper-sheet
text uniformly.
- MLEADER text (lays out via layout_mtext, not the Text arm) gets its own
SDF branch driven by the per-run GlyphRun.
- Pick box comes from the rendered glyph quads (true text extent), not
entity_aabb which mis-places the box for MTEXT; composite paths no longer
clobber the empty text wire's tight AABB (dim pick box hugs the text).
- Selection / hover highlight via a text-highlight overlay (upload_text_
highlight, keyed on selection_generation) mirroring the selected-wire
xray overlay; base glyphs stay neutral so a deselect leaves no stale tint.
- Debug: env OCS_TEXT_BOX draws each run's glyph-bounds rectangle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 17:01:36 +03:00
|
|
|
text_verts: Vec::new(),
|
2026-04-02 23:36:53 +03:00
|
|
|
name: "area_preview".into(),
|
2026-08-03 22:31:20 +03:00
|
|
|
points,
|
2026-06-23 17:08:54 +03:00
|
|
|
points_low: Vec::new(),
|
2026-04-02 23:36:53 +03:00
|
|
|
color: WireModel::CYAN,
|
|
|
|
|
selected: false,
|
|
|
|
|
pattern_length: 0.0,
|
|
|
|
|
pattern: [0.0; 8],
|
|
|
|
|
line_weight_px: 1.0,
|
2026-08-03 22:31:20 +03:00
|
|
|
snap_pts: Vec::new(),
|
|
|
|
|
tangent_geoms: Vec::new(),
|
2026-04-06 23:19:08 +03:00
|
|
|
aci: 0,
|
2026-08-03 22:31:20 +03:00
|
|
|
key_vertices: Vec::new(),
|
2026-04-26 12:21:51 +03:00
|
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
2026-05-06 06:55:28 +03:00
|
|
|
plinegen: true,
|
2026-08-03 22:31:20 +03:00
|
|
|
fill_tris: Vec::new(),
|
2026-06-23 18:03:40 +03:00
|
|
|
fill_tris_low: Vec::new(),
|
2026-04-02 23:36:53 +03:00
|
|
|
})
|
|
|
|
|
}
|
2026-08-04 00:01:12 +03:00
|
|
|
|
|
|
|
|
fn area_preview_regions(&self) -> Option<Vec<AreaPreviewRegion>> {
|
|
|
|
|
let mut regions = self.preview_regions.clone();
|
|
|
|
|
if self.objects_gathering {
|
|
|
|
|
let handles = self
|
|
|
|
|
.selected_entities
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|entity| entity.handle)
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
if !handles.is_empty() {
|
|
|
|
|
regions.push(AreaPreviewRegion {
|
|
|
|
|
source: AreaPreviewSource::Handles(handles),
|
|
|
|
|
subtract: self.mode == AreaMode::Subtract,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} else if self.points.len() >= 3 {
|
|
|
|
|
regions.push(AreaPreviewRegion {
|
|
|
|
|
source: AreaPreviewSource::Boundary(
|
|
|
|
|
self.points.iter().map(|point| [point.x, point.y]).collect(),
|
|
|
|
|
),
|
|
|
|
|
subtract: self.mode == AreaMode::Subtract,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
Some(regions)
|
|
|
|
|
}
|
2026-04-02 23:36:53 +03:00
|
|
|
}
|
2026-05-26 22:45:54 +03:00
|
|
|
|
2026-08-03 22:31:20 +03:00
|
|
|
inventory::submit!(crate::command::CommandRegistration { names: &["AREA"] });
|