forked from 1b-agent/opencadstudio-upstream
Switching to a Layout re-tessellated the whole model — `memo_miss=186455`, 2034 ms — to arrive at geometry identical to what was already in the memo. The only thing that differed was colour. `local_wires_for` already knows this: it stores the raw colour and drops its background argument with `let _ = bg_color;`, noting that `Batches::finalize` applies `adapt_to_bg` last, "so the cache no longer has to rebuild on BACKGROUND / layout-switch". The tessellation memo's guard had not been told, and still mixed the background in, so every entry was discarded on a switch. `finalize` now records what its colour resolution consumed — the raw colour, the raw glyph colours, and the three flags it branches on — as `WireModel::bg_adapt`, and `resolve_colors_for_bg` replays it for another background. The guard drops the background, so a memo entry serves any layout. The resolution recomputes from the recorded raw colour rather than from the resolved one, which makes it idempotent and therefore safe over a set that mixes memo hits with freshly tessellated misses. Re-adapting the resolved colour would not be safe: `adapt_to_bg` snaps a near-white colour to pure black on a light background, and pure black comes back pure *white* on a dark one, losing the tint. A test pins that asymmetry so the raw colour is not optimised away later. The contract is tested against the real `finalize` rather than a copy of its logic: for every background pair, building under one and resolving for the other lands exactly where building under the other would have. `bg_adapt` is `None` for wires built anywhere else, and the resolution skips them, so nothing outside the block cache changes colour. It is also `None` when the resolution would be a no-op — a preserved colour with no canvas fill looks the same under every background — which keeps the common case at no cost. Ordering is unchanged: the pass runs inside the builder, before `append_scene_markers` and `apply_refedit_fade`, both of which have their own separate background dependency. `wires-build` gains `colors=` so the pass has a number of its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1171 lines
39 KiB
Rust
1171 lines
39 KiB
Rust
use acadrust::entities::{Dimension, DimensionAngular2Ln, DimensionAngular3Pt};
|
|
use acadrust::types::{Handle, Vector3};
|
|
use acadrust::EntityType;
|
|
use cadkernel::geom2d::{
|
|
closest_point, line_line, Arc as KernelArc, BulgeArc, Curve as KernelCurve,
|
|
Line as KernelLine,
|
|
};
|
|
|
|
use crate::command::{
|
|
CadCommand, CmdOption, CmdResult, DimensionAssociationInput,
|
|
DimensionAssociationSource, InputKind, WorkingPlane,
|
|
};
|
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
|
use crate::scene::model::wire_model::WireModel;
|
|
use crate::t;
|
|
use glam::DVec3;
|
|
|
|
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/dim_angular.svg"));
|
|
|
|
pub fn tool() -> ToolDef {
|
|
ToolDef {
|
|
id: "DIMANGULAR",
|
|
label: "Angular",
|
|
icon: ICON,
|
|
event: ModuleEvent::Command("DIMANGULAR".to_string()),
|
|
}
|
|
}
|
|
|
|
enum Step {
|
|
Vertex,
|
|
FirstRay(DVec3),
|
|
SecondRay { vertex: DVec3, first: DVec3 },
|
|
CircleSecondRay {
|
|
vertex: DVec3,
|
|
first: DVec3,
|
|
source: Handle,
|
|
},
|
|
SecondLine {
|
|
first_start: DVec3,
|
|
first_end: DVec3,
|
|
first_source: Handle,
|
|
first_refs: [SourceLocator; 2],
|
|
},
|
|
ArcPoint3 {
|
|
vertex: DVec3,
|
|
first: DVec3,
|
|
second: DVec3,
|
|
},
|
|
ArcPoint2 {
|
|
first_start: DVec3,
|
|
first_end: DVec3,
|
|
second_start: DVec3,
|
|
second_end: DVec3,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
struct SourceLocator {
|
|
marker: Option<i32>,
|
|
parameter: f64,
|
|
}
|
|
|
|
impl SourceLocator {
|
|
const INFERRED: Self = Self { marker: None, parameter: 0.0 };
|
|
|
|
const fn explicit(marker: i32, parameter: f64) -> Self {
|
|
Self { marker: Some(marker), parameter }
|
|
}
|
|
|
|
fn bind(self, handle: Handle) -> DimensionAssociationSource {
|
|
match self.marker {
|
|
Some(marker) => DimensionAssociationSource::explicit(handle, marker, self.parameter),
|
|
None => DimensionAssociationSource::inferred(handle),
|
|
}
|
|
}
|
|
}
|
|
|
|
enum PickedCurve {
|
|
Line {
|
|
start: DVec3,
|
|
end: DVec3,
|
|
refs: [SourceLocator; 2],
|
|
normal: Option<DVec3>,
|
|
},
|
|
Arc {
|
|
center: DVec3,
|
|
first: DVec3,
|
|
second: DVec3,
|
|
refs: [SourceLocator; 3],
|
|
normal: DVec3,
|
|
},
|
|
Circle {
|
|
center: DVec3,
|
|
normal: DVec3,
|
|
},
|
|
}
|
|
|
|
pub struct AngularDimensionCommand {
|
|
step: Step,
|
|
plane: WorkingPlane,
|
|
selecting_object: bool,
|
|
picked_entity: Option<EntityType>,
|
|
source_refs: Vec<Option<DimensionAssociationSource>>,
|
|
text_override: Option<String>,
|
|
awaiting_text: bool,
|
|
mtext_override: bool,
|
|
text_angle: Option<f64>,
|
|
awaiting_angle: bool,
|
|
angle_origin: Option<DVec3>,
|
|
awaiting_quadrant: bool,
|
|
quadrant_lock: Option<(f64, f64)>,
|
|
}
|
|
|
|
impl AngularDimensionCommand {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
step: Step::Vertex,
|
|
plane: WorkingPlane::default(),
|
|
selecting_object: true,
|
|
picked_entity: None,
|
|
source_refs: Vec::new(),
|
|
text_override: None,
|
|
awaiting_text: false,
|
|
mtext_override: false,
|
|
text_angle: None,
|
|
awaiting_angle: false,
|
|
angle_origin: None,
|
|
awaiting_quadrant: false,
|
|
quadrant_lock: None,
|
|
}
|
|
}
|
|
|
|
fn finish_three_point(
|
|
&self,
|
|
vertex: DVec3,
|
|
first: DVec3,
|
|
second: DVec3,
|
|
arc_point: DVec3,
|
|
) -> CmdResult {
|
|
let vertex = self.plane.to_local(vertex);
|
|
let first = self.plane.to_local(first);
|
|
let second = self.plane.to_local(second);
|
|
let picked_text = self.plane.to_local(arc_point);
|
|
let arc_point = locked_arc_point(vertex, picked_text, self.quadrant_lock);
|
|
if two_line_frame(vertex, first, vertex, second, arc_point).is_none() {
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
let mut dim = DimensionAngular3Pt::new(v3(vertex), v3(first), v3(second));
|
|
dim.definition_point = v3(arc_point);
|
|
dim.base.definition_point = dim.definition_point;
|
|
dim.base.text_middle_point = dim.definition_point;
|
|
dim.base.insertion_point = dim.definition_point;
|
|
if self.quadrant_lock.is_some_and(|frame| {
|
|
!point_angle_in_frame(vertex, picked_text, frame)
|
|
}) {
|
|
dim.base.text_middle_point = v3(picked_text);
|
|
dim.base.insertion_point = dim.base.text_middle_point;
|
|
dim.base.text_user_positioned = true;
|
|
}
|
|
dim.base.actual_measurement = dim.measurement_degrees();
|
|
crate::entities::dimension::set_dimension_text_override(
|
|
&mut dim.base,
|
|
self.text_override.clone(),
|
|
);
|
|
if let Some(angle) = self.text_angle {
|
|
dim.base.text_rotation = angle;
|
|
}
|
|
self.commit(Dimension::Angular3Pt(dim))
|
|
}
|
|
|
|
fn finish_two_line(
|
|
&self,
|
|
first_start: DVec3,
|
|
first_end: DVec3,
|
|
second_start: DVec3,
|
|
second_end: DVec3,
|
|
arc_point: DVec3,
|
|
) -> CmdResult {
|
|
let first_start = self.plane.to_local(first_start);
|
|
let first_end = self.plane.to_local(first_end);
|
|
let second_start = self.plane.to_local(second_start);
|
|
let second_end = self.plane.to_local(second_end);
|
|
let picked_text = self.plane.to_local(arc_point);
|
|
let Some((vertex, _, _)) = two_line_frame(
|
|
first_start,
|
|
first_end,
|
|
second_start,
|
|
second_end,
|
|
picked_text,
|
|
) else {
|
|
return CmdResult::NeedPoint;
|
|
};
|
|
let arc_point = locked_arc_point(vertex, picked_text, self.quadrant_lock);
|
|
if two_line_frame(
|
|
first_start,
|
|
first_end,
|
|
second_start,
|
|
second_end,
|
|
arc_point,
|
|
)
|
|
.is_none()
|
|
{
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
let mut dim = DimensionAngular2Ln::default();
|
|
dim.first_point = v3(first_start);
|
|
dim.second_point = v3(first_end);
|
|
dim.angle_vertex = v3(second_start);
|
|
dim.definition_point = v3(second_end);
|
|
dim.dimension_arc = v3(arc_point);
|
|
dim.base.definition_point = dim.definition_point;
|
|
dim.base.text_middle_point = dim.dimension_arc;
|
|
dim.base.insertion_point = dim.dimension_arc;
|
|
if self.quadrant_lock.is_some_and(|frame| {
|
|
!point_angle_in_frame(vertex, picked_text, frame)
|
|
}) {
|
|
dim.base.text_middle_point = v3(picked_text);
|
|
dim.base.insertion_point = dim.base.text_middle_point;
|
|
dim.base.text_user_positioned = true;
|
|
}
|
|
dim.base.actual_measurement = dim.measurement_degrees();
|
|
crate::entities::dimension::set_dimension_text_override(
|
|
&mut dim.base,
|
|
self.text_override.clone(),
|
|
);
|
|
if let Some(angle) = self.text_angle {
|
|
dim.base.text_rotation = angle;
|
|
}
|
|
self.commit(Dimension::Angular2Ln(dim))
|
|
}
|
|
|
|
fn commit(&self, dimension: Dimension) -> CmdResult {
|
|
let entity = self.plane.place_entity(EntityType::Dimension(dimension));
|
|
CmdResult::CommitDimension {
|
|
entity,
|
|
association: DimensionAssociationInput::Explicit(self.source_refs.clone()),
|
|
preserve_base_style: false,
|
|
continue_command: false,
|
|
}
|
|
}
|
|
|
|
fn placement_step(&self) -> bool {
|
|
matches!(self.step, Step::ArcPoint3 { .. } | Step::ArcPoint2 { .. })
|
|
}
|
|
|
|
fn editor_anchor(&self) -> DVec3 {
|
|
match self.step {
|
|
Step::ArcPoint3 { vertex, first, second } => {
|
|
let radius = vertex.distance(first).max(vertex.distance(second)).max(1.0);
|
|
let first_angle = self.plane.angle(vertex, first).unwrap_or(0.0);
|
|
let second_angle = self.plane.angle(vertex, second).unwrap_or(first_angle);
|
|
let sweep = (second_angle - first_angle).rem_euclid(std::f64::consts::TAU);
|
|
let angle = first_angle + sweep * 0.5;
|
|
self.plane.to_world(
|
|
self.plane.to_local(vertex)
|
|
+ DVec3::new(angle.cos() * radius, angle.sin() * radius, 0.0),
|
|
)
|
|
}
|
|
Step::ArcPoint2 {
|
|
first_start,
|
|
first_end,
|
|
second_start,
|
|
second_end,
|
|
} => {
|
|
let first_start = self.plane.to_local(first_start);
|
|
let first_end = self.plane.to_local(first_end);
|
|
let second_start = self.plane.to_local(second_start);
|
|
let second_end = self.plane.to_local(second_end);
|
|
two_line_frame(
|
|
first_start,
|
|
first_end,
|
|
second_start,
|
|
second_end,
|
|
(first_start + second_start) * 0.5,
|
|
)
|
|
.map(|(vertex, start, end)| {
|
|
self.plane.to_world(vertex + DVec3::new(
|
|
((start + end) * 0.5).cos(),
|
|
((start + end) * 0.5).sin(),
|
|
0.0,
|
|
))
|
|
})
|
|
.unwrap_or_else(|| self.plane.to_world(first_start))
|
|
}
|
|
_ => self.plane.origin,
|
|
}
|
|
}
|
|
|
|
fn set_quadrant_from_point(&mut self, point: DVec3) -> bool {
|
|
let point = self.plane.to_local(point);
|
|
let frame = match self.step {
|
|
Step::ArcPoint3 { vertex, first, second } => two_line_frame(
|
|
self.plane.to_local(vertex),
|
|
self.plane.to_local(first),
|
|
self.plane.to_local(vertex),
|
|
self.plane.to_local(second),
|
|
point,
|
|
),
|
|
Step::ArcPoint2 {
|
|
first_start,
|
|
first_end,
|
|
second_start,
|
|
second_end,
|
|
} => two_line_frame(
|
|
self.plane.to_local(first_start),
|
|
self.plane.to_local(first_end),
|
|
self.plane.to_local(second_start),
|
|
self.plane.to_local(second_end),
|
|
point,
|
|
),
|
|
_ => None,
|
|
};
|
|
if let Some((_, start, end)) = frame {
|
|
self.quadrant_lock = Some((start, end));
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CadCommand for AngularDimensionCommand {
|
|
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
|
self.plane = plane;
|
|
}
|
|
|
|
fn name(&self) -> &'static str {
|
|
"DIMANGULAR"
|
|
}
|
|
|
|
fn prompt(&self) -> String {
|
|
if self.awaiting_quadrant {
|
|
return t!("DIMANGULAR Specify quadrant:").into_owned();
|
|
}
|
|
if self.awaiting_text {
|
|
return if self.mtext_override {
|
|
t!("DIMANGULAR Enter formatted dimension text (blank = measured value):")
|
|
.into_owned()
|
|
} else {
|
|
t!("DIMANGULAR Enter dimension text (blank = measured value):").into_owned()
|
|
};
|
|
}
|
|
if self.awaiting_angle {
|
|
return if self.angle_origin.is_some() {
|
|
t!("DIMANGULAR Specify second point for text angle:").into_owned()
|
|
} else {
|
|
t!("DIMANGULAR Specify text angle or first point:").into_owned()
|
|
};
|
|
}
|
|
if self.selecting_object {
|
|
return match self.step {
|
|
Step::SecondLine { .. } => t!("DIMANGULAR Select second line:").into_owned(),
|
|
_ => t!(
|
|
"DIMANGULAR Select arc, circle, line, or specify an angle vertex:"
|
|
)
|
|
.into_owned(),
|
|
};
|
|
}
|
|
match self.step {
|
|
Step::Vertex => t!(
|
|
"DIMANGULAR Specify angle vertex:"
|
|
)
|
|
.into_owned(),
|
|
Step::FirstRay(_) => {
|
|
t!("DIMANGULAR Specify first extension line point:").into_owned()
|
|
}
|
|
Step::SecondRay { .. } | Step::CircleSecondRay { .. } => {
|
|
t!("DIMANGULAR Specify second extension line point:").into_owned()
|
|
}
|
|
Step::SecondLine { .. } => t!("DIMANGULAR Select second line:").into_owned(),
|
|
Step::ArcPoint3 { .. } | Step::ArcPoint2 { .. } => t!(
|
|
"DIMANGULAR Specify dimension arc location [Mtext/Text/Angle/Quadrant]:"
|
|
)
|
|
.into_owned(),
|
|
}
|
|
}
|
|
|
|
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
|
if self.awaiting_quadrant {
|
|
if self.set_quadrant_from_point(point) {
|
|
self.awaiting_quadrant = false;
|
|
}
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
if self.awaiting_angle {
|
|
if let Some(origin) = self.angle_origin {
|
|
if let Some(angle) = self.plane.angle(origin, point) {
|
|
self.text_angle = Some(angle);
|
|
self.awaiting_angle = false;
|
|
self.angle_origin = None;
|
|
}
|
|
} else {
|
|
self.angle_origin = Some(point);
|
|
}
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
if self.selecting_object && matches!(self.step, Step::Vertex) {
|
|
self.selecting_object = false;
|
|
self.step = Step::FirstRay(point);
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
match self.step {
|
|
Step::Vertex => {
|
|
self.step = Step::FirstRay(point);
|
|
CmdResult::NeedPoint
|
|
}
|
|
Step::FirstRay(vertex) => {
|
|
if point.distance_squared(vertex) <= 1.0e-24 {
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
self.step = Step::SecondRay { vertex, first: point };
|
|
CmdResult::NeedPoint
|
|
}
|
|
Step::SecondRay { vertex, first } => {
|
|
if point.distance_squared(vertex) <= 1.0e-24 {
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
let first_local = self.plane.vector_to_local(first - vertex);
|
|
let second_local = self.plane.vector_to_local(point - vertex);
|
|
if line_line(
|
|
[0.0, 0.0],
|
|
[first_local.x, first_local.y],
|
|
[0.0, 0.0],
|
|
[second_local.x, second_local.y],
|
|
)
|
|
.is_none()
|
|
{
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
self.step = Step::ArcPoint3 { vertex, first, second: point };
|
|
CmdResult::NeedPoint
|
|
}
|
|
Step::CircleSecondRay { vertex, first, source } => {
|
|
if point.distance_squared(vertex) <= 1.0e-24 {
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
self.source_refs = vec![
|
|
Some(DimensionAssociationSource::inferred(source)),
|
|
Some(DimensionAssociationSource::inferred(source)),
|
|
None,
|
|
];
|
|
self.step = Step::ArcPoint3 { vertex, first, second: point };
|
|
CmdResult::NeedPoint
|
|
}
|
|
Step::SecondLine { .. } => CmdResult::NeedPoint,
|
|
Step::ArcPoint3 { vertex, first, second } => {
|
|
self.finish_three_point(vertex, first, second, point)
|
|
}
|
|
Step::ArcPoint2 {
|
|
first_start,
|
|
first_end,
|
|
second_start,
|
|
second_end,
|
|
} => self.finish_two_line(
|
|
first_start,
|
|
first_end,
|
|
second_start,
|
|
second_end,
|
|
point,
|
|
),
|
|
}
|
|
}
|
|
|
|
fn on_enter(&mut self) -> CmdResult {
|
|
if self.awaiting_quadrant {
|
|
self.awaiting_quadrant = false;
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
if self.awaiting_text {
|
|
self.awaiting_text = false;
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
if self.awaiting_angle {
|
|
self.awaiting_angle = false;
|
|
self.angle_origin = None;
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
if matches!(self.step, Step::Vertex) {
|
|
self.selecting_object = !self.selecting_object;
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
CmdResult::Cancel
|
|
}
|
|
|
|
fn on_escape(&mut self) -> CmdResult {
|
|
CmdResult::Cancel
|
|
}
|
|
|
|
fn input_kind(&self) -> InputKind {
|
|
if self.awaiting_text {
|
|
InputKind::FreeText
|
|
} else {
|
|
InputKind::SingleToken
|
|
}
|
|
}
|
|
|
|
fn options(&self) -> Vec<CmdOption> {
|
|
if self.placement_step() && !self.awaiting_text && !self.awaiting_angle && !self.awaiting_quadrant {
|
|
vec![
|
|
CmdOption::new("MText", "MTEXT"),
|
|
CmdOption::new("Text", "TEXT"),
|
|
CmdOption::new("Angle", "ANGLE"),
|
|
CmdOption::new("Quadrant", "QUADRANT"),
|
|
]
|
|
} else {
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
fn point_step_accepts_keywords(&self) -> bool {
|
|
self.placement_step()
|
|
&& !self.awaiting_text
|
|
&& !self.awaiting_angle
|
|
&& !self.awaiting_quadrant
|
|
}
|
|
|
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
|
if self.awaiting_text {
|
|
let value = text.trim();
|
|
self.text_override = if value.is_empty() || value == "<>" {
|
|
None
|
|
} else {
|
|
Some(value.to_string())
|
|
};
|
|
self.awaiting_text = false;
|
|
return Some(CmdResult::NeedPoint);
|
|
}
|
|
if self.awaiting_angle {
|
|
self.text_angle = if text.trim().is_empty() {
|
|
None
|
|
} else {
|
|
crate::entities::common::parse_typed_angle(text.trim())
|
|
};
|
|
self.awaiting_angle = false;
|
|
self.angle_origin = None;
|
|
return Some(CmdResult::NeedPoint);
|
|
}
|
|
if !self.placement_step() {
|
|
return None;
|
|
}
|
|
match text.trim().to_ascii_uppercase().as_str() {
|
|
"T" | "TEXT" => {
|
|
self.mtext_override = false;
|
|
self.awaiting_text = true;
|
|
Some(CmdResult::NeedPoint)
|
|
}
|
|
"M" | "MTEXT" => {
|
|
self.mtext_override = true;
|
|
Some(CmdResult::SuspendForMTextInput {
|
|
pos: self.editor_anchor(),
|
|
initial: self.text_override.clone().unwrap_or_default(),
|
|
height: 2.5,
|
|
})
|
|
}
|
|
"A" | "ANGLE" => {
|
|
self.awaiting_angle = true;
|
|
self.angle_origin = None;
|
|
Some(CmdResult::NeedPoint)
|
|
}
|
|
"Q" | "QUADRANT" => {
|
|
self.awaiting_quadrant = true;
|
|
Some(CmdResult::NeedPoint)
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn on_editor_text(&mut self, value: String) {
|
|
let value = value.trim();
|
|
self.text_override = if value.is_empty() || value == "<>" {
|
|
None
|
|
} else {
|
|
Some(value.to_string())
|
|
};
|
|
}
|
|
|
|
fn on_editor_closed(&mut self, _committed: bool) -> CmdResult {
|
|
CmdResult::NeedPoint
|
|
}
|
|
|
|
fn needs_entity_pick(&self) -> bool {
|
|
self.selecting_object
|
|
}
|
|
|
|
fn entity_pick_accepts_points(&self) -> bool {
|
|
self.selecting_object && matches!(self.step, Step::Vertex)
|
|
}
|
|
|
|
fn entity_pick_highlights_hover(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
fn inject_before_entity_pick(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
fn inject_picked_entity(&mut self, entity: EntityType) {
|
|
self.picked_entity = Some(entity);
|
|
}
|
|
|
|
fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult {
|
|
if handle.is_null() {
|
|
if matches!(self.step, Step::Vertex) {
|
|
self.selecting_object = false;
|
|
self.step = Step::FirstRay(point);
|
|
}
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
let Some(entity) = self.picked_entity.take() else {
|
|
return CmdResult::NeedPoint;
|
|
};
|
|
let Some(curve) = picked_curve(&entity, point) else {
|
|
return CmdResult::NeedPoint;
|
|
};
|
|
if !curve_is_coplanar(&curve, self.plane) {
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
|
|
match (&self.step, curve) {
|
|
(
|
|
Step::SecondLine {
|
|
first_start,
|
|
first_end,
|
|
first_source,
|
|
first_refs,
|
|
},
|
|
PickedCurve::Line {
|
|
start: second_start,
|
|
end: second_end,
|
|
refs: second_refs,
|
|
..
|
|
},
|
|
) => {
|
|
if (*first_source == handle && *first_refs == second_refs)
|
|
|| lines_parallel_in_plane(
|
|
self.plane,
|
|
*first_start,
|
|
*first_end,
|
|
second_start,
|
|
second_end,
|
|
)
|
|
{
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
self.source_refs = vec![
|
|
Some(first_refs[0].bind(*first_source)),
|
|
Some(first_refs[1].bind(*first_source)),
|
|
Some(second_refs[0].bind(handle)),
|
|
Some(second_refs[1].bind(handle)),
|
|
];
|
|
self.selecting_object = false;
|
|
self.step = Step::ArcPoint2 {
|
|
first_start: *first_start,
|
|
first_end: *first_end,
|
|
second_start,
|
|
second_end,
|
|
};
|
|
CmdResult::NeedPoint
|
|
}
|
|
(
|
|
Step::Vertex,
|
|
PickedCurve::Line {
|
|
start: first_start,
|
|
end: first_end,
|
|
refs,
|
|
..
|
|
},
|
|
) => {
|
|
self.step = Step::SecondLine {
|
|
first_start,
|
|
first_end,
|
|
first_source: handle,
|
|
first_refs: refs,
|
|
};
|
|
CmdResult::NeedPoint
|
|
}
|
|
(
|
|
Step::Vertex,
|
|
PickedCurve::Arc {
|
|
center,
|
|
first,
|
|
second,
|
|
refs,
|
|
..
|
|
},
|
|
) => {
|
|
self.source_refs = refs
|
|
.into_iter()
|
|
.map(|reference| Some(reference.bind(handle)))
|
|
.collect();
|
|
self.selecting_object = false;
|
|
self.step = Step::ArcPoint3 { vertex: center, first, second };
|
|
CmdResult::NeedPoint
|
|
}
|
|
(Step::Vertex, PickedCurve::Circle { center, .. }) => {
|
|
if point.distance_squared(center) <= 1.0e-24 {
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
self.selecting_object = false;
|
|
self.step = Step::CircleSecondRay {
|
|
vertex: center,
|
|
first: point,
|
|
source: handle,
|
|
};
|
|
CmdResult::NeedPoint
|
|
}
|
|
_ => CmdResult::NeedPoint,
|
|
}
|
|
}
|
|
|
|
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
|
|
if self.awaiting_angle {
|
|
return self
|
|
.angle_origin
|
|
.map(|origin| preview_wire(vec![origin, point]));
|
|
}
|
|
let points = match self.step {
|
|
Step::Vertex | Step::SecondLine { .. } => return None,
|
|
Step::FirstRay(vertex) => vec![vertex, point],
|
|
Step::SecondRay { vertex, first } => vec![vertex, first, nan(), vertex, point],
|
|
Step::CircleSecondRay { vertex, first, .. } => {
|
|
vec![vertex, first, nan(), vertex, point]
|
|
}
|
|
Step::ArcPoint3 { vertex, first, second } => {
|
|
let vertex = self.plane.to_local(vertex);
|
|
let first = self.plane.to_local(first);
|
|
let second = self.plane.to_local(second);
|
|
let point = locked_arc_point(
|
|
vertex,
|
|
self.plane.to_local(point),
|
|
self.quadrant_lock,
|
|
);
|
|
angular_preview(vertex, first, second, point)
|
|
.into_iter()
|
|
.map(|value| if value.is_nan() { value } else { self.plane.to_world(value) })
|
|
.collect()
|
|
}
|
|
Step::ArcPoint2 {
|
|
first_start,
|
|
first_end,
|
|
second_start,
|
|
second_end,
|
|
} => {
|
|
let first_start = self.plane.to_local(first_start);
|
|
let first_end = self.plane.to_local(first_end);
|
|
let second_start = self.plane.to_local(second_start);
|
|
let second_end = self.plane.to_local(second_end);
|
|
let point = self.plane.to_local(point);
|
|
let point = two_line_frame(
|
|
first_start,
|
|
first_end,
|
|
second_start,
|
|
second_end,
|
|
point,
|
|
)
|
|
.map(|(vertex, _, _)| locked_arc_point(vertex, point, self.quadrant_lock))
|
|
.unwrap_or(point);
|
|
two_line_preview(
|
|
first_start,
|
|
first_end,
|
|
second_start,
|
|
second_end,
|
|
point,
|
|
)
|
|
.into_iter()
|
|
.map(|value| if value.is_nan() { value } else { self.plane.to_world(value) })
|
|
.collect()
|
|
}
|
|
};
|
|
Some(preview_wire(points))
|
|
}
|
|
}
|
|
|
|
fn picked_curve(entity: &EntityType, click: DVec3) -> Option<PickedCurve> {
|
|
let point = |value: Vector3| DVec3::new(value.x, value.y, value.z);
|
|
let normal = |value: Vector3| DVec3::new(value.x, value.y, value.z);
|
|
match entity {
|
|
EntityType::Line(line) => {
|
|
let start = point(line.start);
|
|
let end = point(line.end);
|
|
(start.distance_squared(end) > 1.0e-24).then_some(PickedCurve::Line {
|
|
start,
|
|
end,
|
|
refs: [SourceLocator::INFERRED; 2],
|
|
normal: None,
|
|
})
|
|
}
|
|
EntityType::Arc(arc) => Some(PickedCurve::Arc {
|
|
center: point(arc.center_wcs()),
|
|
first: point(arc.start_point_wcs()),
|
|
second: point(arc.end_point_wcs()),
|
|
refs: [SourceLocator::INFERRED; 3],
|
|
normal: normal(arc.normal),
|
|
}),
|
|
EntityType::Circle(circle) => Some(PickedCurve::Circle {
|
|
center: point(circle.center_wcs()),
|
|
normal: normal(circle.normal),
|
|
}),
|
|
EntityType::LwPolyline(polyline) => {
|
|
let click = crate::scene::view::transform::wcs_point_to_ocs(
|
|
(click.x, click.y, click.z),
|
|
(polyline.normal.x, polyline.normal.y, polyline.normal.z),
|
|
);
|
|
let points: Vec<_> = polyline
|
|
.vertices
|
|
.iter()
|
|
.map(|vertex| [vertex.location.x, vertex.location.y])
|
|
.collect();
|
|
let bulges: Vec<_> = polyline.vertices.iter().map(|vertex| vertex.bulge).collect();
|
|
let segment = nearest_segment_index(
|
|
&points,
|
|
&bulges,
|
|
polyline.is_closed,
|
|
[click.0, click.1],
|
|
)?;
|
|
let first = polyline.vertices[segment];
|
|
let second = polyline.vertices[(segment + 1) % polyline.vertices.len()];
|
|
polyline_segment(
|
|
[first.location.x, first.location.y],
|
|
[second.location.x, second.location.y],
|
|
first.bulge,
|
|
polyline.elevation,
|
|
polyline.normal,
|
|
segment,
|
|
polyline.vertices.len(),
|
|
)
|
|
}
|
|
EntityType::Polyline2D(polyline) => {
|
|
let vertices = &polyline.vertices;
|
|
let click = crate::scene::view::transform::wcs_point_to_ocs(
|
|
(click.x, click.y, click.z),
|
|
(polyline.normal.x, polyline.normal.y, polyline.normal.z),
|
|
);
|
|
let points: Vec<_> = vertices
|
|
.iter()
|
|
.map(|vertex| [vertex.location.x, vertex.location.y])
|
|
.collect();
|
|
let bulges: Vec<_> = vertices.iter().map(|vertex| vertex.bulge).collect();
|
|
let segment = nearest_segment_index(
|
|
&points,
|
|
&bulges,
|
|
polyline.is_closed(),
|
|
[click.0, click.1],
|
|
)?;
|
|
let first = &vertices[segment];
|
|
let second = &vertices[(segment + 1) % vertices.len()];
|
|
polyline_segment(
|
|
[first.location.x, first.location.y],
|
|
[second.location.x, second.location.y],
|
|
first.bulge,
|
|
polyline.elevation,
|
|
polyline.normal,
|
|
segment,
|
|
vertices.len(),
|
|
)
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn nearest_segment_index(
|
|
points: &[[f64; 2]],
|
|
bulges: &[f64],
|
|
closed: bool,
|
|
click: [f64; 2],
|
|
) -> Option<usize> {
|
|
if points.len() < 2 {
|
|
return None;
|
|
}
|
|
let count = if closed { points.len() } else { points.len() - 1 };
|
|
(0..count).min_by(|first, second| {
|
|
polyline_segment_distance_squared(
|
|
points[*first],
|
|
points[(*first + 1) % points.len()],
|
|
bulges.get(*first).copied().unwrap_or(0.0),
|
|
click,
|
|
)
|
|
.total_cmp(&polyline_segment_distance_squared(
|
|
points[*second],
|
|
points[(*second + 1) % points.len()],
|
|
bulges.get(*second).copied().unwrap_or(0.0),
|
|
click,
|
|
))
|
|
})
|
|
}
|
|
|
|
fn polyline_segment_curve(first: [f64; 2], second: [f64; 2], bulge: f64) -> KernelCurve {
|
|
if let Some(arc) = BulgeArc::from_bulge(first, second, bulge) {
|
|
let (start_angle, end_angle) = if arc.sweep >= 0.0 {
|
|
(arc.start_angle, arc.end_angle)
|
|
} else {
|
|
(arc.end_angle, arc.start_angle)
|
|
};
|
|
KernelCurve::Arc(KernelArc {
|
|
centre: arc.center,
|
|
radius: arc.radius,
|
|
start_angle,
|
|
end_angle,
|
|
})
|
|
} else {
|
|
KernelCurve::Line(KernelLine { start: first, end: second })
|
|
}
|
|
}
|
|
|
|
fn polyline_segment_distance_squared(
|
|
first: [f64; 2],
|
|
second: [f64; 2],
|
|
bulge: f64,
|
|
point: [f64; 2],
|
|
) -> f64 {
|
|
closest_point(&polyline_segment_curve(first, second, bulge), point)
|
|
.distance
|
|
.powi(2)
|
|
}
|
|
|
|
fn polyline_segment(
|
|
first: [f64; 2],
|
|
second: [f64; 2],
|
|
bulge: f64,
|
|
elevation: f64,
|
|
normal: Vector3,
|
|
segment: usize,
|
|
vertex_count: usize,
|
|
) -> Option<PickedCurve> {
|
|
let to_world = |point: [f64; 2]| {
|
|
let value = crate::scene::view::transform::ocs_point_to_wcs(
|
|
(point[0], point[1], elevation),
|
|
(normal.x, normal.y, normal.z),
|
|
);
|
|
DVec3::new(value.0, value.1, value.2)
|
|
};
|
|
let first_world = to_world(first);
|
|
let second_world = to_world(second);
|
|
if first_world.distance_squared(second_world) <= 1.0e-24 {
|
|
return None;
|
|
}
|
|
let refs = [
|
|
SourceLocator::explicit(segment as i32, 0.0),
|
|
SourceLocator::explicit(((segment + 1) % vertex_count) as i32, 0.0),
|
|
];
|
|
let normal_world = DVec3::new(normal.x, normal.y, normal.z);
|
|
if bulge.abs() <= 1.0e-12 {
|
|
return Some(PickedCurve::Line {
|
|
start: first_world,
|
|
end: second_world,
|
|
refs,
|
|
normal: Some(normal_world),
|
|
});
|
|
}
|
|
let center = BulgeArc::from_bulge(first, second, bulge)?.center;
|
|
Some(PickedCurve::Arc {
|
|
center: to_world(center),
|
|
first: first_world,
|
|
second: second_world,
|
|
refs: [
|
|
SourceLocator::explicit(-4, segment as f64),
|
|
refs[0],
|
|
refs[1],
|
|
],
|
|
normal: normal_world,
|
|
})
|
|
}
|
|
|
|
fn curve_is_coplanar(curve: &PickedCurve, plane: WorkingPlane) -> bool {
|
|
let (points, normal): (Vec<DVec3>, Option<DVec3>) = match curve {
|
|
PickedCurve::Line { start, end, normal, .. } => (vec![*start, *end], *normal),
|
|
PickedCurve::Arc { center, first, second, normal, .. } => {
|
|
(vec![*center, *first, *second], Some(*normal))
|
|
}
|
|
PickedCurve::Circle { center, normal } => (vec![*center], Some(*normal)),
|
|
};
|
|
if let Some(normal) = normal {
|
|
let alignment = normal.normalize_or_zero().dot(plane.z).abs();
|
|
if alignment < 1.0 - 1.0e-7 {
|
|
return false;
|
|
}
|
|
}
|
|
let local: Vec<_> = points.into_iter().map(|point| plane.to_local(point)).collect();
|
|
let scale = local
|
|
.iter()
|
|
.map(|point| point.x.abs().max(point.y.abs()).max(1.0))
|
|
.fold(1.0, f64::max);
|
|
local.iter().all(|point| point.z.abs() <= 1.0e-9 * scale)
|
|
}
|
|
|
|
fn lines_parallel_in_plane(
|
|
plane: WorkingPlane,
|
|
first_start: DVec3,
|
|
first_end: DVec3,
|
|
second_start: DVec3,
|
|
second_end: DVec3,
|
|
) -> bool {
|
|
let first_start = plane.to_local(first_start);
|
|
let first_end = plane.to_local(first_end);
|
|
let second_start = plane.to_local(second_start);
|
|
let second_end = plane.to_local(second_end);
|
|
let first = first_end - first_start;
|
|
let second = second_end - second_start;
|
|
line_line(
|
|
[first_start.x, first_start.y],
|
|
[first.x, first.y],
|
|
[second_start.x, second_start.y],
|
|
[second.x, second.y],
|
|
)
|
|
.is_none()
|
|
}
|
|
|
|
fn locked_arc_point(
|
|
vertex: DVec3,
|
|
point: DVec3,
|
|
frame: Option<(f64, f64)>,
|
|
) -> DVec3 {
|
|
let Some((start, end)) = frame else { return point };
|
|
let radius = ((point.x - vertex.x).powi(2) + (point.y - vertex.y).powi(2)).sqrt();
|
|
if radius <= 1.0e-12 {
|
|
return point;
|
|
}
|
|
let angle = (start + end) * 0.5;
|
|
DVec3::new(
|
|
vertex.x + angle.cos() * radius,
|
|
vertex.y + angle.sin() * radius,
|
|
vertex.z,
|
|
)
|
|
}
|
|
|
|
fn point_angle_in_frame(vertex: DVec3, point: DVec3, frame: (f64, f64)) -> bool {
|
|
let angle = (point.y - vertex.y).atan2(point.x - vertex.x);
|
|
let sweep = frame.1 - frame.0;
|
|
(angle - frame.0).rem_euclid(std::f64::consts::TAU) <= sweep + 1.0e-9
|
|
}
|
|
|
|
fn two_line_frame(
|
|
first_start: DVec3,
|
|
first_end: DVec3,
|
|
second_start: DVec3,
|
|
second_end: DVec3,
|
|
arc_point: DVec3,
|
|
) -> Option<(DVec3, f64, f64)> {
|
|
let first_direction = first_end - first_start;
|
|
let second_direction = second_end - second_start;
|
|
let (t, _) = line_line(
|
|
[first_start.x, first_start.y],
|
|
[first_direction.x, first_direction.y],
|
|
[second_start.x, second_start.y],
|
|
[second_direction.x, second_direction.y],
|
|
)?;
|
|
let vertex = first_start + first_direction * t;
|
|
let target = (arc_point.y - vertex.y).atan2(arc_point.x - vertex.x);
|
|
let mut best: Option<(f64, f64, f64)> = None;
|
|
for first_sign in [1.0, -1.0] {
|
|
for second_sign in [1.0, -1.0] {
|
|
let first = first_direction * first_sign;
|
|
let second = second_direction * second_sign;
|
|
for (start, end) in [
|
|
(first.y.atan2(first.x), second.y.atan2(second.x)),
|
|
(second.y.atan2(second.x), first.y.atan2(first.x)),
|
|
] {
|
|
let sweep = (end - start).rem_euclid(std::f64::consts::TAU);
|
|
if sweep <= 1.0e-9 || sweep > std::f64::consts::PI {
|
|
continue;
|
|
}
|
|
let selected = (target - start).rem_euclid(std::f64::consts::TAU);
|
|
if selected <= sweep + 1.0e-9 && best.is_none_or(|known| sweep < known.2) {
|
|
best = Some((start, start + sweep, sweep));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
best.map(|(start, end, _)| (vertex, start, end))
|
|
}
|
|
|
|
fn angular_preview(vertex: DVec3, first: DVec3, second: DVec3, arc_point: DVec3) -> Vec<DVec3> {
|
|
let Some((_, start, end)) = two_line_frame(vertex, first, vertex, second, arc_point) else {
|
|
return vec![vertex, first, nan(), vertex, second];
|
|
};
|
|
angular_preview_with_frame(vertex, first, second, arc_point, start, end)
|
|
}
|
|
|
|
fn two_line_preview(
|
|
first_start: DVec3,
|
|
first_end: DVec3,
|
|
second_start: DVec3,
|
|
second_end: DVec3,
|
|
arc_point: DVec3,
|
|
) -> Vec<DVec3> {
|
|
let Some((vertex, start, end)) = two_line_frame(
|
|
first_start,
|
|
first_end,
|
|
second_start,
|
|
second_end,
|
|
arc_point,
|
|
) else {
|
|
return vec![first_start, first_end, nan(), second_start, second_end];
|
|
};
|
|
angular_preview_with_frame(vertex, first_start, second_start, arc_point, start, end)
|
|
}
|
|
|
|
fn angular_preview_with_frame(
|
|
vertex: DVec3,
|
|
first: DVec3,
|
|
second: DVec3,
|
|
arc_point: DVec3,
|
|
start: f64,
|
|
end: f64,
|
|
) -> Vec<DVec3> {
|
|
let radius = vertex.distance(arc_point);
|
|
let first_end = vertex + DVec3::new(start.cos(), start.sin(), 0.0) * radius;
|
|
let second_end = vertex + DVec3::new(end.cos(), end.sin(), 0.0) * radius;
|
|
let mut points = vec![first, first_end, nan(), second, second_end, nan()];
|
|
points.extend(
|
|
cadkernel::geom2d::tessellate::arc(
|
|
[vertex.x, vertex.y],
|
|
radius,
|
|
start,
|
|
end,
|
|
vertex.z,
|
|
cadkernel::geom2d::tessellate::DEFAULT_SEGMENTS_PER_RADIAN,
|
|
)
|
|
.into_iter()
|
|
.map(DVec3::from_array),
|
|
);
|
|
points
|
|
}
|
|
|
|
fn v3(point: DVec3) -> Vector3 {
|
|
Vector3::new(point.x, point.y, point.z)
|
|
}
|
|
|
|
fn nan() -> DVec3 {
|
|
DVec3::splat(f64::NAN)
|
|
}
|
|
|
|
fn preview_wire(points: Vec<DVec3>) -> WireModel {
|
|
WireModel {
|
|
bg_adapt: None,
|
|
point_marker: None,
|
|
taper_widths: Vec::new(),
|
|
pattern_stations: Vec::new(),
|
|
world_width: 0.0,
|
|
depth_override: None,
|
|
display_visible: true,
|
|
plot_visible: true,
|
|
fill_is_3d: false,
|
|
fill_is_2d_solid: false,
|
|
render_instance: None,
|
|
pick_tris: Vec::new(),
|
|
pick_tris_low: Vec::new(),
|
|
dash_from_start: false,
|
|
dash_align_end: None,
|
|
text_verts: Vec::new(),
|
|
name: "dimangular_preview".to_string(),
|
|
points: points.into_iter().map(|point| [point.x as f32, point.y as f32, point.z as f32]).collect(),
|
|
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,
|
|
key_vertices: vec![],
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
plinegen: true,
|
|
fill_tris: vec![],
|
|
fill_tris_low: Vec::new(),
|
|
}
|
|
}
|
|
|
|
inventory::submit!(crate::command::CommandRegistration { names: &["DIMANGULAR"] });
|