Files
OneCAD-Draft/src/scene/view/dispatch.rs
T
Hakan Seven acde01cfc3 Merge pull request #1289 from ramox81/fix/coincident-standard-command
# Conflicts:
#	src/command.rs
#	src/scene/mod.rs
#	src/scene/parametric_solve.rs
2026-09-15 15:08:35 +03:00

426 lines
15 KiB
Rust

// Dispatch entry points for entity editing.
use acadrust::types::{Color as AcadColor, LineWeight, Transparency};
use acadrust::{EntityType, Handle};
use crate::command::EntityTransform;
use crate::entities::traits::EntityTypeOps;
use crate::scene::model::object::{GripDef, PropSection};
use crate::scene::cache::properties;
thread_local! {
/// Which vertex a multi-vertex entity's Properties panel is focused on
/// (Current Vertex stepper). Set by the app from its `prop_vertex` state
/// before building or editing a polyline's properties; read by the
/// polyline `properties` / `apply_geom_prop` so the X/Y and per-vertex
/// width rows target that vertex. A thread-local keeps the per-entity trait
/// signatures unchanged (mirrors the curve-tolerance override).
static PROP_CURRENT_VERTEX: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
/// Focus the Properties panel on vertex `i` for the next properties build / edit.
pub fn set_prop_current_vertex(i: usize) {
PROP_CURRENT_VERTEX.with(|c| c.set(i));
}
/// The vertex the Properties panel is focused on.
pub fn prop_current_vertex() -> usize {
PROP_CURRENT_VERTEX.with(|c| c.get())
}
/// Extrusion normal of a planar (OCS-stored) entity when it isn't the
/// default +Z. `None` = the entity stores WCS coordinates (LINE, POINT,
/// ELLIPSE, MTEXT, ...) or maps its own OCS internally (INSERT) — grips pass
/// through untouched. Per-entity grip code works in raw field space, so the
/// dispatch layer converts: display grips OCS→WCS, incoming edits WCS→OCS.
fn planar_ocs_normal(entity: &EntityType) -> Option<(f64, f64, f64)> {
let n = match entity {
EntityType::Arc(e) => e.normal,
EntityType::Circle(e) => e.normal,
EntityType::LwPolyline(e) => e.normal,
EntityType::Polyline2D(e) => e.normal,
EntityType::Solid(e) => e.normal,
EntityType::Text(e) => e.normal,
EntityType::AttributeDefinition(e) => e.normal,
EntityType::Hatch(e) => e.normal,
EntityType::Shape(e) => e.normal,
_ => return None,
};
if n.x == 0.0 && n.y == 0.0 && n.z == 1.0 {
None
} else {
Some((n.x, n.y, n.z))
}
}
pub fn grips(entity: &EntityType) -> Vec<GripDef> {
let mut grips = EntityTypeOps::grips(entity);
if let Some(n) = planar_ocs_normal(entity) {
for g in &mut grips {
let (x, y, z) =
super::transform::ocs_point_to_wcs((g.world.x, g.world.y, g.world.z), n);
g.world = glam::DVec3::new(x, y, z);
}
}
grips
}
pub fn properties_sectioned(
handle: Handle,
entity: &EntityType,
text_style_names: &[String],
) -> Vec<PropSection> {
let mut sections = vec![properties::general_section(entity)];
if let Some(viz) = properties::visualization_section(entity) {
sections.push(viz);
}
let groups = entity.geometry_properties(text_style_names);
if groups.is_empty() {
sections.push(properties::fallback_properties(handle, entity));
} else {
sections.extend(groups);
}
if let Some(thickness) = entity_thickness(entity) {
let already_present = sections.iter().flat_map(|section| &section.props).any(|property| {
property.field == "thickness"
|| property.field == "sl_thickness"
|| property.label == crate::t!("Thickness").as_ref()
});
if !already_present {
if let Some(geometry) = sections
.iter_mut()
.find(|section| section.title == crate::t!("Geometry").as_ref())
{
geometry.props.push(crate::entities::common::edit_prop(
crate::t!("Thickness").as_ref(),
"thickness",
thickness,
));
}
}
}
sections
}
pub fn entity_in_working_plane(
entity: &EntityType,
plane: crate::command::WorkingPlane,
) -> EntityType {
let mut local = entity.clone();
if !plane.is_identity() {
apply_transform(
&mut local,
&EntityTransform::Affine(plane.to_local_transform()),
);
}
local
}
pub fn apply_geom_prop_in_working_plane(
entity: &mut EntityType,
field: &str,
value: &str,
plane: crate::command::WorkingPlane,
) {
if plane.is_identity() {
apply_geom_prop(entity, field, value);
return;
}
let mut local = entity_in_working_plane(entity, plane);
apply_geom_prop(&mut local, field, value);
apply_transform(
&mut local,
&EntityTransform::Affine(plane.to_world_transform()),
);
*entity = local;
}
pub fn apply_common_prop(entity: &mut EntityType, field: &str, value: &str) {
let e = entity.as_entity_mut();
match field {
"layer" => e.set_layer(value.to_string()),
"linetype" => {
entity.common_mut().linetype = if value == "ByLayer" {
String::new()
} else {
value.to_string()
};
}
"linetype_scale" => {
if let Ok(v) = value.trim().parse::<f64>() {
if v > 0.0 {
entity.common_mut().linetype_scale = v;
}
}
}
"transparency" => {
let s = value.trim();
if s.eq_ignore_ascii_case("ByLayer") {
entity
.as_entity_mut()
.set_transparency(Transparency::BY_LAYER);
} else if s.eq_ignore_ascii_case("ByBlock") {
entity
.as_entity_mut()
.set_transparency(Transparency::BY_BLOCK);
} else {
let clean = s.trim_end_matches('%').trim();
if let Ok(pct) = clean.parse::<f64>() {
let alpha = (pct.clamp(0.0, 90.0) / 100.0 * 255.0).round() as u8;
entity
.as_entity_mut()
.set_transparency(Transparency::new(alpha));
}
}
}
"material" => {
let common = entity.common_mut();
match value.trim() {
"ByLayer" => {
common.material_flags = 0;
common.material_handle = None;
}
"ByBlock" => {
common.material_flags = 1;
common.material_handle = None;
}
"Global" => {
common.material_flags = 2;
common.material_handle = None;
}
_ => {}
}
}
"plot_style" => {
let common = entity.common_mut();
match value.trim() {
"ByLayer" => {
common.plotstyle_flags = 0;
common.plotstyle_handle = None;
}
"ByBlock" => {
common.plotstyle_flags = 1;
common.plotstyle_handle = None;
}
"Normal" => {
common.plotstyle_flags = 2;
common.plotstyle_handle = None;
}
_ => {}
}
}
"thickness" => {
if let Ok(v) = value.trim().parse::<f64>() {
set_entity_thickness(entity, v);
}
}
_ => {}
}
}
/// Replace — or, when `values` is `None`, remove — the single XDATA record for
/// application `app` on the entity `handle`, leaving every other record intact.
///
/// This is document-level rather than entity-level for two DWG-correctness
/// reasons the entity alone can't satisfy:
/// * the writer skips a record whose application is not registered in the
/// APPID table, so a brand-new hyperlink/override would silently vanish —
/// register the app (with a real handle) first;
/// * on a DWG read each application's XDATA is captured verbatim in
/// `raw_dwg_eed`, and that verbatim blob *wins* over a structured record on
/// save. Editing an existing record therefore also has to drop that app's
/// stale verbatim block (matched by APPID handle) so the new value wins.
/// Other applications' verbatim blocks are preserved for round-trip fidelity.
pub fn set_entity_xdata(
doc: &mut acadrust::CadDocument,
handle: acadrust::Handle,
app: &str,
values: Option<Vec<acadrust::xdata::XDataValue>>,
) {
if values.is_some() && !doc.app_ids.contains(app) {
let mut a = acadrust::tables::AppId::new(app);
a.handle = doc.allocate_handle();
let _ = doc.app_ids.add(a);
}
let app_handle = doc.app_ids.get(app).map(|a| a.handle.value());
let Some(entity) = doc.get_entity_mut(handle) else {
return;
};
let common = entity.common_mut();
let mut rebuilt = acadrust::xdata::ExtendedData::new();
for r in common.extended_data.records() {
if r.application_name != app {
rebuilt.add_record(r.clone());
}
}
if let Some(vals) = values {
let mut rec = acadrust::xdata::ExtendedDataRecord::new(app);
for v in vals {
rec.add_value(v);
}
rebuilt.add_record(rec);
}
rebuilt.raw_dwg_eed = common
.extended_data
.raw_dwg_eed
.iter()
.filter(|(h, _)| Some(*h) != app_handle)
.cloned()
.collect();
common.extended_data = rebuilt;
}
/// The extrusion thickness (DXF 39) of the entities that carry one, or `None`
/// for entity types that have none. The Properties panel uses this to append
/// the shared field to each entity's Geometry group.
pub fn entity_thickness(entity: &EntityType) -> Option<f64> {
Some(match entity {
EntityType::Arc(e) => e.thickness,
EntityType::Circle(e) => e.thickness,
EntityType::Line(e) => e.thickness,
EntityType::LwPolyline(e) => e.thickness,
EntityType::Point(e) => e.thickness,
EntityType::PolyfaceMesh(e) => e.thickness,
EntityType::Polyline2D(e) => e.thickness,
EntityType::Shape(e) => e.thickness,
EntityType::Solid(e) => e.thickness,
EntityType::Text(e) => e.thickness,
_ => return None,
})
}
/// Set the extrusion thickness on the entity types that carry one; no-op for
/// the rest.
pub fn set_entity_thickness(entity: &mut EntityType, v: f64) {
match entity {
EntityType::Arc(e) => e.thickness = v,
EntityType::Circle(e) => e.thickness = v,
EntityType::Line(e) => e.thickness = v,
EntityType::LwPolyline(e) => e.thickness = v,
EntityType::Point(e) => e.thickness = v,
EntityType::PolyfaceMesh(e) => e.thickness = v,
EntityType::Polyline2D(e) => e.thickness = v,
EntityType::Shape(e) => e.thickness = v,
EntityType::Solid(e) => e.thickness = v,
EntityType::Text(e) => e.thickness = v,
_ => {}
}
}
pub fn toggle_invisible(entity: &mut EntityType) {
let cur = entity.as_entity_mut().is_invisible();
entity.as_entity_mut().set_invisible(!cur);
}
pub fn apply_color(entity: &mut EntityType, color: AcadColor) {
entity.as_entity_mut().set_color(color);
let common = entity.common_mut();
common.color_name = None;
common.color_book_handle = None;
}
pub fn apply_line_weight(entity: &mut EntityType, lw: LineWeight) {
entity.as_entity_mut().set_line_weight(lw);
}
pub fn apply_geom_prop(entity: &mut EntityType, field: &str, value: &str) {
EntityTypeOps::apply_geom_prop(entity, field, value);
}
pub fn apply_grip(entity: &mut EntityType, grip_id: usize, apply: crate::scene::model::object::GripApply) {
use crate::scene::model::object::GripApply;
// Grip drags arrive in world space; per-entity apply code writes raw
// fields, which for planar entities live in OCS — convert first (the
// pure-rotation map handles points and deltas alike).
let apply = match (planar_ocs_normal(entity), apply) {
(Some(n), GripApply::Absolute(p)) => {
let (x, y, z) = super::transform::wcs_point_to_ocs((p.x, p.y, p.z), n);
GripApply::Absolute(glam::DVec3::new(x, y, z))
}
(Some(n), GripApply::Translate(d)) => {
let (x, y, z) = super::transform::wcs_point_to_ocs((d.x, d.y, d.z), n);
GripApply::Translate(glam::DVec3::new(x, y, z))
}
(None, a) => a,
};
EntityTypeOps::apply_grip(entity, grip_id, apply);
}
/// Rebuild an arc from its drag-start control points and the point grips moved
/// in the current frame. Returns `None` for non-arc entities and `Some(false)`
/// when the requested points are temporarily degenerate.
pub fn refit_arc_grips(
entity: &mut EntityType,
original: &EntityType,
edits: &[(usize, glam::DVec3)],
) -> Option<bool> {
let EntityType::Arc(arc) = entity else {
return None;
};
let EntityType::Arc(original_arc) = original else {
return None;
};
let edits = match planar_ocs_normal(original) {
Some(normal) => edits
.iter()
.map(|&(grip_id, point)| {
let (x, y, z) =
super::transform::wcs_point_to_ocs((point.x, point.y, point.z), normal);
(grip_id, glam::DVec3::new(x, y, z))
})
.collect::<Vec<_>>(),
None => edits.to_vec(),
};
Some(crate::entities::arc::refit_grips(
arc,
original_arc,
&edits,
))
}
/// Convert a world-space cursor point into the value expected by an
/// interactive grip-menu action.
pub fn grip_menu_point_value(
entity: &EntityType,
grip_id: usize,
action: crate::scene::model::object::GripMenuAction,
point: glam::DVec3,
) -> Option<f64> {
let point = match planar_ocs_normal(entity) {
Some(normal) => {
let (x, y, z) =
super::transform::wcs_point_to_ocs((point.x, point.y, point.z), normal);
glam::DVec3::new(x, y, z)
}
None => point,
};
EntityTypeOps::grip_menu_point_value(entity, grip_id, action, point)
}
pub fn apply_transform(entity: &mut EntityType, t: &EntityTransform) {
EntityTypeOps::apply_transform(entity, t);
}
#[cfg(test)]
mod tests {
use super::*;
use acadrust::types::Vector3;
#[test]
fn line_thickness_follows_the_entity_geometry_rows() {
let mut line = acadrust::entities::Line::from_points(Vector3::ZERO, Vector3::UNIT_X);
line.thickness = 2.5;
let sections = properties_sectioned(Handle::NULL, &EntityType::Line(line), &[]);
assert!(!sections[0]
.props
.iter()
.any(|property| property.field == "thickness"));
let geometry = sections
.iter()
.find(|section| section.props.iter().any(|property| property.field == "start_x"))
.unwrap();
assert_eq!(geometry.props.last().unwrap().field, "thickness");
}
}