forked from 1b-agent/opencadstudio-upstream
Add sketch mode
CREATESKETCH opens a sketch on the current UCS, or on one of the three origin planes with XY, XZ or YZ. FINISHSKETCH closes it again. Nothing here is new machinery: the plane is a UCS, the square-on view is PLAN, the drafting aids are the snapper. What the pair adds is holding them together and putting them back afterwards, including the snap master state, so a user who works with snapping off gets that back rather than silently keeping it. Squaring the view up is not cosmetic. Drawing on a plane seen nearly edge-on is the main way geometry lands somewhere the user did not mean, because a few pixels of cursor travel cover a long way in world space. A sketch is a mode rather than a document object, so what it produces is ordinary model-space geometry and the file stays a DWG other software can open. Whatever was drawn becomes the Previous selection set, so EXTRUDE then Previous consumes it without making the user reselect a profile they just finished.
This commit is contained in:
3 files changed
+156
No files matched your search
@@ -631,6 +631,61 @@ impl OpenCADStudio {
|
||||
// PLAN — plan view of a UCS (#326). Shows its options right on
|
||||
// activation instead of flipping the view like a cube click; the
|
||||
// view only changes once an option (or the Current default) runs.
|
||||
// ── Sketch mode ──────────────────────────────────────────────
|
||||
// Fusion's sketch loop, assembled from pieces that already exist:
|
||||
// a UCS for the plane, PLAN for the square-on view, the snapper
|
||||
// for the drafting aids. A sketch is a mode, not a document
|
||||
// object, so what it produces is ordinary model-space geometry
|
||||
// and the file stays a DWG every other program can still open.
|
||||
cmd if cmd == "CREATESKETCH" || cmd.starts_with("CREATESKETCH ") => {
|
||||
if self.tabs[i].sketch_session.is_some() {
|
||||
self.command_line.push_error(
|
||||
crate::t!("A sketch is already open — FINISHSKETCH closes it.")
|
||||
.as_ref(),
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
let arg = cmd["CREATESKETCH".len()..].trim().to_uppercase();
|
||||
// The three origin planes. Without them the first sketch in
|
||||
// an empty drawing would have no face to land on.
|
||||
let origin_plane = match arg.as_str() {
|
||||
"XY" => Some(glam::DVec3::Z),
|
||||
"XZ" => Some(glam::DVec3::NEG_Y),
|
||||
"YZ" => Some(glam::DVec3::X),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(normal) = origin_plane {
|
||||
self.tabs[i].active_ucs =
|
||||
super::super::helpers::ucs_from_normal(glam::DVec3::ZERO, normal);
|
||||
self.commit_active_ucs_change(i, "CREATESKETCH");
|
||||
} else if !arg.is_empty() {
|
||||
self.command_line.push_error(
|
||||
crate::t!(
|
||||
"Usage: CREATESKETCH [XY|XZ|YZ]; bare uses the current UCS."
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
self.open_sketch(i);
|
||||
}
|
||||
// BROWSER — toggle the docked outline of planes, sketch and bodies.
|
||||
"BROWSER" => {
|
||||
self.show_browser ^= true;
|
||||
if self.show_browser {
|
||||
// Open expanded so it is usable straight away; the pin
|
||||
// button still collapses it.
|
||||
self.dock_expanded = Some(crate::ui::dock::PanelId::Browser);
|
||||
self.command_line
|
||||
.push_output(crate::t!("Browser opened.").as_ref());
|
||||
} else {
|
||||
self.command_line
|
||||
.push_output(crate::t!("Browser closed.").as_ref());
|
||||
}
|
||||
}
|
||||
"FINISHSKETCH" => {
|
||||
self.finish_sketch(i);
|
||||
}
|
||||
"PLAN" => {
|
||||
use crate::command::KeywordCommand;
|
||||
let c = KeywordCommand::new(
|
||||
@@ -1491,6 +1546,74 @@ impl OpenCADStudio {
|
||||
|
||||
/// Snap to the plan view of the UCS whose rotation is `r_ucs`, straight —
|
||||
/// no "already there → flip to the opposite face" cube behaviour (#326).
|
||||
/// Enter sketch mode on the current UCS.
|
||||
///
|
||||
/// Squaring the view up is not cosmetic: drawing on a plane seen nearly
|
||||
/// edge-on is the main way geometry ends up somewhere the user did not
|
||||
/// mean, because a few pixels of cursor travel cover a long way in
|
||||
/// world space.
|
||||
fn open_sketch(&mut self, i: usize) {
|
||||
let opened_with: std::collections::HashSet<acadrust::Handle> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.map(|entity| entity.common().handle)
|
||||
.collect();
|
||||
self.tabs[i].sketch_count += 1;
|
||||
let name = format!("Sketch{}", self.tabs[i].sketch_count);
|
||||
self.tabs[i].sketch_session = Some(crate::app::document::SketchSession {
|
||||
name: name.clone(),
|
||||
previous_ucs: self.tabs[i].active_ucs.clone(),
|
||||
previous_snap_enabled: self.snapper.snap_enabled,
|
||||
opened_with,
|
||||
});
|
||||
let rotation = self.tabs[i].scene.viewcube_ucs_mat();
|
||||
self.plan_snap(i, rotation);
|
||||
self.snapper.snap_enabled = true;
|
||||
self.command_line.push_output(
|
||||
crate::tf!("{} open. Draw on the plane, then FINISHSKETCH.", name).as_ref(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Leave sketch mode, restoring what the sketch borrowed.
|
||||
///
|
||||
/// Whatever was drawn becomes the "Previous" selection set, so the next
|
||||
/// EXTRUDE can take it with the Previous keyword instead of making the
|
||||
/// user reselect a profile they just finished drawing.
|
||||
fn finish_sketch(&mut self, i: usize) {
|
||||
let Some(session) = self.tabs[i].sketch_session.take() else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("No sketch is open.").as_ref());
|
||||
return;
|
||||
};
|
||||
let created: Vec<acadrust::Handle> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.map(|entity| entity.common().handle)
|
||||
.filter(|handle| !session.opened_with.contains(handle))
|
||||
.collect();
|
||||
self.tabs[i].active_ucs = session.previous_ucs;
|
||||
self.snapper.snap_enabled = session.previous_snap_enabled;
|
||||
self.commit_active_ucs_change(i, "FINISHSKETCH");
|
||||
if created.is_empty() {
|
||||
self.command_line.push_output(
|
||||
crate::tf!("{} closed; nothing was drawn.", session.name).as_ref(),
|
||||
);
|
||||
} else {
|
||||
let count = created.len();
|
||||
self.tabs[i].prev_selection = created;
|
||||
self.command_line.push_output(
|
||||
crate::tf!(
|
||||
"{} closed with {} object(s). EXTRUDE then Previous uses them.",
|
||||
session.name,
|
||||
count
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_snap(&mut self, i: usize, r_ucs: glam::Mat4) {
|
||||
let eye_dir = r_ucs.transform_vector3(
|
||||
crate::scene::CubeRegion::Face(crate::scene::pipeline::viewcube::FACE_TOP)
|
||||
|
||||
@@ -739,6 +739,11 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"SELSIM",
|
||||
// Draw a new object of the same type as the selected one. (#239)
|
||||
"ADDSELECTED",
|
||||
// Open and close a sketch: a drawing plane, a square-on view and
|
||||
// snapping, held together until the profile is done.
|
||||
"BROWSER",
|
||||
"CREATESKETCH",
|
||||
"FINISHSKETCH",
|
||||
"SHEETSET",
|
||||
"SHORTCUTS",
|
||||
"SSM",
|
||||
|
||||
@@ -101,6 +101,27 @@ fn default_role_for(component: DynComponent) -> crate::command::DynRole {
|
||||
}
|
||||
}
|
||||
|
||||
/// An open sketch — the state CREATESKETCH parks so FINISHSKETCH can put it
|
||||
/// back.
|
||||
///
|
||||
/// A sketch here is a *mode*, not a document object: the drawing plane, a
|
||||
/// square-on view, and snapping forced live. The geometry it produces is
|
||||
/// ordinary model-space entities, which is what keeps the result a normal
|
||||
/// DWG that other software can open. `opened_with` is the handle set at the
|
||||
/// moment the sketch opened, so the difference on finish is exactly what the
|
||||
/// user drew — that difference is what gets offered to EXTRUDE.
|
||||
pub(super) struct SketchSession {
|
||||
/// Display name, e.g. `"Sketch1"`.
|
||||
pub(super) name: String,
|
||||
/// UCS active before the sketch opened. Restored on finish.
|
||||
pub(super) previous_ucs: Option<Ucs>,
|
||||
/// Snap master state before the sketch forced it on, so a user who works
|
||||
/// with snapping off gets that back rather than silently keeping it.
|
||||
pub(super) previous_snap_enabled: bool,
|
||||
/// Entity handles already present when the sketch opened.
|
||||
pub(super) opened_with: std::collections::HashSet<acadrust::Handle>,
|
||||
}
|
||||
|
||||
// ── Per-document tab state ─────────────────────────────────────────────────
|
||||
|
||||
pub(super) struct DocumentTab {
|
||||
@@ -186,6 +207,11 @@ pub(super) struct DocumentTab {
|
||||
pub(super) active_layer: String,
|
||||
/// Currently active UCS. `None` means WCS (identity transform).
|
||||
pub(super) active_ucs: Option<Ucs>,
|
||||
/// Open sketch, if CREATESKETCH is in effect. `None` is the normal
|
||||
/// direct-modelling state.
|
||||
pub(super) sketch_session: Option<SketchSession>,
|
||||
/// Sketches opened in this tab so far, so each gets a distinct name.
|
||||
pub(super) sketch_count: u32,
|
||||
/// Custom model-space background color. `None` = default dark grey.
|
||||
pub(super) bg_color: Option<[f32; 4]>,
|
||||
/// Custom paper-space background color. `None` = default off-white grey.
|
||||
@@ -629,6 +655,8 @@ impl DocumentTab {
|
||||
history: HistoryState::default(),
|
||||
active_layer: "0".to_string(),
|
||||
active_ucs: None,
|
||||
sketch_session: None,
|
||||
sketch_count: 0,
|
||||
bg_color: None,
|
||||
paper_bg_color: None,
|
||||
refedit_session: None,
|
||||
|
||||
Reference in new issue
Block a user