mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
Fillet dialog: Setback shows its default as a length and flags one too small before OK
The Setback field reached the command and the kernel used it: on a box corner with r 2 rounds a 3 mm setback built, while 1.5 and 2 mm failed inside SMLib (status 1001), shown as "could not be built" with nothing saying why. A setback must reach past where the rounds meet without one (the radius on square faces). The kernel now refuses such a setback before SMLib runs, naming the least (the pre-check of aa2b09bc2 on fillet-kernel-cases), and exposes that least (fillet_least_setback). The field shows the default, 1.5 x the radius, as an editable length that follows the radius until the user types one, and outlines a value at or below the least in the warning colour with "More than 2 mm" under it (ui_kit::checked_row, warning_caption). Its hover text says what a setback does, the default and the least. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5Lzv2n8fyKqD61NCHSF
This commit is contained in:
@@ -3,42 +3,42 @@
|
||||
"ui.continuity_smooth_curvature_change": "Smooth curvature change",
|
||||
"ui.continuity_smooth_curvature_change_tip": "Smooth curvature change (G4): even the way the curvature changes runs on smoothly across the seam.",
|
||||
"ui.fillet_corner_tangent_tip": "Tangent (G1): a rolling-ball corner, a piece of the rolling ball's sphere tangent to each round.",
|
||||
"ui.fillet_corner_curvature_tip": "Curvature (G2): a smooth patch. The corner is set back along each edge by the Setback (empty: {factor} × the radius) and filled with a patch, so reflections run through it without a kink."
|
||||
"ui.fillet_corner_curvature_tip": "Curvature (G2): a smooth patch. The corner is set back along each edge by the Setback ({factor} × the radius unless you type one) and filled with a patch, so reflections run through it without a kink."
|
||||
},
|
||||
"ar": {
|
||||
"ui.continuity_smooth_curvature_change": "تغيّر انحناء سلس",
|
||||
"ui.continuity_smooth_curvature_change_tip": "تغيّر انحناء سلس (G4): حتى طريقة تغيّر الانحناء تستمر بسلاسة عبر خط الالتقاء.",
|
||||
"ui.fillet_corner_tangent_tip": "مماس (G1): زاوية بالكرة المتدحرجة، جزء من سطح الكرة المتدحرجة مماسّ لكل تقويس.",
|
||||
"ui.fillet_corner_curvature_tip": "الانحناء (G2): رقعة ناعمة. تُرجَع الزاوية على طول كل حافة بمقدار الإرجاع (فارغ: {factor} ضعف نصف القطر) وتُملأ برقعة، فتمر الانعكاسات عبرها دون انكسار."
|
||||
"ui.fillet_corner_curvature_tip": "الانحناء (G2): رقعة ناعمة. تُرجَع الزاوية على طول كل حافة بمقدار الإرجاع ({factor} ضعف نصف القطر ما لم تكتب قيمة) وتُملأ برقعة، فتمر الانعكاسات عبرها دون انكسار."
|
||||
},
|
||||
"de": {
|
||||
"ui.continuity_smooth_curvature_change": "Glatte Krümmungsänderung",
|
||||
"ui.continuity_smooth_curvature_change_tip": "Glatte Krümmungsänderung (G4): Selbst die Art, wie sich die Krümmung ändert, läuft an der Naht glatt weiter.",
|
||||
"ui.fillet_corner_tangent_tip": "Tangential (G1): eine Kugelecke, ein Stück der Kugel des rollenden Balls, tangential zu jeder Rundung.",
|
||||
"ui.fillet_corner_curvature_tip": "Krümmung (G2): ein glattes Flächenstück. Die Ecke wird entlang jeder Kante um den Rücksprung zurückgesetzt (leer: {factor} × der Radius) und mit einem Flächenstück gefüllt, sodass Spiegelungen ohne Knick hindurchlaufen."
|
||||
"ui.fillet_corner_curvature_tip": "Krümmung (G2): ein glattes Flächenstück. Die Ecke wird entlang jeder Kante um den Rücksprung zurückgesetzt ({factor} × der Radius, sofern Sie keinen eingeben) und mit einem Flächenstück gefüllt, sodass Spiegelungen ohne Knick hindurchlaufen."
|
||||
},
|
||||
"he": {
|
||||
"ui.continuity_smooth_curvature_change": "שינוי עקמומיות חלק",
|
||||
"ui.continuity_smooth_curvature_change_tip": "שינוי עקמומיות חלק (G4): גם האופן שבו העקמומיות משתנה ממשיך בצורה חלקה לאורך התפר.",
|
||||
"ui.fillet_corner_tangent_tip": "משיק (G1): פינת כדור מתגלגל, חלק מספירת הכדור המתגלגל, משיק לכל עיגול.",
|
||||
"ui.fillet_corner_curvature_tip": "עקמומיות (G2): טלאי חלק. הפינה נסוגה לאורך כל קצה במרחק הנסיגה (ריק: פי {factor} מהרדיוס) ומתמלאת בטלאי, כך שההשתקפויות עוברות בה בלי שבר."
|
||||
"ui.fillet_corner_curvature_tip": "עקמומיות (G2): טלאי חלק. הפינה נסוגה לאורך כל קצה במרחק הנסיגה (פי {factor} מהרדיוס אלא אם תקלידו ערך) ומתמלאת בטלאי, כך שההשתקפויות עוברות בה בלי שבר."
|
||||
},
|
||||
"ja": {
|
||||
"ui.continuity_smooth_curvature_change": "なめらかな曲率変化",
|
||||
"ui.continuity_smooth_curvature_change_tip": "なめらかな曲率変化 (G4): 境目で曲率の変わり方そのものもなめらかに続きます。",
|
||||
"ui.fillet_corner_tangent_tip": "接線 (G1): 転がるボールのコーナー。転がるボールの球面の一部で、各フィレットに接します。",
|
||||
"ui.fillet_corner_curvature_tip": "曲率 (G2): なめらかなパッチ。コーナーを各エッジに沿ってセットバック分 (空欄のときは半径の {factor} 倍) 後退させてパッチで埋め、映り込みが折れずに通ります。"
|
||||
"ui.fillet_corner_curvature_tip": "曲率 (G2): なめらかなパッチ。コーナーを各エッジに沿ってセットバック分 (入力しなければ半径の {factor} 倍) 後退させてパッチで埋め、映り込みが折れずに通ります。"
|
||||
},
|
||||
"ko": {
|
||||
"ui.continuity_smooth_curvature_change": "부드러운 곡률 변화",
|
||||
"ui.continuity_smooth_curvature_change_tip": "부드러운 곡률 변화(G4): 이음매에서 곡률이 변하는 방식까지 부드럽게 이어집니다.",
|
||||
"ui.fillet_corner_tangent_tip": "접선(G1): 굴러가는 볼 코너. 굴러가는 볼의 구면 일부로, 각 라운드에 접합니다.",
|
||||
"ui.fillet_corner_curvature_tip": "곡률(G2): 부드러운 패치. 코너를 각 모서리를 따라 셋백만큼(비워 두면 반지름의 {factor}배) 물려 패치로 채우므로, 반사가 꺾임 없이 지나갑니다."
|
||||
"ui.fillet_corner_curvature_tip": "곡률(G2): 부드러운 패치. 코너를 각 모서리를 따라 셋백만큼(입력하지 않으면 반지름의 {factor}배) 물려 패치로 채우므로, 반사가 꺾임 없이 지나갑니다."
|
||||
},
|
||||
"zh": {
|
||||
"ui.continuity_smooth_curvature_change": "平滑曲率变化",
|
||||
"ui.continuity_smooth_curvature_change_tip": "平滑曲率变化 (G4):连曲率的变化方式在接缝处也平滑延续。",
|
||||
"ui.fillet_corner_tangent_tip": "相切 (G1):滚球角,滚球球面的一部分,与每个圆角相切。",
|
||||
"ui.fillet_corner_curvature_tip": "曲率 (G2):平滑曲面片。角点沿每条边后退一个后退距离(留空:半径的 {factor} 倍),并用曲面片填充,反射穿过时没有折痕。"
|
||||
"ui.fillet_corner_curvature_tip": "曲率 (G2):平滑曲面片。角点沿每条边后退一个后退距离(未输入时为半径的 {factor} 倍),并用曲面片填充,反射穿过时没有折痕。"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"ui.fillet_fit_needs_constant": "Radius fit needs Constant radius.",
|
||||
"ui.fillet_setback": "Setback",
|
||||
"ui.fillet_setback_hint": "{factor} × radius",
|
||||
"ui.fillet_setback_tip": "How far along each edge from the corner the patch starts. Empty: {factor} × the radius.",
|
||||
"ui.fillet_setback_tip": "Where the smooth corner patch starts: how far along each edge from the corner. The default is {factor} × the radius. It must be more than the least setback, where the rounds meet without one: the radius where the faces are square to each other.",
|
||||
"ui.fillet_smooth_radius": "Smooth radius change",
|
||||
"ui.fillet_type_curvature": "Curvature continuous",
|
||||
"ui.fillet_type_tip": "The section across the edge. Curvature continuous: a curvature-continuous (G2) section between planar faces, so reflections run from the faces into the round without a kink."
|
||||
@@ -22,7 +22,7 @@
|
||||
"ui.fillet_fit_needs_constant": "ملاءمة نصف القطر تتطلب نصف قطر ثابتًا.",
|
||||
"ui.fillet_setback": "مسافة الإرجاع",
|
||||
"ui.fillet_setback_hint": "{factor} × نصف القطر",
|
||||
"ui.fillet_setback_tip": "المسافة على طول كل حافة من الزاوية التي تبدأ عندها الرقعة. فارغ: {factor} ضعف نصف القطر.",
|
||||
"ui.fillet_setback_tip": "حيث تبدأ رقعة الزاوية الناعمة: المسافة على طول كل حافة من الزاوية. الافتراضي {factor} ضعف نصف القطر. يجب أن تكون أكبر من أقل مسافة إرجاع، حيث تلتقي الاستدارات بدونها: نصف القطر حيث تكون الأوجه متعامدة.",
|
||||
"ui.fillet_smooth_radius": "تغيّر ناعم لنصف القطر",
|
||||
"ui.fillet_type_curvature": "متصل الانحناء",
|
||||
"ui.fillet_type_tip": "المقطع عبر الحافة. متصل الانحناء: مقطع متصل الانحناء (G2) بين وجوه مستوية، فتمر الانعكاسات من الوجوه إلى التقويس دون انكسار."
|
||||
@@ -36,7 +36,7 @@
|
||||
"ui.fillet_fit_needs_constant": "Radiusanpassung braucht einen konstanten Radius.",
|
||||
"ui.fillet_setback": "Rücksprung",
|
||||
"ui.fillet_setback_hint": "{factor} × Radius",
|
||||
"ui.fillet_setback_tip": "Wie weit von der Ecke entlang jeder Kante das Flächenstück beginnt. Leer: {factor} × der Radius.",
|
||||
"ui.fillet_setback_tip": "Wo das glatte Eckflächenstück beginnt: wie weit von der Ecke entlang jeder Kante. Standard ist {factor} × der Radius. Er muss größer sein als der kleinste Rücksprung, wo sich die Rundungen ohne ihn treffen: der Radius, wo die Flächen rechtwinklig zueinander stehen.",
|
||||
"ui.fillet_smooth_radius": "Radius glatt ändern",
|
||||
"ui.fillet_type_curvature": "Krümmungsstetig",
|
||||
"ui.fillet_type_tip": "Der Querschnitt über die Kante. Krümmungsstetig: ein krümmungsstetiger Querschnitt (G2) zwischen ebenen Flächen, sodass Spiegelungen ohne Knick von den Flächen in die Rundung laufen."
|
||||
@@ -50,7 +50,7 @@
|
||||
"ui.fillet_fit_needs_constant": "התאמת רדיוס דורשת רדיוס קבוע.",
|
||||
"ui.fillet_setback": "נסיגה",
|
||||
"ui.fillet_setback_hint": "{factor} × רדיוס",
|
||||
"ui.fillet_setback_tip": "המרחק לאורך כל קצה מהפינה שבו הטלאי מתחיל. ריק: פי {factor} מהרדיוס.",
|
||||
"ui.fillet_setback_tip": "היכן מתחיל טלאי הפינה החלק: המרחק לאורך כל קצה מהפינה. ברירת המחדל היא פי {factor} מהרדיוס. עליה להיות גדולה מהנסיגה המזערית, שבה העיגולים נפגשים בלעדיה: הרדיוס כשהפאות ניצבות זו לזו.",
|
||||
"ui.fillet_smooth_radius": "שינוי רדיוס חלק",
|
||||
"ui.fillet_type_curvature": "רציף בעקמומיות",
|
||||
"ui.fillet_type_tip": "החתך לרוחב הקצה. רציף בעקמומיות: חתך רציף בעקמומיות (G2) בין פאות מישוריות, כך שההשתקפויות עוברות מהפאות אל העיגול בלי שבר."
|
||||
@@ -64,7 +64,7 @@
|
||||
"ui.fillet_fit_needs_constant": "半径フィットには一定の半径が必要です。",
|
||||
"ui.fillet_setback": "セットバック",
|
||||
"ui.fillet_setback_hint": "{factor} × 半径",
|
||||
"ui.fillet_setback_tip": "パッチが始まる、コーナーから各エッジに沿った距離。空欄のときは半径の {factor} 倍。",
|
||||
"ui.fillet_setback_tip": "なめらかなコーナーパッチが始まる位置: コーナーから各エッジに沿った距離。既定値は半径の {factor} 倍。最小セットバック (セットバックなしでラウンド同士が交わる位置。面どうしが直角なら半径) より大きくする必要があります。",
|
||||
"ui.fillet_smooth_radius": "半径をなめらかに変化",
|
||||
"ui.fillet_type_curvature": "曲率連続",
|
||||
"ui.fillet_type_tip": "エッジを横切る断面。曲率連続: 平面の間の曲率連続 (G2) 断面で、映り込みが面からフィレットへ折れずにつながります。"
|
||||
@@ -78,7 +78,7 @@
|
||||
"ui.fillet_fit_needs_constant": "반지름 맞춤에는 일정한 반지름이 필요합니다.",
|
||||
"ui.fillet_setback": "셋백",
|
||||
"ui.fillet_setback_hint": "{factor} × 반지름",
|
||||
"ui.fillet_setback_tip": "패치가 시작되는, 코너에서 각 모서리를 따른 거리. 비워 두면 반지름의 {factor}배.",
|
||||
"ui.fillet_setback_tip": "부드러운 코너 패치가 시작되는 위치: 코너에서 각 모서리를 따른 거리. 기본값은 반지름의 {factor}배. 최소 셋백(셋백 없이 라운드가 만나는 곳, 면이 서로 직각이면 반지름)보다 커야 합니다.",
|
||||
"ui.fillet_smooth_radius": "반지름 부드럽게 변화",
|
||||
"ui.fillet_type_curvature": "곡률 연속",
|
||||
"ui.fillet_type_tip": "모서리를 가로지르는 단면. 곡률 연속: 평면 사이의 곡률 연속(G2) 단면으로, 반사가 면에서 라운드로 꺾임 없이 이어집니다."
|
||||
@@ -92,7 +92,7 @@
|
||||
"ui.fillet_fit_needs_constant": "半径拟合需要恒定半径。",
|
||||
"ui.fillet_setback": "后退距离",
|
||||
"ui.fillet_setback_hint": "{factor} × 半径",
|
||||
"ui.fillet_setback_tip": "曲面片从角点沿每条边开始的距离。留空:半径的 {factor} 倍。",
|
||||
"ui.fillet_setback_tip": "平滑角点曲面片开始的位置:从角点沿每条边的距离。默认值为半径的 {factor} 倍。必须大于最小后退距离(不后退时圆角相交之处;面相互垂直时即半径)。",
|
||||
"ui.fillet_smooth_radius": "半径平滑变化",
|
||||
"ui.fillet_type_curvature": "曲率连续",
|
||||
"ui.fillet_type_tip": "横跨边的截面。曲率连续:平面之间的曲率连续 (G2) 截面,反射从面平滑过渡到圆角,没有折痕。"
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"en": {
|
||||
"ui.fillet_setback_least": "More than {least}"
|
||||
},
|
||||
"ar": {
|
||||
"ui.fillet_setback_least": "أكثر من {least}"
|
||||
},
|
||||
"de": {
|
||||
"ui.fillet_setback_least": "Mehr als {least}"
|
||||
},
|
||||
"he": {
|
||||
"ui.fillet_setback_least": "יותר מ-{least}"
|
||||
},
|
||||
"ja": {
|
||||
"ui.fillet_setback_least": "{least} より大きく"
|
||||
},
|
||||
"ko": {
|
||||
"ui.fillet_setback_least": "{least}보다 크게"
|
||||
},
|
||||
"zh": {
|
||||
"ui.fillet_setback_least": "须大于 {least}"
|
||||
}
|
||||
}
|
||||
@@ -237,14 +237,8 @@ fn setbacks_reach(body: &Body, edges: &[Vec3], radius: f64, setbacks: &[(Vec3, f
|
||||
let faces = Faces::of(body)?;
|
||||
let (picked, _) = crate::blend_smlib::picked_edges(body, edges)?;
|
||||
let picked: BTreeSet<usize> = picked.into_iter().collect();
|
||||
let reach = body.size() * 0.05 + 1e-3;
|
||||
for (at, distance) in setbacks {
|
||||
let nearest = (0..faces.rec.vertices.len())
|
||||
.filter_map(|v| u32::try_from(v).ok())
|
||||
.filter_map(|v| faces.point(v).map(|p| (p.dist(*at), v)))
|
||||
.filter(|(d, _)| *d <= reach)
|
||||
.min_by(|a, b| a.0.total_cmp(&b.0));
|
||||
let Some((_, v)) = nearest else { continue };
|
||||
let Some(v) = corner_vertex(body, &faces, *at) else { continue };
|
||||
let all_rounded = (0..faces.rec.edges.len())
|
||||
.filter(|e| faces.rec.edges.get(*e).is_some_and(|x| !x.uses.is_empty() && x.vertices.contains(&v)))
|
||||
.all(|e| picked.contains(&e));
|
||||
@@ -267,6 +261,27 @@ fn setbacks_reach(body: &Body, edges: &[Vec3], radius: f64, setbacks: &[(Vec3, f
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The least setback the corner at the vertex nearest `at` takes with rounds of `radius`
|
||||
/// ([`setback_minimum`]): a setback there must be more than this, or [`fillet_setback`] and
|
||||
/// [`fillet_g2_corners`] refuse it. `None` where no vertex is near `at`, or at a corner whose
|
||||
/// least SMLib decides (one with a curved face or edge).
|
||||
pub fn fillet_least_setback(body: &Body, at: Vec3, radius: f64) -> Result<Option<f64>> {
|
||||
body.require_brep("fillet")?;
|
||||
let faces = Faces::of(body)?;
|
||||
Ok(corner_vertex(body, &faces, at).and_then(|v| setback_minimum(&faces, v, radius)))
|
||||
}
|
||||
|
||||
/// The vertex a setback at `at` names: the nearest, within the reach the setbacks take.
|
||||
fn corner_vertex(body: &Body, faces: &Faces, at: Vec3) -> Option<u32> {
|
||||
let reach = body.size() * 0.05 + 1e-3;
|
||||
(0..faces.rec.vertices.len())
|
||||
.filter_map(|v| u32::try_from(v).ok())
|
||||
.filter_map(|v| faces.point(v).map(|p| (p.dist(at), v)))
|
||||
.filter(|(d, _)| *d <= reach)
|
||||
.min_by(|a, b| a.0.total_cmp(&b.0))
|
||||
.map(|(_, v)| v)
|
||||
}
|
||||
|
||||
/// The least setback a corner of rounds of `radius` at `vertex` takes: on each face at the
|
||||
/// corner, the contact lines (rails) of the two rounded edges beside it cross, and a setback
|
||||
/// must reach past that crossing along both edges (smlib_blend_setback). A rail lies
|
||||
|
||||
@@ -56,4 +56,9 @@ fn a_setback_not_past_the_radius_is_refused_naming_the_least() {
|
||||
}
|
||||
let out = corner_case(Some(5.5)).unwrap();
|
||||
assert!(out.validity().is_empty(), "{:?}", out.validity());
|
||||
// The least the refusal names, for a form to show before it runs; none at no vertex.
|
||||
let b = box_solid(Vec3::new(0.0, 0.0, 0.0), Vec3::new(60.0, 40.0, 20.0)).unwrap();
|
||||
let least = crate::fillet_least_setback(&b, Vec3::new(60.0, 40.0, 20.0), 5.0).unwrap();
|
||||
assert!(least.is_some_and(|l| (l - 5.0).abs() < 1e-9), "{least:?}");
|
||||
assert_eq!(crate::fillet_least_setback(&b, Vec3::new(30.0, 20.0, 10.0), 5.0).unwrap(), None);
|
||||
}
|
||||
|
||||
@@ -154,8 +154,8 @@ mod validity;
|
||||
pub mod watch;
|
||||
|
||||
pub use blend::{
|
||||
ChamferSide, chamfer, chamfer_sides, fillet, fillet_each, fillet_g2, fillet_g2_corners, fillet_max, fillet_max_each, fillet_setback,
|
||||
tangent_chain,
|
||||
ChamferSide, chamfer, chamfer_sides, fillet, fillet_each, fillet_g2, fillet_g2_corners, fillet_least_setback, fillet_max, fillet_max_each,
|
||||
fillet_setback, tangent_chain,
|
||||
};
|
||||
pub use blend_corner::fillet_radii;
|
||||
pub use blend_variable::{RadiusSpline, Taper, fillet_chord, fillet_tapered, fillet_variable, fillet_variable_points, fillet_variable_spline, taper};
|
||||
|
||||
@@ -188,9 +188,10 @@ crate::dialog_descriptor::dialog_descriptors! {
|
||||
distance2: String,
|
||||
angle: String,
|
||||
flip: bool,
|
||||
/// A smooth-patch corner's setback along each edge, as typed; empty for the
|
||||
/// command's own default (1.5 × the radius).
|
||||
setback: String,
|
||||
/// A smooth-patch corner's setback along each edge, as typed; `None` until the
|
||||
/// user types one: the field then shows the command's own default (1.5 × the
|
||||
/// radius) as a length that follows the radius, and the command gets none.
|
||||
setback: Option<String>,
|
||||
},
|
||||
Mirror,
|
||||
/// Rectangular pattern of bodies: along one direction, optionally a second.
|
||||
@@ -1710,6 +1711,34 @@ pub(crate) fn fillet_corners(s: &Session, items: &[Sel]) -> Vec<Vec3> {
|
||||
corners
|
||||
}
|
||||
|
||||
/// The least setback the smooth-patch `corners` of a Fillet dialog's edges take with rounds of
|
||||
/// `radius`: the largest the kernel names at any of them ([`kernel::fillet_least_setback`]; a
|
||||
/// setback must be more than this). `None` where the kernel names none (a corner whose least
|
||||
/// SMLib decides). Kept per design revision, radius and corners: the kernel reads the bodies'
|
||||
/// faces, and the form asks every frame.
|
||||
pub(crate) fn fillet_least_setback(ctx: &egui::Context, s: &Session, items: &[Sel], corners: &[Vec3], radius: f64) -> Option<f64> {
|
||||
let mut bodies: Vec<String> = fillet_edge_ids(s, items).into_iter().map(|(b, _)| b).collect();
|
||||
bodies.dedup();
|
||||
let mut key = std::collections::hash_map::DefaultHasher::new();
|
||||
(&s.document_id, s.revision, radius.to_bits(), &bodies).hash(&mut key);
|
||||
corners.iter().for_each(|c| [c.x, c.y, c.z].map(f64::to_bits).hash(&mut key));
|
||||
let key = key.finish();
|
||||
let id = egui::Id::new("fillet_least_setback");
|
||||
if let Some((k, least)) = ctx.data(|d| d.get_temp::<(u64, Option<f64>)>(id))
|
||||
&& k == key
|
||||
{
|
||||
return least;
|
||||
}
|
||||
let st = crate::open_meshing::session_world(s);
|
||||
let least = bodies
|
||||
.iter()
|
||||
.filter_map(|b| st.body(b))
|
||||
.flat_map(|b| corners.iter().filter_map(|c| solidcraft_engine::doc::kernel::fillet_least_setback(&b.body, *c, radius).ok().flatten()))
|
||||
.reduce(f64::max);
|
||||
ctx.data_mut(|d| d.insert_temp(id, (key, least)));
|
||||
least
|
||||
}
|
||||
|
||||
/// Profiles of the sketch a feature would use, when there is just one: the active sketch's, else
|
||||
/// the last sketch's while no feature builds from it yet. A used profile (a cube's sketch on its
|
||||
/// bottom face) is only taken when it is picked.
|
||||
@@ -3502,16 +3531,7 @@ pub(crate) const CHAMFER_TYPES: [&str; 3] = ["Equal Distance", "Two Distances",
|
||||
|
||||
/// A new Fillet or Chamfer dialog's values.
|
||||
fn fillet_kind(size: &str, chamfer: bool) -> Kind {
|
||||
Kind::Fillet {
|
||||
radius: size.into(),
|
||||
chamfer,
|
||||
chain: true,
|
||||
ctype: 0,
|
||||
distance2: size.into(),
|
||||
angle: "45 deg".into(),
|
||||
flip: false,
|
||||
setback: String::new(),
|
||||
}
|
||||
Kind::Fillet { radius: size.into(), chamfer, chain: true, ctype: 0, distance2: size.into(), angle: "45 deg".into(), flip: false, setback: None }
|
||||
}
|
||||
|
||||
/// A length field's first text: `number` in the design's units, with the unit written out so the
|
||||
@@ -4107,15 +4127,15 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
|
||||
}
|
||||
}
|
||||
}
|
||||
// A smooth patch's setback, typed once, goes to each of the dialog's corners; left
|
||||
// empty, the command sets them back by its default (1.5 × the radius).
|
||||
// A smooth patch's setback, typed once, goes to each of the dialog's corners; not
|
||||
// typed (or emptied), the command sets them back by its default (1.5 × the radius).
|
||||
if d.extra.get("corner").and_then(Value::as_str) == Some("g2")
|
||||
&& dense::fillet_option_applies(&d.extra, "corner")
|
||||
&& !setback.trim().is_empty()
|
||||
&& let Some(setback) = setback.as_deref().map(str::trim).filter(|s| !s.is_empty())
|
||||
{
|
||||
let corners = fillet_corners(s, sels(d, 0));
|
||||
if !corners.is_empty() {
|
||||
p["setbacks"] = Value::Array(corners.into_iter().map(|v| json!({"vertex": pt3(v), "distance": setback.trim()})).collect());
|
||||
p["setbacks"] = Value::Array(corners.into_iter().map(|v| json!({"vertex": pt3(v), "distance": setback})).collect());
|
||||
}
|
||||
}
|
||||
("solid.fillet", p)
|
||||
@@ -5438,7 +5458,7 @@ pub fn for_feature(app: &SolveApp, id: u64, marker: Option<usize>) -> Option<Dia
|
||||
let at_corners =
|
||||
corners.len() == setbacks.len() && setbacks.iter().all(|c| corners.iter().any(|v| v.dist(c.vertex.0) < VERTEX_TOLERANCE));
|
||||
match &mut d.kind {
|
||||
Kind::Fillet { setback, .. } if !corner.is_rolling() && uniform && at_corners => *setback = first.distance.to_string(),
|
||||
Kind::Fillet { setback, .. } if !corner.is_rolling() && uniform && at_corners => *setback = Some(first.distance.to_string()),
|
||||
_ => {
|
||||
let saved = setbacks.iter().map(|c| json!({"vertex": pt3(c.vertex.0), "distance": c.distance.to_string()}));
|
||||
d.extra.insert("setbacks".into(), Value::Array(saved.collect()));
|
||||
|
||||
@@ -114,12 +114,16 @@ const FILLET_TYPES: [(&str, &str); 5] = [
|
||||
const FILLET_CORNERS: [(Continuity, &str); 2] =
|
||||
[(Continuity::G1, "ui.fillet_corner_tangent_tip"), (Continuity::G2, "ui.fillet_corner_curvature_tip")];
|
||||
|
||||
/// The default setback of a smooth-patch corner, as a multiple of the radius.
|
||||
const SETBACK_FACTOR: &str = "1.5";
|
||||
/// The default setback of a smooth-patch corner, as a multiple of the radius (the kernel's), as
|
||||
/// the messages show it.
|
||||
fn setback_factor() -> String {
|
||||
solidcraft_engine::doc::kernel::DEFAULT_SETBACK_FACTOR.to_string()
|
||||
}
|
||||
|
||||
/// The Corners row's hover text: a line for each choice.
|
||||
fn fillet_corner_tip() -> String {
|
||||
FILLET_CORNERS.map(|(_, tip)| crate::i18n::format(tip, &[("factor", SETBACK_FACTOR)])).join("\n")
|
||||
let factor = setback_factor();
|
||||
FILLET_CORNERS.map(|(_, tip)| crate::i18n::format(tip, &[("factor", &factor)])).join("\n")
|
||||
}
|
||||
|
||||
/// What `solid.fillet` does with a radius too large for the faces beside an edge (`fit`): stop
|
||||
@@ -150,14 +154,14 @@ pub(super) fn fillet_option_applies(extra: &serde_json::Map<String, Value>, key:
|
||||
/// After `changed` (`fit` or `corner`) took a new value, the options it rules out back to their
|
||||
/// defaults: a fitted radius (`fit` reduce or taper) takes tangent corners, a curvature corner
|
||||
/// takes the exact radius. A type other than Constant clears both ([`choose_fillet_type`]).
|
||||
fn settle_fillet_options(extra: &mut serde_json::Map<String, Value>, changed: &str, setback: &mut String) {
|
||||
fn settle_fillet_options(extra: &mut serde_json::Map<String, Value>, changed: &str, setback: &mut Option<String>) {
|
||||
let fitted = extra.get("fit").and_then(Value::as_str).is_some_and(|fit| fit != "exact");
|
||||
let patch = extra.get("corner").and_then(Value::as_str) == Some("g2");
|
||||
match changed {
|
||||
"fit" if fitted => {
|
||||
extra.remove("corner");
|
||||
extra.remove("setbacks");
|
||||
setback.clear();
|
||||
*setback = None;
|
||||
}
|
||||
"corner" if patch => {
|
||||
extra.remove("fit");
|
||||
@@ -174,10 +178,13 @@ fn fillet_corner_rows(
|
||||
s: &solidcraft_engine::Session,
|
||||
extra: &mut serde_json::Map<String, Value>,
|
||||
inputs: &[selection::SelInput],
|
||||
setback: &mut String,
|
||||
radius: &str,
|
||||
setback: &mut Option<String>,
|
||||
) -> bool {
|
||||
let items = selection::items(inputs, 0);
|
||||
let corners = fillet_corners(s, items);
|
||||
let patch = extra.get("corner").and_then(Value::as_str) == Some("g2");
|
||||
if !patch && fillet_corners(s, selection::items(inputs, 0)).is_empty() {
|
||||
if !patch && corners.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let mut enter = false;
|
||||
@@ -192,18 +199,38 @@ fn fillet_corner_rows(
|
||||
settle_fillet_options(extra, "corner", setback);
|
||||
}
|
||||
if extra.get("corner").and_then(Value::as_str) == Some("g2") {
|
||||
ui.scope(|ui| {
|
||||
let hint = crate::i18n::format("ui.fillet_setback_hint", &[("factor", SETBACK_FACTOR)]);
|
||||
enter |= hint_row(ui, "ui.fillet_setback", setback, &hint);
|
||||
})
|
||||
.response
|
||||
.on_hover_text(crate::i18n::format("ui.fillet_setback_tip", &[("factor", SETBACK_FACTOR)]));
|
||||
let factor = setback_factor();
|
||||
let length = |text: &str| crate::length_input::eval(&s.doc, text, solidcraft_engine::doc::expr::Kind::Length).ok().filter(|v| v.is_finite());
|
||||
let r = length(radius).filter(|r| *r > 0.0);
|
||||
// Not typed, the field shows the command's default as a length, following the radius.
|
||||
let default = r.map(|r| crate::length_input::shown(&s.doc, solidcraft_engine::doc::kernel::DEFAULT_SETBACK_FACTOR * r));
|
||||
let shown = setback.clone().or(default).unwrap_or_default();
|
||||
// A setback at or below the least the kernel takes there is flagged before OK.
|
||||
let least = r.and_then(|r| crate::dialogs::fillet_least_setback(ui.ctx(), s, items, &corners, r));
|
||||
let warning = least
|
||||
.filter(|least| length(&shown).is_some_and(|v| v <= least * (1.0 + 1e-9)))
|
||||
.map(|least| crate::i18n::format("ui.fillet_setback_least", &[("least", &crate::length_input::shown(&s.doc, least))]));
|
||||
let mut text = shown.clone();
|
||||
let row = ui.scope(|ui| {
|
||||
let hint = crate::i18n::format("ui.fillet_setback_hint", &[("factor", &factor)]);
|
||||
ui_kit::checked_row(ui, "ui.fillet_setback", &mut text, &hint, warning.as_deref())
|
||||
});
|
||||
row.response.on_hover_text(crate::i18n::format("ui.fillet_setback_tip", &[("factor", &factor)]));
|
||||
let (accepted, field) = row.inner;
|
||||
enter |= accepted;
|
||||
if text != shown {
|
||||
*setback = Some(text);
|
||||
}
|
||||
// Emptied and left, the field goes back to the default.
|
||||
if setback.as_deref().is_some_and(|t| t.trim().is_empty()) && !field.has_focus() {
|
||||
*setback = None;
|
||||
}
|
||||
}
|
||||
enter
|
||||
}
|
||||
|
||||
/// A Constant radius fillet's "If too large" row (`fit`), with the radius a reduced fillet used.
|
||||
fn fillet_fit_rows(app: &SolveApp, ui: &mut egui::Ui, extra: &mut serde_json::Map<String, Value>, setback: &mut String) {
|
||||
fn fillet_fit_rows(app: &SolveApp, ui: &mut egui::Ui, extra: &mut serde_json::Map<String, Value>, setback: &mut Option<String>) {
|
||||
let t = Tokens::get();
|
||||
let before = extra.get("fit").cloned();
|
||||
let row = ui.scope(|ui| form_row(ui, "ui.fillet_fit", |ui| extra_choice(ui, extra, "fi_fit", "fit", &FILLET_FITS)));
|
||||
@@ -478,7 +505,7 @@ pub(super) fn rows(app: &mut SolveApp, ctx: &egui::Context, d: &mut Dialog, ui:
|
||||
})
|
||||
.on_hover_text(tr("ui.fillet_type_choices_tip"));
|
||||
if rtype == 0 {
|
||||
enter |= fillet_corner_rows(ui, &app.session, &mut d.extra, &d.inputs, setback);
|
||||
enter |= fillet_corner_rows(ui, &app.session, &mut d.extra, &d.inputs, radius, setback);
|
||||
fillet_fit_rows(app, ui, &mut d.extra, setback);
|
||||
}
|
||||
}
|
||||
@@ -1260,7 +1287,7 @@ mod tests {
|
||||
}
|
||||
|
||||
/// Corners shows only once three rounded edges meet at a corner; Curvature (a smooth patch) writes
|
||||
/// `corner: g2`, its Setback (empty for the command's 1.5 × radius) goes to that corner as
|
||||
/// `corner: g2`, its Setback (untouched: the command's 1.5 × radius) goes to that corner as
|
||||
/// `setbacks`, and Tangent (a rolling ball) writes neither.
|
||||
#[test]
|
||||
fn corners_row_follows_the_selection_and_maps_to_corner_and_setbacks() {
|
||||
@@ -1282,7 +1309,7 @@ mod tests {
|
||||
assert!(p.get("setbacks").is_none(), "an empty Setback leaves the command's default: {p}");
|
||||
assert!(scenario::handle_at("dialog:ui.fillet_setback").is_some());
|
||||
if let Some(Dialog { kind: Kind::Fillet { setback, .. }, .. }) = h.app.dialog.as_mut() {
|
||||
*setback = "3 mm".into();
|
||||
*setback = Some("3 mm".into());
|
||||
}
|
||||
assert_eq!(parameters(&h)["setbacks"], json!([{"vertex": [10.0, 0.0, 5.0], "distance": "3 mm"}]));
|
||||
choose(&mut h, "ui.corners", Continuity::G1.label_id());
|
||||
@@ -1297,6 +1324,73 @@ mod tests {
|
||||
assert!(scenario::handle_at("dialog:ui.corners").is_none());
|
||||
}
|
||||
|
||||
/// Setback shows the command's default as an editable length that follows the radius until
|
||||
/// one is typed (and sends none), keeps a typed one through radius changes, and flags one at
|
||||
/// or below the least setback the kernel takes there (the radius, at this square corner)
|
||||
/// before OK; a setback past it builds.
|
||||
#[test]
|
||||
fn setback_shows_its_default_keeps_a_typed_value_and_flags_one_too_small() {
|
||||
let mut h = fillet_harness();
|
||||
set_edges(&mut h, 3);
|
||||
choose(&mut h, "ui.corners", Continuity::G2.label_id());
|
||||
let radius = |h: &mut Harness, r: &str| {
|
||||
if let Some(Dialog { kind: Kind::Fillet { radius, .. }, .. }) = h.app.dialog.as_mut() {
|
||||
*radius = r.into();
|
||||
}
|
||||
h.frames(3);
|
||||
};
|
||||
let shown = |h: &Harness| {
|
||||
let tree = h.output.accesskit_update.as_ref().unwrap();
|
||||
let label = crate::i18n::tr_label("ui.fillet_setback");
|
||||
tree.nodes.iter().find(|(_, n)| n.label() == Some(label)).and_then(|(_, n)| n.value()).unwrap_or_default().to_string()
|
||||
};
|
||||
let typed = |h: &Harness| match &h.app.dialog.as_ref().unwrap().kind {
|
||||
Kind::Fillet { setback, .. } => setback.clone(),
|
||||
other => panic!("{other:?}"),
|
||||
};
|
||||
let flagged = || scenario::rect_of("dialog:caption:ui.fillet_setback").is_some();
|
||||
let type_setback = |h: &mut Harness, text: &str| {
|
||||
h.step(&json!({"click":{"handle":"dialog:ui.fillet_setback"}})).unwrap();
|
||||
h.step(&json!({"key":"A","cmd":true})).unwrap();
|
||||
h.step(&json!({"text": text})).unwrap();
|
||||
h.frames(3);
|
||||
};
|
||||
radius(&mut h, "2 mm");
|
||||
assert_eq!((shown(&h), typed(&h)), ("3 mm".to_string(), None), "the default, 1.5 × the radius, as a length");
|
||||
assert!(!flagged() && parameters(&h).get("setbacks").is_none(), "an untouched default sends none");
|
||||
radius(&mut h, "3 mm");
|
||||
assert_eq!(shown(&h), "4.5 mm", "untouched, it follows the radius");
|
||||
radius(&mut h, "2 mm");
|
||||
type_setback(&mut h, "1.5 mm");
|
||||
assert_eq!(typed(&h).as_deref(), Some("1.5 mm"));
|
||||
radius(&mut h, "2.5 mm");
|
||||
assert_eq!(shown(&h), "1.5 mm", "typed, it stays as typed");
|
||||
radius(&mut h, "2 mm");
|
||||
assert!(flagged(), "1.5 mm is not past the least, the 2 mm radius");
|
||||
let mut copy = solidcraft_engine::Session::new((*h.app.session.doc).clone());
|
||||
let refused = copy.execute("solid.fillet", ¶meters(&h)).unwrap_err().to_string();
|
||||
assert!(refused.contains("setback must be more than the radius (2 mm)"), "the kernel refuses what the field flags: {refused}");
|
||||
let least = crate::dialogs::fillet_least_setback(
|
||||
&h.ctx,
|
||||
&h.app.session,
|
||||
&h.app.dialog.as_ref().unwrap().inputs[0].items,
|
||||
&[Vec3::new(10.0, 0.0, 5.0)],
|
||||
2.0,
|
||||
);
|
||||
assert!(least.is_some_and(|l| (l - 2.0).abs() < 1e-9), "{least:?}");
|
||||
assert_eq!(crate::length_input::shown(&h.app.session.doc, least.unwrap()), "2 mm", "the message's least");
|
||||
type_setback(&mut h, "2 mm");
|
||||
assert!(flagged(), "the least itself is refused too");
|
||||
type_setback(&mut h, "3 mm");
|
||||
assert!(!flagged(), "past the least");
|
||||
let p = parameters(&h);
|
||||
assert_eq!(p["setbacks"], json!([{"vertex": [10.0, 0.0, 5.0], "distance": "3 mm"}]));
|
||||
let mut copy = solidcraft_engine::Session::new((*h.app.session.doc).clone());
|
||||
copy.execute("solid.fillet", &p).unwrap();
|
||||
let st = crate::open_meshing::session_world(©);
|
||||
assert!(st.bodies.iter().all(|b| b.body.validity().is_empty()), "a 3 mm setback on 2 mm rounds builds");
|
||||
}
|
||||
|
||||
/// No visible label of the fillet dialog names a continuity by its code (G0 … G3): the words
|
||||
/// are on the controls, the codes only in hover texts. Every section open, the corner row shown.
|
||||
#[test]
|
||||
@@ -1327,9 +1421,9 @@ mod tests {
|
||||
#[test]
|
||||
fn a_saved_smooth_patch_reopens_with_its_setback() {
|
||||
for (setbacks, field) in [
|
||||
(json!([{"vertex": [0, 0, 0], "distance": 7}]), "7"),
|
||||
(json!([{"vertex": [0, 0, 0], "distance": 7}]), Some("7")),
|
||||
// Named near the vertex, as the command allows, rather than at it.
|
||||
(json!([{"vertex": [0.1, 0.1, 0], "distance": 7}]), ""),
|
||||
(json!([{"vertex": [0.1, 0.1, 0], "distance": 7}]), None),
|
||||
] {
|
||||
let mut h = Harness::new();
|
||||
// The command's own smooth-patch corner: a 20 mm box, its three edges at the origin.
|
||||
@@ -1346,7 +1440,7 @@ mod tests {
|
||||
h.app.edit_feature(id);
|
||||
h.frames(3);
|
||||
let d = h.app.dialog.as_ref().unwrap();
|
||||
assert!(matches!(&d.kind, Kind::Fillet { setback, .. } if setback == field), "{:?}", d.kind);
|
||||
assert!(matches!(&d.kind, Kind::Fillet { setback, .. } if setback.as_deref() == field), "{:?}", d.kind);
|
||||
assert_eq!(d.extra.get("corner"), Some(&json!("g2")));
|
||||
for (command, params) in apply_commands(&h.app, d).unwrap() {
|
||||
crate::dialogs::roll_marker(&mut h.app, None).unwrap();
|
||||
|
||||
@@ -80,6 +80,17 @@ pub(crate) fn model(doc: &Document, mm: f64) -> String {
|
||||
format!("{} {unit}", mm / scale)
|
||||
}
|
||||
|
||||
/// A computed length shown in an editable field or a message, in the design's unit, to a
|
||||
/// millionth of it with trailing zeros dropped ("3 mm", not "3.000000 mm" or 2.9999999999999996):
|
||||
/// for a value the field shows but sends only once the user edits it.
|
||||
pub(crate) fn shown(doc: &Document, mm: f64) -> String {
|
||||
let unit = unit(doc);
|
||||
let scale = unit_info(unit).map(|(_, scale)| scale).unwrap_or(1.0);
|
||||
let text = format!("{:.6}", mm / scale);
|
||||
let text = text.trim_end_matches('0').trim_end_matches('.');
|
||||
format!("{} {unit}", if text == "-0" || text.is_empty() { "0" } else { text })
|
||||
}
|
||||
|
||||
/// The editable length expressions in a dialog. Counts, angles, ratios, selectors and standard
|
||||
/// part designations never go through length conversion. Keep this list shared by submission
|
||||
/// and feature refill, so a bare stored millimetre cannot become inches just by pressing OK.
|
||||
@@ -92,9 +103,9 @@ pub(crate) fn fields<'a>(doc: &Document, kind: &'a mut crate::dialogs::Kind) ->
|
||||
match kind {
|
||||
D::Extrude { distance, distance2, to_offset, start, wall, wall2, .. } => vec![distance, distance2, to_offset, start, wall, wall2],
|
||||
D::Fillet { radius, distance2, setback, .. } => {
|
||||
// An empty setback stands for the command's default and stays empty.
|
||||
// A setback not typed (or emptied) stands for the command's default and stays so.
|
||||
let mut lengths = vec![radius, distance2];
|
||||
if !setback.trim().is_empty() {
|
||||
if let Some(setback) = setback.as_mut().filter(|s| !s.trim().is_empty()) {
|
||||
lengths.push(setback);
|
||||
}
|
||||
lengths
|
||||
|
||||
@@ -27,6 +27,18 @@ pub(crate) fn hint_field(ui: &mut Ui, value: &mut String, hint: &str) -> bool {
|
||||
editor(ui, value, hint, true).0
|
||||
}
|
||||
|
||||
/// [`hint_field`] for a value the form checks before it runs: `flagged`, it is outlined in the
|
||||
/// warning colour (the caller says why in words under it, [`super::warning_caption`]). Returns
|
||||
/// accepted Enter and the editor's response. Tokens: `same as field, warning`.
|
||||
pub(crate) fn checked_field(ui: &mut Ui, value: &mut String, hint: &str, flagged: bool) -> (bool, Response) {
|
||||
let (enter, _, response) = editor(ui, value, hint, true);
|
||||
if flagged {
|
||||
let t = Tokens::get();
|
||||
ui.painter().rect_stroke(response.rect, t.control_radius, egui::Stroke::new(1.5, t.warning), egui::StrokeKind::Inside);
|
||||
}
|
||||
(enter, response)
|
||||
}
|
||||
|
||||
/// The whole numbers a count field may hold, and how its text evaluates (parameters and
|
||||
/// expressions included). `eval` returns None for text that is not a value.
|
||||
pub(crate) struct Count<'a> {
|
||||
|
||||
@@ -83,6 +83,18 @@ pub(crate) fn hint_row(ui: &mut Ui, label: &str, value: &mut String, hint: &str)
|
||||
row(ui, label, |ui| super::hint_field(ui, value, hint))
|
||||
}
|
||||
|
||||
/// Expression row with an empty-value hint whose value the form checks: with `warning`, the
|
||||
/// field is outlined in the warning colour and the message sits under it in that colour, so the
|
||||
/// colour is never the only cue. Returns accepted Enter and the field's response. Tokens: `row,
|
||||
/// checked_field, warning_caption`.
|
||||
pub(crate) fn checked_row(ui: &mut Ui, label: &str, value: &mut String, hint: &str, warning: Option<&str>) -> (bool, Response) {
|
||||
let out = row(ui, label, |ui| super::checked_field(ui, value, hint, warning.is_some()));
|
||||
if let Some(message) = warning {
|
||||
warning_caption(ui, label, message);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Plain text row; the caller retains Enter/change ownership. Tokens: `row, text_field`.
|
||||
pub(crate) fn text_row(ui: &mut Ui, label: &str, value: &mut String, hint: &str) -> (bool, bool) {
|
||||
row(ui, label, |ui| super::text_field(ui, value, hint))
|
||||
@@ -159,12 +171,22 @@ pub(super) fn sized_row<R>(ui: &mut Ui, text: &str, height: f32, control: impl F
|
||||
/// row's control. It says in words what a colour or an icon shows, so colour is never the only cue.
|
||||
/// Dim helper beneath a row control. Tokens: `form_icon_size, font_small, text_dim, space_8; label-column tokens`.
|
||||
pub(crate) fn caption(ui: &mut Ui, aligned_to: &str, text: &str) {
|
||||
caption_in(ui, aligned_to, text, Tokens::get().form_dim);
|
||||
}
|
||||
|
||||
/// [`caption`] in the warning colour, for what is wrong with the row's value (a value the
|
||||
/// operation would refuse). Tokens: `caption tokens, warning`.
|
||||
pub(crate) fn warning_caption(ui: &mut Ui, aligned_to: &str, text: &str) {
|
||||
caption_in(ui, aligned_to, text, Tokens::get().warning);
|
||||
}
|
||||
|
||||
fn caption_in(ui: &mut Ui, aligned_to: &str, text: &str, color: egui::Color32) {
|
||||
let t = Tokens::get();
|
||||
let width = dimension(ui.available_width());
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(width, t.form_icon_size), egui::Sense::hover());
|
||||
let left = (rect.left() + label_column(ui, aligned_to, width) + label_gap(ui, width)).min(rect.right());
|
||||
let text_rect = egui::Rect::from_min_max(egui::pos2(left, rect.top()), rect.max);
|
||||
paint_text(ui, text_rect, text, egui::FontId::proportional(t.font_small), t.form_dim, false);
|
||||
paint_text(ui, text_rect, text, egui::FontId::proportional(t.font_small), color, false);
|
||||
scenario::publish_rect(&format!("dialog:caption:{aligned_to}"), text_rect);
|
||||
}
|
||||
|
||||
|
||||
+6
-3
@@ -24,7 +24,10 @@ preferences, manager cards, prompts, context menus and the sketch palette. Impor
|
||||
- `expr_row`, `hint_row`, `text_row` and `choice_row` compose `row` with the existing
|
||||
expression, hinted expression, plain text and dropdown controls. They preserve row order,
|
||||
labels, focus IDs and completion; callers keep Enter/change ownership and explicit dropdown IDs.
|
||||
`count_row` composes `row` with `count_field` for pattern quantities.
|
||||
`count_row` composes `row` with `count_field` for pattern quantities. `checked_row` is
|
||||
`hint_row` for a value the form checks before it runs: with a warning, the field is outlined in
|
||||
the warning colour and the message sits under it (`warning_caption`), so colour is never the
|
||||
only cue.
|
||||
- `Panel` owns bounded windows, feature/palette composition, scrolling bodies,
|
||||
pinned actions, compute status and keyboard routing. `wide()` uses three card
|
||||
widths. `feature_frame` and `palette_frame` retain docked/floating surfaces.
|
||||
@@ -89,13 +92,13 @@ table. Panel/Window/Confirm compositions also have dedicated geometry and intera
|
||||
| confirm: `Confirm`, `Tone` | Modal warning/danger/info, title/message, mono detail and ghost/cancel/primary actions | `warning`, `form_error/fill`, `accent_soft`, `form_primary`, `on_accent`, `tab_on_fill_dark`, `font_subtitle`, spacing |
|
||||
| metrics: `row_height`, `table_row_height`, `header_height` | Shared control/header heights | `control_height`, `space_8`, `form_header_height` |
|
||||
| layout: `scope`, `row` | Form styling and aligned label/control columns | `form_label_width`, `form_gap_y`, `control_*`, `font_label`, `dialog_label`, `space_4/8` |
|
||||
| `caption` | Dim caption aligned beneath a control | `font_small`, `text_dim`, label-column tokens |
|
||||
| `caption`, `warning_caption` | Dim caption aligned beneath a control; the warning one says what is wrong with its value | `font_small`, `text_dim`, `warning`, label-column tokens |
|
||||
| `figure_rows`, `FigureSlot` | A caller-painted figure in the label column beside a stack of captioned expression fields (the Hole dialog's section); the caller gets each field's rect and hover/focus state to draw leaders and light the part it sizes. Fields keep their `dialog:<label>` handles; the block publishes `dialog:figure:<id>` and its picture `dialog:figure_picture:<id>` | `control_height`, `form_label_width`, `font_small`, `form_dim`, `space_2/4`, label gap |
|
||||
| `section`, `section_count`, `divider` | Persisted small-caps disclosure with chevron, optional count badge and trailing hairline; full-width rule | `control_height`, `dialog_rule`, `font_caption`, tracking, spacing |
|
||||
| `list_row` | Picked item with a remove target | `control_height`, `font_label`, icon/spacing tokens |
|
||||
| `text`, `Text` | Hint, note, report, inline or cell text; see variants below | `font_small/label`, `text/dim/error`, `control_height`, `space_4` |
|
||||
| `text_lines` | Virtualized read-only licence/report lines, horizontal overflow | `font_small`, `text`, `control_height` |
|
||||
| fields: `field`, `hint_field`, `focused_field` | Expression editors, optional empty hint or retained focus; returns accepted Enter | `control_height/fill`, `form_inset`, `font_label` |
|
||||
| fields: `field`, `hint_field`, `focused_field`, `checked_field` | Expression editors, optional empty hint or retained focus, or outlined in the warning colour when flagged; returns accepted Enter | `control_height/fill`, `form_inset`, `font_label`, `warning` |
|
||||
| `count_field`, `Count` | Expression editor for a whole-number count (a pattern's quantity) with up/down step arrows at its right edge. `Count` gives the limits and the evaluator (the design's parameters); an arrow steps the evaluated count by one within them, replacing a plain number or adjusting an expression's trailing `+ n`, and is dimmed when it can't step. Handles `dialog:<row> up` / `down`; returns accepted Enter | field tokens, `control_border`, `control_radius`, `selected_bg`, `form_ink/dim`, `space_4` |
|
||||
| `text_field`, `focused_text_field` | Plain name editor, optionally retaining focus | field tokens |
|
||||
| `multiline_text_field` | Three-line plain-text row with stable focus ID | field/row tokens |
|
||||
|
||||
Reference in New Issue
Block a user