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:
Matus Mastena committed 2026-09-17 16:51:41 +02:00
1 parent bfb786d56e
commit 1aa299e204
3 files changed
+156

No files matched your search

+123
View File
@@ -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)
+5
View File
@@ -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",
+28
View File
@@ -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,