mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
Merge main 176c22aca into smlib
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SdcgEu4Lr3moCCisdZShKz
This commit is contained in:
@@ -1918,6 +1918,8 @@
|
||||
"motion.track_name": "المسار {number}",
|
||||
"nav.alias.help": "Shift+Alt مع السحب بالزر الأيسر يدوّر العرض، وShift+Alt مع الأوسط يُزيحه، وShift+Alt مع الأيمن يكبّره؛ والسحب بالزر الأوسط يُزيح أيضًا والسحب بالأيمن يدوّر",
|
||||
"nav.blender.help": "السحب بالزر الأوسط يدوّر العرض، وShift مع الأوسط يُزيحه، وCtrl مع الأوسط يكبّره؛ والسحب بالأيمن لا يحرك الكاميرا",
|
||||
"nav.creo.help": "السحب بالزر الأوسط يدوّر العرض، وShift مع الأوسط يُزيحه، وCtrl مع السحب العمودي بالأوسط يكبّره أو يصغّره (السحب لأسفل للتكبير)؛ وتدوير العجلة يكبّره أو يصغّره",
|
||||
"nav.creo.label": "Creo",
|
||||
"nav.free_orbit.help": "الفأرة والفأرة ثلاثية الأبعاد تدوّران العرض عبر القطبين وتحافظان على اللف. يُعطَّل التقاط المحاور بـ Shift.",
|
||||
"nav.free_orbit.label": "تدوير حر",
|
||||
"nav.free_orbit.snap_help": "الفأرة والفأرة ثلاثية الأبعاد تدوّران العرض عبر القطبين وتحافظان على اللف. اضغط مطولًا على Shift أثناء سحب التدوير لالتقاط اتجاه العرض واللف.",
|
||||
|
||||
@@ -1916,6 +1916,8 @@
|
||||
"motion.track_name": "Spur {number}",
|
||||
"nav.alias.help": "Umschalt+Alt+Linksziehen dreht, Umschalt+Alt+Mitte verschiebt, Umschalt+Alt+Rechtsziehen zoomt; mittleres Ziehen verschiebt ebenfalls und rechtes Ziehen dreht",
|
||||
"nav.blender.help": "Mittlere Maustaste dreht, Umschalt+Mitte verschiebt, Strg+Mitte zoomt; rechtes Ziehen lässt die Kamera unverändert",
|
||||
"nav.creo.help": "Ziehen mit der mittleren Maustaste dreht, Umschalt+Mitte verschiebt, Strg+Mitte zoomt beim vertikalen Ziehen (nach unten zum Vergrößern); das Mausrad zoomt",
|
||||
"nav.creo.label": "Creo",
|
||||
"nav.free_orbit.help": "Maus und 3D-Maus drehen über beide Pole hinweg und behalten die Rollneigung bei. Das Einrasten mit Umschalt ist deaktiviert.",
|
||||
"nav.free_orbit.label": "Freie Drehung",
|
||||
"nav.free_orbit.snap_help": "Maus und 3D-Maus drehen über beide Pole hinweg und behalten die Rollausrichtung bei. Halten Sie beim Ziehen zum Drehen die Umschalttaste gedrückt, um Blickrichtung und Rollausrichtung einrasten zu lassen.",
|
||||
|
||||
@@ -1918,6 +1918,8 @@
|
||||
"motion.track_name": "Track {number}",
|
||||
"nav.alias.help": "Shift+Alt+left drag orbits, Shift+Alt+middle pans, Shift+Alt+right zooms; middle drag also pans and right drag orbits",
|
||||
"nav.blender.help": "Middle drag orbits, Shift+middle pans, Ctrl+middle zooms; right drag leaves the camera alone",
|
||||
"nav.creo.help": "Middle drag orbits, Shift+middle pans, Ctrl+middle drag vertically zooms (down to zoom in); scroll the wheel to zoom",
|
||||
"nav.creo.label": "Creo",
|
||||
"nav.free_orbit.help": "Mouse and 3D mouse rotate through both poles and preserve roll. Shift axis snapping is disabled.",
|
||||
"nav.free_orbit.label": "Free orbit",
|
||||
"nav.free_orbit.snap_help": "Mouse and 3D mouse rotate through both poles and preserve roll. Hold Shift during an orbit drag to snap the view direction and roll.",
|
||||
|
||||
@@ -1918,6 +1918,8 @@
|
||||
"motion.track_name": "רצועה {number}",
|
||||
"nav.alias.help": "Shift+Alt וגרירה שמאלית מסובבים, Shift+Alt וגרירה אמצעית מזיזים, Shift+Alt וגרירה ימנית מבצעים זום; גם גרירה אמצעית מזיזה וגרירה ימנית מסובבת",
|
||||
"nav.blender.help": "גרירה אמצעית מסובבת, Shift וגרירה אמצעית מזיזים, Ctrl וגרירה אמצעית מבצעים זום; גרירה ימנית לא מזיזה את המצלמה",
|
||||
"nav.creo.help": "גרירה בלחצן האמצעי מסובבת את התצוגה, Shift והלחצן האמצעי מזיזים אותה, Ctrl וגרירה אנכית בלחצן האמצעי משנים את הזום (גרירה מטה להגדלה); גלילת הגלגלת משנה גם היא את הזום",
|
||||
"nav.creo.label": "Creo",
|
||||
"nav.free_orbit.help": "העכבר והעכבר התלת־ממדי מסובבים דרך שני הקטבים ושומרים על הגלגול. הצמדת הצירים עם Shift מושבתת.",
|
||||
"nav.free_orbit.label": "סיבוב חופשי",
|
||||
"nav.free_orbit.snap_help": "העכבר והעכבר התלת־ממדי מסובבים דרך שני הקטבים ושומרים על הגלגול. יש להחזיק את Shift בזמן גרירת סיבוב כדי להצמיד את כיוון התצוגה ואת הגלגול.",
|
||||
|
||||
@@ -1918,6 +1918,8 @@
|
||||
"motion.track_name": "トラック {number}",
|
||||
"nav.alias.help": "Shift+Alt+左ドラッグで回転、Shift+Alt+中ドラッグでパン、Shift+Alt+右ドラッグでズーム。中ドラッグでもパン、右ドラッグで回転。",
|
||||
"nav.blender.help": "中ドラッグで回転、Shift+中ドラッグでパン、Ctrl+中ドラッグでズーム。右ドラッグではカメラは動きません。",
|
||||
"nav.creo.help": "マウスの中ボタンドラッグで回転、Shift+中ボタンで平行移動、Ctrl+中ボタンの上下ドラッグでズーム(下方向で拡大)。ホイールの回転でもズーム",
|
||||
"nav.creo.label": "Creo",
|
||||
"nav.free_orbit.help": "マウスと3Dマウスで、極点を越えて回転し、ロールを保ちます。\nShiftによる軸スナップは無効になります。",
|
||||
"nav.free_orbit.label": "自由回転",
|
||||
"nav.free_orbit.snap_help": "マウスと3Dマウスで両極を越えて回転でき、ロールも保持されます。オービットのドラッグ中にShiftを押すと、視線方向とロールがスナップします。",
|
||||
|
||||
@@ -1353,6 +1353,8 @@
|
||||
"motion.track_name": "트랙 {number}",
|
||||
"nav.alias.help": "Shift+Alt+왼쪽 드래그는 회전, Shift+Alt+가운데 드래그는 이동, Shift+Alt+오른쪽 드래그는 확대/축소입니다. 가운데 드래그로도 이동하고 오른쪽 드래그로도 회전합니다",
|
||||
"nav.blender.help": "가운데 드래그는 회전, Shift+가운데 드래그는 이동, Ctrl+가운데 드래그는 확대/축소입니다. 오른쪽 드래그는 카메라에 영향을 주지 않습니다",
|
||||
"nav.creo.help": "가운데 버튼을 끌면 회전, Shift+가운데 버튼은 이동, Ctrl+가운데 버튼을 위아래로 끌면 확대/축소합니다(아래로 끌면 확대). 휠을 굴려도 확대/축소합니다",
|
||||
"nav.creo.label": "Creo",
|
||||
"nav.free_orbit.help": "마우스와 3D 마우스로 롤을 유지하며 양 극점을 넘어 회전합니다. Shift 축 스냅은 비활성화됩니다.",
|
||||
"nav.free_orbit.label": "자유 회전",
|
||||
"nav.free_orbit.snap_help": "마우스와 3D 마우스로 양쪽 극점을 지나 회전할 수 있으며 롤 각도가 유지됩니다. 궤도 회전 드래그 중 Shift를 누르면 보기 방향과 롤 각도가 스냅됩니다.",
|
||||
|
||||
@@ -1918,6 +1918,8 @@
|
||||
"motion.track_name": "Rãnh {number}",
|
||||
"nav.alias.help": "Shift+Alt+kéo chuột trái để xoay, Shift+Alt+chuột giữa để trượt, Shift+Alt+chuột phải để thu phóng; kéo chuột giữa cũng trượt và kéo chuột phải xoay",
|
||||
"nav.blender.help": "Kéo chuột giữa để xoay, Shift+chuột giữa để trượt, Ctrl+chuột giữa để thu phóng; kéo chuột phải giữ nguyên camera",
|
||||
"nav.creo.help": "Kéo nút giữa để xoay, Shift+nút giữa để di chuyển, Ctrl+kéo nút giữa theo chiều dọc để thu phóng (kéo xuống để phóng to); lăn con lăn để thu phóng",
|
||||
"nav.creo.label": "Creo",
|
||||
"nav.free_orbit.help": "Chuột và chuột 3D xoay tự do qua cả hai cực và giữ nguyên độ lăn nghiêng (Roll). Bắt dính trục bằng phím Shift sẽ bị tắt.",
|
||||
"nav.free_orbit.label": "Xoay góc nhìn tự do",
|
||||
"nav.free_orbit.snap_help": "Chuột và chuột 3D xoay qua cả hai cực và giữ nguyên góc nghiêng cuộn. Giữ Shift trong khi kéo xoay quỹ đạo để bắt dính hướng nhìn và góc nghiêng cuộn.",
|
||||
|
||||
@@ -1918,6 +1918,8 @@
|
||||
"motion.track_name": "轨道 {number}",
|
||||
"nav.alias.help": "Shift+Alt+左键拖动旋转,Shift+Alt+中键拖动平移,Shift+Alt+右键拖动缩放;中键拖动也可平移,右键拖动旋转",
|
||||
"nav.blender.help": "中键拖动旋转,Shift+中键拖动平移,Ctrl+中键拖动缩放;右键拖动不会移动相机",
|
||||
"nav.creo.help": "中键拖动旋转,Shift+中键平移,Ctrl+中键上下拖动缩放(向下放大);滚动滚轮也可缩放",
|
||||
"nav.creo.label": "Creo",
|
||||
"nav.free_orbit.help": "鼠标和3D鼠标可保持滚转并越过两个极点旋转。Shift轴吸附将被禁用。",
|
||||
"nav.free_orbit.label": "自由旋转",
|
||||
"nav.free_orbit.snap_help": "鼠标和3D鼠标可越过两极旋转,并保留画面旋转角度。在拖动环绕时按住Shift,可吸附视图方向和画面旋转角度。",
|
||||
|
||||
@@ -1271,6 +1271,77 @@ fn tool_unknown_command_errors_explain_packed_params() {
|
||||
assert!(body["failed"]["error"].as_str().unwrap().contains("command ids have no parameters; pass them in `params`"), "{body}");
|
||||
}
|
||||
|
||||
/// #10678: through direct `execute`, an unknown command id that is exactly a registered
|
||||
/// MCP tool name gets the namespace hint instead of unrelated closest-command suggestions.
|
||||
#[test]
|
||||
fn tool_name_command_ids_get_namespace_hint_via_execute() {
|
||||
let mut s = server();
|
||||
for id in ["execute", "list_commands", "describe_command", "screenshot"] {
|
||||
let r = raw_tool(&mut s, "execute", json!({"command": id, "params": {}}));
|
||||
assert_eq!(r["result"]["isError"], true, "{r}");
|
||||
let text = r["result"]["content"][0]["text"].as_str().unwrap();
|
||||
assert!(text.starts_with(&format!("unknown command `{id}`")), "{text}");
|
||||
assert!(text.contains(&format!("`{id}` is an MCP tool, not an engine command id")), "{text}");
|
||||
assert!(text.contains("tools/list") && text.contains("list_commands"), "{text}");
|
||||
assert!(!text.contains("closest:"), "{text}");
|
||||
}
|
||||
}
|
||||
|
||||
/// #10678: the direct `describe_command` hint inspects the requested `id`, not the outer
|
||||
/// `engine.describe` command `exec` sees.
|
||||
#[test]
|
||||
fn tool_name_command_ids_get_namespace_hint_via_describe_command() {
|
||||
let mut s = server();
|
||||
for id in ["execute", "list_commands"] {
|
||||
let r = raw_tool(&mut s, "describe_command", json!({"id": id}));
|
||||
assert_eq!(r["result"]["isError"], true, "{r}");
|
||||
let text = r["result"]["content"][0]["text"].as_str().unwrap();
|
||||
assert!(text.starts_with(&format!("unknown command `{id}`")), "{text}");
|
||||
assert!(text.contains(&format!("`{id}` is an MCP tool, not an engine command id")), "{text}");
|
||||
assert!(!text.contains("engine.describe"), "{text}");
|
||||
}
|
||||
// Control: a valid engine command id still describes itself.
|
||||
let r = raw_tool(&mut s, "describe_command", json!({"id": "solid.box"}));
|
||||
assert_ne!(r["result"]["isError"], true, "{r}");
|
||||
}
|
||||
|
||||
/// #10678: an engine-id typo keeps its closest-command suggestions; the tool-name hint is
|
||||
/// only for exact registered tool names.
|
||||
#[test]
|
||||
fn engine_command_id_typo_still_gets_closest_suggestions() {
|
||||
let mut s = server();
|
||||
let r = raw_tool(&mut s, "execute", json!({"command": "solid.bxo"}));
|
||||
assert_eq!(r["result"]["isError"], true, "{r}");
|
||||
let text = r["result"]["content"][0]["text"].as_str().unwrap();
|
||||
assert!(text.contains("closest: `solid.box`"), "{text}");
|
||||
assert!(!text.contains("MCP tool"), "{text}");
|
||||
}
|
||||
|
||||
/// #10678: a genuinely unknown id keeps the ordinary unknown-command error on both paths.
|
||||
#[test]
|
||||
fn genuinely_unknown_command_id_keeps_the_ordinary_error() {
|
||||
let mut s = server();
|
||||
for (name, args) in [("execute", json!({"command": "madeup.thing"})), ("describe_command", json!({"id": "madeup.thing"}))] {
|
||||
let r = raw_tool(&mut s, name, args);
|
||||
assert_eq!(r["result"]["isError"], true, "{r}");
|
||||
let text = r["result"]["content"][0]["text"].as_str().unwrap();
|
||||
assert!(text.contains("unknown command `madeup.thing`"), "{text}");
|
||||
assert!(!text.contains("MCP tool"), "{text}");
|
||||
}
|
||||
}
|
||||
|
||||
/// #10678: a valid engine command id that overlaps a tool name resolves as a command —
|
||||
/// the engine accepts it before any tool-name hint could apply.
|
||||
#[test]
|
||||
fn valid_engine_command_id_overlapping_a_tool_name_resolves_as_command() {
|
||||
let mut b = Recorder { calls: Vec::new(), batch: Ok(json!({})) };
|
||||
let r = crate::tools::call_tool(&mut b, "execute", &json!({"command": "measure"}));
|
||||
assert_ne!(r["isError"], json!(true), "{r}");
|
||||
assert!(r["content"][0]["text"].as_str().unwrap().contains("measure"), "{r}");
|
||||
let r = crate::tools::call_tool(&mut b, "describe_command", &json!({"id": "measure"}));
|
||||
assert_ne!(r["isError"], json!(true), "{r}");
|
||||
}
|
||||
|
||||
fn hostile_values() -> Vec<Value> {
|
||||
vec![
|
||||
Value::Null,
|
||||
|
||||
+31
-1
@@ -729,6 +729,11 @@ fn command_error(command: &str, error: String) -> String {
|
||||
return error;
|
||||
}
|
||||
let id = command.split(['{', '(']).next().unwrap_or(command).trim();
|
||||
// An id that is exactly a registered tool name confuses the two namespaces: name the
|
||||
// distinction and its recovery instead of offering unrelated closest-command suggestions.
|
||||
if id == command.trim() && is_tool_name(id) {
|
||||
return tool_name_error(id);
|
||||
}
|
||||
let packed = id != command.trim();
|
||||
let normalize = |s: &str| s.chars().filter(char::is_ascii_alphanumeric).map(|c| c.to_ascii_lowercase()).collect::<String>();
|
||||
let normalized = normalize(id);
|
||||
@@ -762,6 +767,29 @@ fn command_error(command: &str, error: String) -> String {
|
||||
message
|
||||
}
|
||||
|
||||
/// Whether `name` is exactly a registered MCP tool name, read from the tool definitions
|
||||
/// themselves so the two namespaces can never drift apart.
|
||||
fn is_tool_name(name: &str) -> bool {
|
||||
tool_definitions().iter().any(|t| t["name"].as_str() == Some(name))
|
||||
}
|
||||
|
||||
/// The reply when a rejected command id is exactly a tool name: the recovery belongs to the
|
||||
/// tool namespace, not to a typo's closest-command suggestions.
|
||||
fn tool_name_error(id: &str) -> String {
|
||||
format!(
|
||||
"unknown command `{id}`: `{id}` is an MCP tool, not an engine command id. Use MCP tools/list for its tool schema, or list_commands for engine command ids."
|
||||
)
|
||||
}
|
||||
|
||||
/// A `describe_command` rejection of an id that is exactly a tool name gets the namespace
|
||||
/// hint; every other error is left exactly as the engine said it.
|
||||
fn describe_error(id: &str, error: String) -> String {
|
||||
if error.starts_with(&format!("unknown command `{id}`")) && is_tool_name(id) {
|
||||
return tool_name_error(id);
|
||||
}
|
||||
error
|
||||
}
|
||||
|
||||
fn command_distance(a: &str, b: &str) -> usize {
|
||||
let mut row: Vec<usize> = (1..=b.len()).collect();
|
||||
for (i, ca) in a.bytes().enumerate() {
|
||||
@@ -808,7 +836,9 @@ fn dispatch(b: &mut dyn Backend, name: &str, a: &Map<String, Value>) -> Result<V
|
||||
}
|
||||
"describe_command" => {
|
||||
let id = str_arg(a, "id").ok_or("`id` is required")?;
|
||||
exec(b, "engine.describe", json!({"command": id}))
|
||||
// The engine rejects the requested id inside its own error, so the tool-name
|
||||
// hint must inspect the `id`, not the outer `engine.describe` command `exec` sees.
|
||||
exec(b, "engine.describe", json!({"command": id})).map_err(|e| describe_error(id, e))
|
||||
}
|
||||
"execute" => {
|
||||
let id = str_arg(a, "command").ok_or("`command` is required")?;
|
||||
|
||||
@@ -206,6 +206,8 @@ mod prefs_search;
|
||||
#[cfg(test)]
|
||||
mod preselect_tests;
|
||||
pub mod preview;
|
||||
#[cfg(test)]
|
||||
mod preview_placement_tests;
|
||||
mod primitive_placement;
|
||||
#[cfg(test)]
|
||||
mod primitive_units_tests;
|
||||
|
||||
@@ -1153,7 +1153,7 @@ mod tests {
|
||||
let mut app = app();
|
||||
assert!(!active(&app), "off by default");
|
||||
app.preferences.mouse_gestures.enabled = true;
|
||||
for nav in ["fusion", "inventor", "alias", "blender", "tinkercad", "openscad", "touchpad"] {
|
||||
for nav in ["fusion", "creo", "inventor", "alias", "blender", "tinkercad", "openscad", "touchpad"] {
|
||||
app.preferences.nav = nav.into();
|
||||
assert!(!active(&app), "{nav}: gestures only work with the SolidWorks mouse mode");
|
||||
}
|
||||
|
||||
@@ -815,7 +815,8 @@ pub(crate) fn handle(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect, resp: &e
|
||||
Some(NavMode::Zoom) => {
|
||||
let press = ui.input(|i| i.pointer.press_origin());
|
||||
let anchor = model_zoom_anchor(app, rect, press.or(hover), caps.orbit);
|
||||
app.cam.zoom_at((f64::from(delta.y) * 0.01).exp(), anchor);
|
||||
let sign = if crate::prefs::nav_preset(&app.preferences.nav).reverse_drag_zoom { -1.0 } else { 1.0 };
|
||||
app.cam.zoom_at((f64::from(delta.y) * 0.01 * sign).exp(), anchor);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
+107
-1
@@ -89,6 +89,8 @@ pub struct NavPreset {
|
||||
pub help: &'static str,
|
||||
/// Choosing the preset turns the wheel the other way.
|
||||
pub reverse_zoom: bool,
|
||||
/// Dragging down zooms in instead of out (independent of the wheel preference).
|
||||
pub reverse_drag_zoom: bool,
|
||||
/// Choosing the preset sets what a middle double-click does (`None` leaves it as it is).
|
||||
pub middle_double_click: Option<&'static str>,
|
||||
/// Choosing the preset sets "Right drag orbits" (`None` leaves it as it is).
|
||||
@@ -113,6 +115,7 @@ pub const NAV_PRESETS: &[NavPreset] = &[
|
||||
label: "Fusion",
|
||||
help: "Middle drag pans, Shift+middle orbits, right drag orbits",
|
||||
reverse_zoom: false,
|
||||
reverse_drag_zoom: false,
|
||||
middle_double_click: None,
|
||||
right_drag_orbits: None,
|
||||
bindings: &[bind(Middle, NavKeys::SHIFT, Orbit), bind(Middle, NavKeys::NONE, Pan), bind(Right, NavKeys::NONE, Orbit)],
|
||||
@@ -122,6 +125,7 @@ pub const NAV_PRESETS: &[NavPreset] = &[
|
||||
label: "SolidWorks",
|
||||
help: "Middle drag orbits, Ctrl+middle pans, Shift+middle zooms, right drag orbits",
|
||||
reverse_zoom: true,
|
||||
reverse_drag_zoom: false,
|
||||
// Picked here, the right button is left to the context menu and mouse gestures (#6492).
|
||||
middle_double_click: Some("fit"),
|
||||
right_drag_orbits: Some(false),
|
||||
@@ -132,6 +136,18 @@ pub const NAV_PRESETS: &[NavPreset] = &[
|
||||
bind(Right, NavKeys::NONE, Orbit),
|
||||
],
|
||||
},
|
||||
// Public Creo Parametric dynamic-viewing bindings; no product code or assets are used.
|
||||
// https://support.ptc.com/help/creo/creo_pma/r12/usascii/fundamentals/fundamentals/About_Dynamic_Viewing.html
|
||||
NavPreset {
|
||||
id: "creo",
|
||||
label: "Creo",
|
||||
help: "Middle drag orbits, Shift+middle pans, Ctrl+middle drag vertically zooms (down to zoom in); scroll the wheel to zoom",
|
||||
reverse_zoom: false,
|
||||
reverse_drag_zoom: true,
|
||||
middle_double_click: None,
|
||||
right_drag_orbits: None,
|
||||
bindings: &[bind(Middle, NavKeys::CTRL, Zoom), bind(Middle, NavKeys::SHIFT, Pan), bind(Middle, NavKeys::NONE, Orbit)],
|
||||
},
|
||||
NavPreset {
|
||||
id: "nx",
|
||||
label: "NX",
|
||||
@@ -140,6 +156,7 @@ pub const NAV_PRESETS: &[NavPreset] = &[
|
||||
// middle drag with Ctrl (zoom) and Shift (pan).
|
||||
help: "Middle drag orbits, Shift+middle pans (NX's middle + right), Ctrl+middle zooms (NX's middle + left); right drag leaves the camera",
|
||||
reverse_zoom: false,
|
||||
reverse_drag_zoom: false,
|
||||
middle_double_click: Some("fit"),
|
||||
right_drag_orbits: Some(false),
|
||||
bindings: &[bind(Middle, NavKeys::SHIFT, Pan), bind(Middle, NavKeys::CTRL, Zoom), bind(Middle, NavKeys::NONE, Orbit)],
|
||||
@@ -149,6 +166,7 @@ pub const NAV_PRESETS: &[NavPreset] = &[
|
||||
label: "Inventor",
|
||||
help: "Middle drag pans, Shift+middle orbits, Ctrl+middle zooms, right drag orbits",
|
||||
reverse_zoom: false,
|
||||
reverse_drag_zoom: false,
|
||||
middle_double_click: None,
|
||||
right_drag_orbits: None,
|
||||
bindings: &[
|
||||
@@ -163,6 +181,7 @@ pub const NAV_PRESETS: &[NavPreset] = &[
|
||||
label: "Alias",
|
||||
help: "Shift+Alt+left drag orbits, Shift+Alt+middle pans, Shift+Alt+right zooms; middle drag also pans and right drag orbits",
|
||||
reverse_zoom: false,
|
||||
reverse_drag_zoom: false,
|
||||
middle_double_click: None,
|
||||
right_drag_orbits: None,
|
||||
bindings: &[
|
||||
@@ -179,6 +198,7 @@ pub const NAV_PRESETS: &[NavPreset] = &[
|
||||
label: "Maya",
|
||||
help: "Alt+left drag orbits, Alt+middle pans, Alt+right zooms; middle drag also pans",
|
||||
reverse_zoom: false,
|
||||
reverse_drag_zoom: false,
|
||||
middle_double_click: None,
|
||||
right_drag_orbits: None,
|
||||
bindings: &[
|
||||
@@ -193,6 +213,7 @@ pub const NAV_PRESETS: &[NavPreset] = &[
|
||||
label: "Blender",
|
||||
help: "Middle drag orbits, Shift+middle pans, Ctrl+middle zooms; right drag leaves the camera alone",
|
||||
reverse_zoom: false,
|
||||
reverse_drag_zoom: false,
|
||||
middle_double_click: None,
|
||||
right_drag_orbits: None,
|
||||
bindings: &[bind(Middle, NavKeys::SHIFT, Pan), bind(Middle, NavKeys::CTRL, Zoom), bind(Middle, NavKeys::NONE, Orbit)],
|
||||
@@ -202,6 +223,7 @@ pub const NAV_PRESETS: &[NavPreset] = &[
|
||||
label: "Tinkercad",
|
||||
help: "Right drag orbits, middle or Shift+right drag pans",
|
||||
reverse_zoom: false,
|
||||
reverse_drag_zoom: false,
|
||||
middle_double_click: None,
|
||||
right_drag_orbits: None,
|
||||
bindings: &[bind(Right, NavKeys::SHIFT, Pan), bind(Right, NavKeys::NONE, Orbit), bind(Middle, NavKeys::NONE, Pan)],
|
||||
@@ -211,6 +233,7 @@ pub const NAV_PRESETS: &[NavPreset] = &[
|
||||
label: "OpenSCAD",
|
||||
help: "Left drag on empty space orbits, Shift+left drag box-selects, sketch geometry drags edit; right or middle drag pans, Shift+right or Shift+middle drag zooms",
|
||||
reverse_zoom: false,
|
||||
reverse_drag_zoom: false,
|
||||
middle_double_click: None,
|
||||
right_drag_orbits: None,
|
||||
bindings: &[
|
||||
@@ -226,6 +249,7 @@ pub const NAV_PRESETS: &[NavPreset] = &[
|
||||
label: "HFSS",
|
||||
help: "Alt+left drag pans, Shift+Alt+left zooms, Ctrl+left drag on empty space orbits",
|
||||
reverse_zoom: false,
|
||||
reverse_drag_zoom: false,
|
||||
middle_double_click: None,
|
||||
right_drag_orbits: None,
|
||||
// Ctrl+left orbit is the drag from empty space every preset has: as a binding it would
|
||||
@@ -237,6 +261,7 @@ pub const NAV_PRESETS: &[NavPreset] = &[
|
||||
label: "Touchpad",
|
||||
help: "Hold Shift and move the pointer to pan; hold Alt/Option and move it to orbit. No mouse button is needed. Vertical scroll zooms.",
|
||||
reverse_zoom: false,
|
||||
reverse_drag_zoom: false,
|
||||
middle_double_click: None,
|
||||
right_drag_orbits: None,
|
||||
bindings: &[bind(Middle, NavKeys::NONE, Pan), bind(Right, NavKeys::NONE, Orbit)],
|
||||
@@ -257,7 +282,15 @@ impl NavPreset {
|
||||
}
|
||||
|
||||
pub(crate) fn translated_label(&self) -> &str {
|
||||
if self.id == TOUCHPAD_NAV { crate::i18n::tr("nav.touchpad.label") } else { crate::i18n::tr_label(self.label) }
|
||||
match self.id {
|
||||
"creo" => crate::i18n::tr("nav.creo.label"),
|
||||
TOUCHPAD_NAV => crate::i18n::tr("nav.touchpad.label"),
|
||||
_ => crate::i18n::tr_label(self.label),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn translated_help(&self) -> &str {
|
||||
if self.id == "creo" { crate::i18n::tr("nav.creo.help") } else { crate::i18n::tr_label(self.help) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8634,6 +8667,79 @@ mod tests {
|
||||
assert_eq!(nav_mode(&app, NavButton::Right, none), Some(NavMode::Orbit), "turned back on, the right drag orbits again");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creo_preset_maps_navigation_and_persists() {
|
||||
let mut app = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
|
||||
assert_eq!(app.preferences.nav, "fusion");
|
||||
choose_nav_preset(&mut app.preferences, nav_preset("creo"));
|
||||
assert_eq!(app.preferences.nav, "creo");
|
||||
assert!(!app.preferences.zoom_reverse);
|
||||
for (mods, mode) in [
|
||||
(Modifiers::NONE, NavMode::Orbit),
|
||||
(Modifiers::SHIFT, NavMode::Pan),
|
||||
(Modifiers::CTRL, NavMode::Zoom),
|
||||
(Modifiers::COMMAND, NavMode::Zoom),
|
||||
] {
|
||||
assert_eq!(nav_mode(&app, NavButton::Middle, mods), Some(mode));
|
||||
}
|
||||
for button in [NavButton::Left, NavButton::Right] {
|
||||
assert_eq!(nav_mode(&app, button, Modifiers::NONE), None);
|
||||
}
|
||||
assert!(nav_preset("creo").reverse_drag_zoom);
|
||||
for preset in NAV_PRESETS.iter().filter(|p| p.id != "creo") {
|
||||
assert!(!preset.reverse_drag_zoom, "{} keeps its drag zoom direction", preset.id);
|
||||
}
|
||||
let mut other = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
|
||||
other.load_prefs(&app.prefs());
|
||||
assert_eq!(other.preferences.nav, "creo");
|
||||
assert_eq!(nav_mode(&other, NavButton::Middle, Modifiers::NONE), Some(NavMode::Orbit));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creo_navigation_settings_show_localised_bindings() {
|
||||
let _language = crate::i18n::scope("de");
|
||||
let mut app = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
|
||||
control(&mut app, &json!({"open":"Navigation", "nav":"creo", "language":"de"})).unwrap();
|
||||
let ctx = crate::fonts::test_context();
|
||||
let screen = egui::Rect::from_min_size(Pos2::ZERO, egui::vec2(1400.0, 1000.0));
|
||||
let mut textures = std::collections::HashMap::new();
|
||||
let mut draw = || {
|
||||
let raw = egui::RawInput { screen_rect: Some(screen), ..Default::default() };
|
||||
let mut output = ctx.run_ui(raw, |ui| show(&mut app, ui.ctx()));
|
||||
crate::ui_kit::test_textures_update(&mut textures, &output.textures_delta);
|
||||
output.textures_delta.clear();
|
||||
output
|
||||
};
|
||||
for _ in 0..3 {
|
||||
draw();
|
||||
}
|
||||
let output = draw();
|
||||
for label in ["Creo", crate::i18n::tr("prefs.gesture_orbit"), crate::i18n::tr("prefs.gesture_pan"), crate::i18n::tr("prefs.gesture_zoom")] {
|
||||
assert!(crate::test_support::text_shapes(&output).any(|text| text.galley.text().contains(label)), "missing binding label: {label}");
|
||||
}
|
||||
assert_ne!(nav_preset("creo").translated_help(), nav_preset("creo").help);
|
||||
if let Ok(dir) = std::env::var("SOLIDCRAFT_CREO_EVIDENCE") {
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
crate::ui_kit::test_raster(&ctx, output.shapes, output.pixels_per_point, screen, &textures)
|
||||
.save(std::path::Path::new(&dir).join("creo-navigation-de.png"))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creo_preset_obeys_preference_transactions() {
|
||||
let mut app = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
|
||||
control(&mut app, &json!({"open":"Navigation", "nav":"creo"})).unwrap();
|
||||
assert_eq!(app.preferences.nav, "creo");
|
||||
control(&mut app, &json!({"button":"cancel"})).unwrap();
|
||||
assert_eq!(app.preferences.nav, "fusion");
|
||||
control(&mut app, &json!({"open":"Navigation", "nav":"creo", "button":"apply"})).unwrap();
|
||||
control(&mut app, &json!({"button":"defaults"})).unwrap();
|
||||
assert_eq!(app.preferences.nav, "fusion");
|
||||
control(&mut app, &json!({"button":"cancel"})).unwrap();
|
||||
assert_eq!(app.preferences.nav, "creo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openscad_preset_maps_navigation_and_persists() {
|
||||
let mut app = SolveApp::new(solidcraft_engine::Session::default(), Default::default());
|
||||
|
||||
@@ -61,7 +61,7 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
}
|
||||
});
|
||||
crate::scenario::publish_target("dialog:Pan, zoom, orbit", picker.rect);
|
||||
picker.on_hover_text(crate::i18n::tr_label(nav_preset(&app.preferences.nav).help));
|
||||
picker.on_hover_text(nav_preset(&app.preferences.nav).translated_help());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
//! #2103: the live preview of an Extrude (Cut) on a sketch attached to a face of a body inside
|
||||
//! a placed component occurrence must be drawn where the viewport shows the sketch, not at the
|
||||
//! component's own frame. The committed result was always correct; the preview used to be built
|
||||
//! from the component-local model state (`Session::preview`) while the viewport shows placed
|
||||
//! occurrences, so the tool appeared offset by the placement — seen on a mirrored STEP copy,
|
||||
//! and on a plain translated copy of a component. The fix routes the tool preview through
|
||||
//! `Session::preview_world` (shipped in #4455); these tests pin the placement of the cut tool
|
||||
//! the preview draws.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::Session;
|
||||
use solidcraft_engine::doc::{FeatureKind, PlaneRef};
|
||||
use solidcraft_engine::geom::Vec3;
|
||||
|
||||
fn run(s: &mut Session, command: &str, params: Value) -> Value {
|
||||
s.execute(command, ¶ms).unwrap_or_else(|e| panic!("{command} {params}: {e}"))
|
||||
}
|
||||
|
||||
/// The ear component: a 40 × 20 × 10 block `EarBlock` (local 0..40 x, 0..20 y, 0..10 z),
|
||||
/// optionally with its body mirrored in the component's own YZ plane (the mirrored body of the
|
||||
/// STEP copy — a document keeps every occurrence placement rigid, so an import's mirror lives
|
||||
/// in the geometry), then its occurrence moved by [10, 20, 0] and left uncaptured: the
|
||||
/// occurrence transform is what the viewport draws.
|
||||
fn ear(mirror: bool) -> (Session, u64, u64) {
|
||||
let mut s = Session::default();
|
||||
let created = run(&mut s, "component.create", json!({"name": "Ear", "grounded": false}));
|
||||
let (component, occurrence) = (created["component"].as_u64().unwrap(), created["occurrence"].as_u64().unwrap());
|
||||
run(&mut s, "solid.box", json!({"length": 40, "width": 20, "height": 10, "body_name": "EarBlock"}));
|
||||
if mirror {
|
||||
run(&mut s, "solid.mirror", json!({"bodies": ["EarBlock"], "plane": "YZ"}));
|
||||
}
|
||||
run(&mut s, "component.activate", json!({"component": 0}));
|
||||
run(&mut s, "occurrence.move", json!({"occurrence": occurrence, "translate": [10, 20, 0]}));
|
||||
(s, component, occurrence)
|
||||
}
|
||||
|
||||
/// A face-attached sketch on the placed block's top face at `face_point` (world), with two
|
||||
/// circular cut profiles 8 mm either side of the picked point along the face plane's x axis,
|
||||
/// and the Cut command the dialog would apply: One Side, 5 mm into the material.
|
||||
/// Returns the command and the sketch.
|
||||
fn two_circle_cut_on_face(s: &mut Session, component: u64, occurrence: u64, face_point: [f64; 3]) -> (Vec<(String, Value)>, u64) {
|
||||
run(s, "component.activate", json!({"component": component, "occurrence": occurrence}));
|
||||
let sketch = run(s, "sketch.create", json!({"plane": {"face": face_point}, "project_edges": false}))["sketch"].as_u64().unwrap();
|
||||
// The face plane's origin is the component's own, not the pick: place the circles around
|
||||
// the picked point in the plane's coordinates so both profiles sit on the face.
|
||||
let FeatureKind::Sketch { plane: PlaneRef::Face { plane, at, .. }, .. } = &s.doc.feature(sketch).unwrap().kind else {
|
||||
panic!("a sketch on a face")
|
||||
};
|
||||
let centre = plane.to_local(*at);
|
||||
for d in [-8.0, 8.0] {
|
||||
run(s, "sketch.circle.center", json!({"center": [centre.x + d, centre.y], "radius": 3}));
|
||||
}
|
||||
run(s, "sketch.finish", json!({}));
|
||||
let commands =
|
||||
vec![("solid.extrude".to_string(), json!({"sketch": sketch, "profiles": "all", "operation": "cut", "distance": 5, "direction": "negative"}))];
|
||||
(commands, sketch)
|
||||
}
|
||||
|
||||
/// The vertices the cut preview draws as the see-through red tool (`GpuScene::xray`: 28-byte
|
||||
/// records of position, normal and palette index), as their world bounds.
|
||||
fn tool_bounds(xray: &[u8]) -> (Vec3, Vec3) {
|
||||
assert!(!xray.is_empty() && xray.len().is_multiple_of(28), "the cut preview must draw its tool");
|
||||
let (mut lo, mut hi) = ([f64::MAX; 3], [f64::MIN; 3]);
|
||||
for record in xray.as_chunks::<28>().0 {
|
||||
for k in 0..3 {
|
||||
let v = f32::from_le_bytes(record[k * 4..k * 4 + 4].try_into().unwrap()) as f64;
|
||||
lo[k] = lo[k].min(v);
|
||||
hi[k] = hi[k].max(v);
|
||||
}
|
||||
}
|
||||
(Vec3::new(lo[0], lo[1], lo[2]), Vec3::new(hi[0], hi[1], hi[2]))
|
||||
}
|
||||
|
||||
/// The tool must sit at `want` (world): its bounds centre within a tenth of a millimetre, its
|
||||
/// spans within half a millimetre (an inscribed tessellation), and far from `buggy` — where a
|
||||
/// preview built from the component-local state would draw it (#2103).
|
||||
fn assert_tool_at(built: &crate::preview::Built, want: [f64; 3], buggy: [f64; 3]) {
|
||||
assert_eq!(built.error, None, "the cut preview must not fail");
|
||||
let (lo, hi) = tool_bounds(&built.scene.xray);
|
||||
let mid = [(lo.x + hi.x) / 2.0, (lo.y + hi.y) / 2.0, (lo.z + hi.z) / 2.0];
|
||||
let span = [hi.x - lo.x, hi.y - lo.y, hi.z - lo.z];
|
||||
for k in 0..3 {
|
||||
assert!((mid[k] - want[k]).abs() < 0.1, "tool centre {mid:?}, want {want:?} (the local-frame bug drew it centred near {buggy:?})");
|
||||
}
|
||||
let off = ((mid[0] - buggy[0]).powi(2) + (mid[1] - buggy[1]).powi(2) + (mid[2] - buggy[2]).powi(2)).sqrt();
|
||||
assert!(off > 15.0, "the tool sits at the component-local place the bug drew it ({buggy:?}, off by {off})");
|
||||
let want_span = [22.0, 6.0, 5.0];
|
||||
for k in 0..3 {
|
||||
assert!((span[k] - want_span[k]).abs() < 0.5, "tool span {span:?}, want {want_span:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// #2103: a copy of a component translated by [10, 20, 0], a sketch on its top face with two
|
||||
/// circular cut profiles: the red Cut preview sits on the sketch where the viewport shows it
|
||||
/// (centred at the placed face, spanning both circles, 5 mm down into the material), not at the
|
||||
/// component's own frame offset by the placement.
|
||||
#[test]
|
||||
fn extrude_cut_preview_on_a_face_sketch_in_a_translated_component_sits_at_the_sketch() {
|
||||
let (mut s, component, occurrence) = ear(false);
|
||||
let (commands, sketch) = two_circle_cut_on_face(&mut s, component, occurrence, [30.0, 30.0, 10.0]);
|
||||
let colors = crate::preview::Colors::from_tokens(&crate::theme::Tokens::dark());
|
||||
let built = crate::preview::compute(&s, &commands, colors).unwrap();
|
||||
// Placed: the local top-face centre (20, 10, 10) plus [10, 20, 0]; the tool cuts 5 mm down.
|
||||
assert_tool_at(&built, [30.0, 30.0, 7.5], [20.0, 10.0, 7.5]);
|
||||
// The sketch is anchored to the face of the placed body, as in the issue.
|
||||
let FeatureKind::Sketch { plane: PlaneRef::Face { .. }, .. } = s.doc.feature(sketch).unwrap().kind else { panic!("a sketch on a face") };
|
||||
}
|
||||
|
||||
/// #2103: the shape of the STEP repro — a component whose body was mirrored, placed as a copy
|
||||
/// (a document keeps occurrence placements rigid, so an import's mirror sits in the geometry).
|
||||
/// The Cut preview must still sit on the sketch at its world position, not at the component's
|
||||
/// own frame, and not be thrown off by the mirrored body beside it.
|
||||
#[test]
|
||||
fn extrude_cut_preview_on_a_face_sketch_in_a_mirrored_component_sits_at_the_sketch() {
|
||||
let (mut s, component, occurrence) = ear(true);
|
||||
let (commands, sketch) = two_circle_cut_on_face(&mut s, component, occurrence, [30.0, 30.0, 10.0]);
|
||||
let colors = crate::preview::Colors::from_tokens(&crate::theme::Tokens::dark());
|
||||
let built = crate::preview::compute(&s, &commands, colors).unwrap();
|
||||
assert_tool_at(&built, [30.0, 30.0, 7.5], [20.0, 10.0, 7.5]);
|
||||
let FeatureKind::Sketch { plane: PlaneRef::Face { .. }, .. } = s.doc.feature(sketch).unwrap().kind else { panic!("a sketch on a face") };
|
||||
}
|
||||
@@ -269,3 +269,25 @@ fn the_normal_switch_needs_a_length_and_draws_a_normal_line() {
|
||||
let curve = sk.curves.last().unwrap();
|
||||
assert!(curve.construction && curve.infinite, "{curve:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn perpendicular_click_on_reference_endpoint_places_line() {
|
||||
// #10600: after picking the reference line, a click exactly on that line's own endpoint
|
||||
// must reach the tool and place the perpendicular through the endpoint. The Length box
|
||||
// waits beside the pointer there, and its click must not swallow the endpoint's.
|
||||
let mut h = sketch();
|
||||
let sid = h.app.session.active_sketch.unwrap();
|
||||
let before = h.app.session.doc.sketch(sid).unwrap().curves.len();
|
||||
steps(&mut h, "sketch.construction_line.perpendicular", &[[10., 0.], [20., 0.]]);
|
||||
let sk = h.app.session.doc.sketch(sid).unwrap();
|
||||
assert_eq!(sk.curves.len(), before + 1, "endpoint click swallowed: tool={:?} status={:?}", h.app.tool, h.app.status);
|
||||
let curve = sk.curves.last().unwrap();
|
||||
assert!(curve.infinite && curve.construction, "{curve:?}");
|
||||
let CurveKind::Line { a, b } = curve.kind else { panic!("a line") };
|
||||
let (p, q) = (sk.point(a).unwrap(), sk.point(b).unwrap());
|
||||
let d = q - p;
|
||||
let end = sk.resolve_point("a.end").and_then(|i| sk.point(i)).unwrap();
|
||||
// Through a.end (20, 0) and perpendicular to a, which runs along the sketch x axis.
|
||||
assert!((end - p).x.abs() < 1e-6, "not through a.end: {p:?} {q:?}");
|
||||
assert!(d.x.abs() < 1e-6, "not perpendicular to a: {d:?}");
|
||||
}
|
||||
|
||||
@@ -718,6 +718,15 @@ fn polygon_input_positions(bounds: egui::Rect, sizes: &[egui::Vec2], view: Optio
|
||||
/// About the size of a value box, for keeping it in view.
|
||||
pub(crate) const BOX: egui::Vec2 = egui::vec2(76.0, 24.0);
|
||||
|
||||
/// Whether `p` is over the active tool's own floating boxes: a value box or the construction
|
||||
/// line's construction/normal switch. #10600: these must not swallow a click that lands on a
|
||||
/// sketch point, so the viewport asks before letting one take the click.
|
||||
pub(crate) fn over_tool_boxes(app: &SolveApp, ctx: &egui::Context, p: egui::Pos2) -> bool {
|
||||
let Some(t) = app.tool.as_ref() else { return false };
|
||||
(0..t.dims.len()).any(|i| ctx.memory(|m| m.area_rect(egui::Id::new(("sc_dim_area", i)))).is_some_and(|r| r.contains(p)))
|
||||
|| ctx.memory(|m| m.area_rect(egui::Id::new("sc_construction_switch"))).is_some_and(|r| r.contains(p))
|
||||
}
|
||||
|
||||
/// Where a value box whose top left would be `pos` goes so it stays in the drawing area `view`:
|
||||
/// as it is when it fits; else mirrored through the pointer (a box set off the cursor, past the
|
||||
/// view's edge, goes to the cursor's other side, still clear of it, #2201); else pushed inside.
|
||||
|
||||
@@ -4655,6 +4655,25 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
&& app.viewport.draw_drag.is_none();
|
||||
let resting = hover.filter(|p| free_pointer && rect.contains(*p) && !axes.contains(*p) && !crate::shell::covered(app, *p));
|
||||
crate::sketch_line_arc::follow_pointer(app, ui.ctx(), &proj, resting);
|
||||
// #10600: a running tool's own floating value box (or its construction/normal switch) must
|
||||
// not swallow a click that lands on a sketch point: the click places the point there, and
|
||||
// the box keeps the keyboard for typed values. A click on the box away from any sketch point
|
||||
// still edits the box. The box is anchored to the tool's hover, which freezes while the
|
||||
// pointer rests on the box, so a point the pointer reaches from the box's side can sit under
|
||||
// it; the snap identifies the intended point even there.
|
||||
if !inside
|
||||
&& app.tool.is_some()
|
||||
&& !held
|
||||
&& primary_nav.is_none()
|
||||
&& mode.is_none()
|
||||
&& ui.input(|i| i.pointer.button_released(egui::PointerButton::Primary))
|
||||
&& let Some(p) = hover.filter(|p| rect.contains(*p))
|
||||
&& crate::sketch_dims::over_tool_boxes(app, ui.ctx(), p)
|
||||
&& sketch_point_at(app, &proj, p).is_some_and(|(_, id)| id.is_some())
|
||||
{
|
||||
crate::tools::on_hover(app, &proj, p, mods.shift);
|
||||
crate::tools::on_click(app, &proj, p, mods.shift);
|
||||
}
|
||||
if let Some(p) = hover.filter(|_| inside) {
|
||||
if app.tool.is_some() {
|
||||
crate::tools::on_hover(app, &proj, p, mods.shift);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
[
|
||||
{"note": "Creo navigation: choose the preset in Preferences, then orbit, pan and zoom using the middle button. Other presets keep their drag zoom direction."},
|
||||
{"call": "engine.execute", "params": {"command": "solid.box", "params": {"length": 20, "width": 10, "height": 5}}},
|
||||
{"call": "ui.prefs", "params": {"open": "Navigation"}},
|
||||
{"click": {"handle": "dialog:Pan, zoom, orbit"}},
|
||||
{"widget": "Creo"},
|
||||
{"expect": {"prefs": {"nav": "creo"}}},
|
||||
{"call": "ui.prefs", "params": {"button": "ok"}},
|
||||
{"call": "ui.prefs", "params": {"orbit_around": "view_centre", "zoom_to_cursor": false, "free_orbit": false}},
|
||||
{"camera": {"yaw": 0, "pitch": 0}},
|
||||
{"call": "ui.drag", "params": {"x0": 800, "y0": 500, "x1": 848, "y1": 500, "steps": 8, "button": "middle"}},
|
||||
{"expect": {"camera": {"yaw_deg": 22, "pitch_deg": 0, "panned": false, "zoomed": "no"}, "selection": 0}},
|
||||
{"camera": {"yaw": 0, "pitch": 0}},
|
||||
{"call": "ui.drag", "params": {"x0": 800, "y0": 500, "x1": 860, "y1": 540, "steps": 8, "shift": true, "button": "middle"}},
|
||||
{"expect": {"camera": {"yaw_deg": 0, "pitch_deg": 0, "panned": true, "zoomed": "no"}}},
|
||||
{"camera": {"yaw": 0, "pitch": 0}},
|
||||
{"call": "ui.drag", "params": {"x0": 800, "y0": 500, "x1": 800, "y1": 560, "steps": 8, "ctrl": true, "button": "middle"}},
|
||||
{"expect": {"camera": {"yaw_deg": 0, "pitch_deg": 0, "panned": false, "zoomed": "in"}}},
|
||||
{"camera": {"yaw": 0, "pitch": 0}},
|
||||
{"call": "ui.drag", "params": {"x0": 800, "y0": 500, "x1": 800, "y1": 440, "steps": 8, "ctrl": true, "button": "middle"}},
|
||||
{"expect": {"camera": {"yaw_deg": 0, "pitch_deg": 0, "panned": false, "zoomed": "out"}}},
|
||||
{"camera": {"yaw": 0, "pitch": 0}},
|
||||
{"call": "ui.drag", "params": {"x0": 800, "y0": 500, "x1": 860, "y1": 540, "steps": 8, "button": "right"}},
|
||||
{"expect": {"camera": {"yaw_deg": 0, "pitch_deg": 0, "panned": false, "zoomed": "no"}, "selection": 0}},
|
||||
{"call": "ui.prefs", "params": {"nav": "blender"}},
|
||||
{"camera": {"yaw": 0, "pitch": 0}},
|
||||
{"call": "ui.drag", "params": {"x0": 800, "y0": 500, "x1": 800, "y1": 560, "steps": 8, "ctrl": true, "button": "middle"}},
|
||||
{"expect": {"camera": {"yaw_deg": 0, "pitch_deg": 0, "panned": false, "zoomed": "out"}}}
|
||||
]
|
||||
@@ -466,7 +466,7 @@ One setting per line, sorted by name. A change that adds a setting adds its own
|
||||
- `max_step_file_mb` (default 512): the largest STEP or import file opened, in MB, from the default up to 16384 (16 GB) (Preferences › General › Largest file to open, #4086; a `--max-file-size` launch flag overrides both for that run; the web app keeps the defaults and refuses other values; [large files](book/src/guide/files.md#large-files))
|
||||
- `middle_double_click` (`home` (default), `fit`, `off`)
|
||||
- `msaa`
|
||||
- `nav` (`fusion`, `solidworks`, `inventor`, `alias`, `blender`, `tinkercad`, `openscad`, `touchpad`)
|
||||
- `nav` (`fusion`, `solidworks`, `creo`, `inventor`, `alias`, `blender`, `tinkercad`, `openscad`, `touchpad`)
|
||||
- `nav_bridge_enabled` (default false) and `nav_bridge_port` (default 8765: a device's own app on this computer moves the view, [the navigation bridge](3d-mouse.md#device-apps-the-navigation-bridge))
|
||||
- `navigator_fit` (default false: a view chosen on the view cube or the axis gizmo also looks at the middle of the model from the one distance that fits its bounding sphere in the free part of the view, at `home_fit_size`; Home and an opened design use the same zoom and centre while it is on, so views chosen after them only turn; #10091)
|
||||
- `offer_stand_up`
|
||||
|
||||
Reference in New Issue
Block a user