Files
OneCAD-Draft/src/modules/draw/modify/mirror.rs
T
Hakan SevenandClaude Opus 5.5 f537aff75c feat: node graph overlay; move to opencadcodec/opencadkernel/opencadgraph
The codec and kernel repositories and crates are renamed opencadcodec and
opencadkernel. Inside OCS they are depended on under short keys, so code
reads `codec::`, `kernel::` and the new `graph::` (opencadgraph, the node
graph engine). File-format and protocol spellings stay as they were: the
ACIS product id `acadrust`, the ACADRUST_* switches and the plugin
version keys `acadrust_version` / `acadrust_source`.

A button left of MCP on the command line opens a translucent node graph
over the viewport, with a palette of object, input, math, logic, list,
text and geometry nodes added by click or drag. An object node owns
drawing entities and its rows are the Properties panel's own rows for
them: the single-object page is extracted from refresh_properties as
entity_property_sections, values use the control protocol's
property_json, and writes go through set_property_value with a property
target override, so the panel, MCP and the graph share one read/write
path. A list reaching an object row makes one entity per element. Each
graph action is one undo step; the canvas pans and zooms.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-25 07:34:23 +03:00

224 lines
7.0 KiB
Rust

// Mirror tool — ribbon definition + interactive command.
//
// Command: MIRROR (MI)
// Requires at least one entity selected.
// Step 1: pick first mirror-line point
// Step 2: pick second mirror-line point
// Step 3: "Erase source objects? [Yes/No] <No>"
// No → keep the original, add a mirrored copy
// Yes → flip the original in place (no copy kept)
use codec::Handle;
use glam::DVec3;
use crate::t;
use crate::command::{CadCommand, CmdResult, EntityTransform, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
pub fn tool() -> ToolDef {
ToolDef {
id: "MIRROR",
label: "Mirror",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/mirror.svg")),
event: ModuleEvent::Command("MIRROR".to_string()),
}
}
enum Step {
P1,
P2(DVec3),
/// Both mirror-line points fixed; waiting on the erase-source answer.
AskErase { p1: DVec3, p2: DVec3 },
}
pub struct MirrorCommand {
handles: Vec<Handle>,
wire_models: Vec<WireModel>,
/// Text ghosts paired with their bounding-box centre — mirrored per MIRRTEXT
/// so the preview matches the commit (true glyph mirror on / symmetric
/// right-reading off).
text_ghosts: Vec<(WireModel, DVec3)>,
mirror_text: bool,
step: Step,
plane: WorkingPlane,
}
impl MirrorCommand {
pub fn new(
handles: Vec<Handle>,
wire_models: Vec<WireModel>,
text_ghosts: Vec<(WireModel, DVec3)>,
mirror_text: bool,
) -> Self {
Self {
handles,
wire_models,
text_ghosts,
mirror_text,
step: Step::P1,
plane: WorkingPlane::default(),
}
}
}
impl CadCommand for MirrorCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str {
"MIRROR"
}
fn prompt(&self) -> String {
match &self.step {
Step::P1 => t!(
"MIRROR Specify first mirror-line point [%{count} objects]:",
count = self.handles.len()
)
.into_owned(),
Step::P2(p1) => {
let px = format!("{:.2}", p1.x);
let py = format!("{:.2}", p1.y);
t!(
"MIRROR Specify second point [p1=%{px},%{py}]:",
px = px,
py = py
)
.into_owned()
}
Step::AskErase { .. } => {
t!("MIRROR Erase source objects? [Yes/No] <No>:").into_owned()
}
}
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
match &self.step {
Step::P1 => {
self.step = Step::P2(pt);
CmdResult::NeedPoint
}
Step::P2(p1) => {
self.step = Step::AskErase { p1: *p1, p2: pt };
CmdResult::NeedPoint
}
// Second point is fixed; further clicks ignored until the
// erase-source question is answered via the command line.
Step::AskErase { .. } => CmdResult::NeedPoint,
}
}
fn on_enter(&mut self) -> CmdResult {
// Enter at the erase prompt accepts the default (No → keep source).
match &self.step {
Step::AskErase { p1, p2 } => self.finish(*p1, *p2, false),
_ => CmdResult::Cancel,
}
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn wants_text_input(&self) -> bool {
matches!(self.step, Step::AskErase { .. })
}
fn options(&self) -> Vec<crate::command::CmdOption> {
use crate::command::CmdOption;
if matches!(self.step, Step::AskErase { .. }) {
vec![CmdOption::new("Yes", "Y"), CmdOption::new("No", "N")]
} else {
Vec::new()
}
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let Step::AskErase { p1, p2 } = &self.step else {
return None;
};
let (p1, p2) = (*p1, *p2);
let t = text.trim().to_ascii_lowercase();
let erase = match t.as_str() {
"y" | "yes" => true,
// Empty input (bare Enter) or an explicit No keeps the source.
"" | "n" | "no" => false,
// Unrecognised input: re-ask without committing.
_ => return Some(CmdResult::NeedPoint),
};
Some(self.finish(p1, p2, erase))
}
fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
// While picking the second point the ghost tracks the cursor; once it
// is fixed (erase prompt) the ghost freezes at the chosen axis.
let (p1, p2) = match &self.step {
Step::P2(p1) => (*p1, pt),
Step::AskErase { p1, p2 } => (*p1, *p2),
_ => return vec![],
};
// Mirrored ghosts of all non-text objects (full geometric reflection).
let mut out: Vec<WireModel> = self
.wire_models
.iter()
.map(|w| {
w.mirrored_in_plane(
p1.as_vec3(),
p2.as_vec3(),
self.plane.z.as_vec3(),
)
})
.collect();
// Text ghosts honour MIRRTEXT: on → true glyph mirror (full reflection);
// off → keep glyphs readable, relocate to the mirror-symmetric position
// by reflecting the box centre and translating.
for (w, center) in &self.text_ghosts {
if self.mirror_text {
out.push(w.mirrored_in_plane(
p1.as_vec3(),
p2.as_vec3(),
self.plane.z.as_vec3(),
));
} else {
let reflected = crate::scene::view::transform::reflected_point(
*center,
p1,
p2,
self.plane.z,
);
let delta = (reflected - *center).as_vec3();
out.push(w.translated(delta));
}
}
// Mirror-axis line (rubber-band).
out.push(WireModel::solid(
"rubber_band".into(),
vec![
[p1.x as f32, p1.y as f32, p1.z as f32],
[p2.x as f32, p2.y as f32, p2.z as f32],
],
WireModel::CYAN,
false,
));
out
}
}
impl MirrorCommand {
/// Commit the mirror. `erase` true flips the originals in place; false
/// keeps them and adds a mirrored copy. Either way the command ends.
fn finish(&self, p1: DVec3, p2: DVec3, erase: bool) -> CmdResult {
let xform = EntityTransform::Mirror {
p1,
p2,
working_normal: self.plane.z,
};
if erase {
CmdResult::TransformSelected(self.handles.clone(), xform)
} else {
CmdResult::BatchCopy(self.handles.clone(), vec![xform])
}
}
}