diff --git a/crates/bone-app/src/relation_tools.rs b/crates/bone-app/src/relation_tools.rs index daa056a..d5ccd78 100644 --- a/crates/bone-app/src/relation_tools.rs +++ b/crates/bone-app/src/relation_tools.rs @@ -15,6 +15,7 @@ pub enum RelationKind { Equal, Concentric, Midpoint, + Symmetric, Fix, } @@ -42,6 +43,7 @@ impl RelationKind { Self::Equal, Self::Concentric, Self::Midpoint, + Self::Symmetric, Self::Fix, ]; @@ -92,6 +94,11 @@ impl RelationKind { label: strings::TOOL_MIDPOINT, check: midpoint, }, + Self::Symmetric => RelationDescriptor { + key: "rel.symmetric", + label: strings::TOOL_SYMMETRIC, + check: symmetric, + }, Self::Fix => RelationDescriptor { key: "rel.fix", label: strings::TOOL_FIX, @@ -229,6 +236,41 @@ fn fix(sketch: &Sketch, selection: &[SketchEntityId]) -> Eligibility { ) } +fn symmetric(sketch: &Sketch, selection: &[SketchEntityId]) -> Eligibility { + let disabled = || Eligibility::Disabled(strings::REL_HINT_SYMMETRIC); + if selection.len() != 3 { + return disabled(); + } + let kinds: Vec<(SketchEntityId, SketchEntityKind)> = selection + .iter() + .filter_map(|id| sketch.entities().get(*id).map(|e| (*id, e.kind()))) + .collect(); + if kinds.len() != 3 { + return disabled(); + } + let points: Vec = kinds + .iter() + .filter(|(_, k)| *k == SketchEntityKind::Point) + .map(|(id, _)| *id) + .collect(); + let lines: Vec = kinds + .iter() + .filter(|(_, k)| *k == SketchEntityKind::Line) + .map(|(id, _)| *id) + .collect(); + let [a, b] = points.as_slice() else { + return disabled(); + }; + let [axis] = lines.as_slice() else { + return disabled(); + }; + Eligibility::Eligible(SketchRelation::Symmetric { + a: *a, + b: *b, + axis: *axis, + }) +} + fn pair_eligibility( sketch: &Sketch, selection: &[SketchEntityId], @@ -307,10 +349,10 @@ mod tests { #[test] fn relation_kind_all_matches_descriptor_arms() { - assert_eq!(RelationKind::ALL.len(), 10); + assert_eq!(RelationKind::ALL.len(), 11); let keys: std::collections::BTreeSet<_> = RelationKind::ALL.iter().map(|k| k.key()).collect(); - assert_eq!(keys.len(), 10, "every kind has a unique widget key"); + assert_eq!(keys.len(), 11, "every kind has a unique widget key"); } #[test] diff --git a/crates/bone-app/src/strings.rs b/crates/bone-app/src/strings.rs index 8a77e8f..a71c86e 100644 --- a/crates/bone-app/src/strings.rs +++ b/crates/bone-app/src/strings.rs @@ -38,6 +38,7 @@ pub const TOOL_EQUAL: StringKey = StringKey::new("tool.equal"); pub const TOOL_CONCENTRIC: StringKey = StringKey::new("tool.concentric"); pub const TOOL_FIX: StringKey = StringKey::new("tool.fix"); pub const TOOL_MIDPOINT: StringKey = StringKey::new("tool.midpoint"); +pub const TOOL_SYMMETRIC: StringKey = StringKey::new("tool.symmetric"); pub const REL_HINT_ONE_LINE: StringKey = StringKey::new("rel.hint.one_line"); pub const REL_HINT_TWO_LINES: StringKey = StringKey::new("rel.hint.two_lines"); @@ -47,6 +48,8 @@ pub const REL_HINT_TANGENT: StringKey = StringKey::new("rel.hint.tangent"); pub const REL_HINT_EQUAL: StringKey = StringKey::new("rel.hint.equal"); pub const REL_HINT_MIDPOINT: StringKey = StringKey::new("rel.hint.midpoint"); pub const REL_HINT_ENTITY: StringKey = StringKey::new("rel.hint.entity"); +pub const REL_HINT_SYMMETRIC: StringKey = StringKey::new("rel.hint.symmetric"); +pub const PROPERTY_ROW_AXIS: StringKey = StringKey::new("property.row.axis"); pub const TOOL_SMART_DIMENSION: StringKey = StringKey::new("tool.smart_dimension"); pub const TOOL_RADIUS: StringKey = StringKey::new("tool.radius"); @@ -130,15 +133,40 @@ pub const MENU_EDIT_REDO: StringKey = StringKey::new("menu.edit.redo"); pub const MENU_VIEW_ZOOM_FIT: StringKey = StringKey::new("menu.view.zoom_fit"); pub const MENU_PLACEHOLDER_COMING_SOON: StringKey = StringKey::new("menu.placeholder.coming_soon"); pub const MENU_TOOLS_OPTIONS: StringKey = StringKey::new("menu.tools.options"); +pub const MENU_TOOLS_KEYBOARD: StringKey = StringKey::new("menu.tools.keyboard"); +pub const KEYBOARD_DIALOG_TITLE: StringKey = StringKey::new("keyboard.dialog.title"); pub const SETTINGS_DIALOG_TITLE: StringKey = StringKey::new("settings.dialog.title"); pub const SETTINGS_PICK_APERTURE_LABEL: StringKey = StringKey::new("settings.pick_aperture.label"); pub const SETTINGS_PICK_APERTURE_HINT: StringKey = StringKey::new("settings.pick_aperture.hint"); pub const SETTINGS_RESET: StringKey = StringKey::new("settings.reset"); pub const SETTINGS_CLOSE: StringKey = StringKey::new("settings.close"); -pub const SHORTCUT_QUIT: StringKey = StringKey::new("shortcut.quit"); -pub const SHORTCUT_UNDO: StringKey = StringKey::new("shortcut.undo"); -pub const SHORTCUT_REDO: StringKey = StringKey::new("shortcut.redo"); -pub const SHORTCUT_ZOOM_FIT: StringKey = StringKey::new("shortcut.zoom_fit"); +pub const HOTKEY_SECTION_HEADING: StringKey = StringKey::new("hotkey.section.heading"); +pub const HOTKEY_RECORDING_HINT: StringKey = StringKey::new("hotkey.recording.hint"); +pub const HOTKEY_RECORDING_PROMPT: StringKey = StringKey::new("hotkey.recording.prompt"); +pub const HOTKEY_UNBOUND_LABEL: StringKey = StringKey::new("hotkey.unbound.label"); +pub const HOTKEY_LABEL_SKETCH: StringKey = StringKey::new("hotkey.label.sketch"); +pub const HOTKEY_LABEL_ESCAPE: StringKey = StringKey::new("hotkey.label.escape"); +pub const HOTKEY_LABEL_SMART_DIMENSION: StringKey = StringKey::new("hotkey.label.smart_dimension"); +pub const HOTKEY_LABEL_TRIM: StringKey = StringKey::new("hotkey.label.trim"); +pub const HOTKEY_LABEL_EXTEND: StringKey = StringKey::new("hotkey.label.extend"); +pub const HOTKEY_LABEL_MIRROR: StringKey = StringKey::new("hotkey.label.mirror"); +pub const HOTKEY_LABEL_CONSTRUCTION_TOGGLE: StringKey = + StringKey::new("hotkey.label.construction_toggle"); +pub const HOTKEY_LABEL_UNDO: StringKey = StringKey::new("hotkey.label.undo"); +pub const HOTKEY_LABEL_REDO: StringKey = StringKey::new("hotkey.label.redo"); +pub const HOTKEY_LABEL_NEW: StringKey = StringKey::new("hotkey.label.new"); +pub const HOTKEY_LABEL_OPEN: StringKey = StringKey::new("hotkey.label.open"); +pub const HOTKEY_LABEL_SAVE: StringKey = StringKey::new("hotkey.label.save"); +pub const HOTKEY_LABEL_SELECT_ALL: StringKey = StringKey::new("hotkey.label.select_all"); +pub const HOTKEY_LABEL_DELETE_SELECTION: StringKey = + StringKey::new("hotkey.label.delete_selection"); +pub const HOTKEY_LABEL_ZOOM_FIT: StringKey = StringKey::new("hotkey.label.zoom_fit"); +pub const HOTKEY_LABEL_SHORTCUT_BAR: StringKey = StringKey::new("hotkey.label.shortcut_bar"); +pub const HOTKEY_LABEL_QUIT: StringKey = StringKey::new("hotkey.label.quit"); +pub const NOTIFY_COMING_SOON: StringKey = StringKey::new("notify.coming_soon"); +pub const NOTIFY_MIRROR_SELECTION_HINT: StringKey = StringKey::new("notify.mirror.selection_hint"); +pub const NOTIFY_HOTKEY_CONFLICT: StringKey = StringKey::new("notify.hotkey.conflict"); +pub const SHORTCUT_BAR_TITLE: StringKey = StringKey::new("shortcut_bar.title"); pub const PROPERTY_PANE_NO_SELECTION: StringKey = StringKey::new("property.no_selection"); pub const PROPERTY_ROW_KIND: StringKey = StringKey::new("property.row.kind"); @@ -227,6 +255,7 @@ const EN_US: &[(StringKey, &str)] = &[ (TOOL_CONCENTRIC, "Concentric"), (TOOL_FIX, "Fix"), (TOOL_MIDPOINT, "Midpoint"), + (TOOL_SYMMETRIC, "Symmetric"), (REL_HINT_ONE_LINE, "Select a line"), (REL_HINT_TWO_LINES, "Select two lines"), (REL_HINT_COINCIDENT, "Select a point and another entity"), @@ -235,6 +264,8 @@ const EN_US: &[(StringKey, &str)] = &[ (REL_HINT_EQUAL, "Select two lines or two curves"), (REL_HINT_MIDPOINT, "Select a point and a line"), (REL_HINT_ENTITY, "Select an entity"), + (REL_HINT_SYMMETRIC, "Select two points and a line"), + (PROPERTY_ROW_AXIS, "Axis"), (TOOL_SMART_DIMENSION, "Smart Dimension"), (TOOL_RADIUS, "Radius"), (TOOL_DIAMETER, "Diameter"), @@ -320,6 +351,8 @@ const EN_US: &[(StringKey, &str)] = &[ (MENU_VIEW_ZOOM_FIT, "Zoom to Fit"), (MENU_PLACEHOLDER_COMING_SOON, "Coming Soon"), (MENU_TOOLS_OPTIONS, "Options..."), + (MENU_TOOLS_KEYBOARD, "Customize Keyboard..."), + (KEYBOARD_DIALOG_TITLE, "Customize Keyboard"), (SETTINGS_DIALOG_TITLE, "Selection options"), (SETTINGS_PICK_APERTURE_LABEL, "Pick aperture"), ( @@ -328,10 +361,37 @@ const EN_US: &[(StringKey, &str)] = &[ ), (SETTINGS_RESET, "Reset"), (SETTINGS_CLOSE, "Close"), - (SHORTCUT_QUIT, "Ctrl+Q"), - (SHORTCUT_UNDO, "Ctrl+Z"), - (SHORTCUT_REDO, "Ctrl+Y"), - (SHORTCUT_ZOOM_FIT, "F"), + (HOTKEY_SECTION_HEADING, "Keyboard shortcuts"), + (HOTKEY_RECORDING_HINT, "Click to record, Esc to cancel"), + (HOTKEY_RECORDING_PROMPT, "Press a key..."), + (HOTKEY_UNBOUND_LABEL, "Unbound"), + (HOTKEY_LABEL_SKETCH, "Sketch"), + (HOTKEY_LABEL_ESCAPE, "Cancel / Exit Sketch"), + (HOTKEY_LABEL_SMART_DIMENSION, "Smart Dimension"), + (HOTKEY_LABEL_TRIM, "Trim Entities"), + (HOTKEY_LABEL_EXTEND, "Extend Entities"), + (HOTKEY_LABEL_MIRROR, "Mirror Entities"), + (HOTKEY_LABEL_CONSTRUCTION_TOGGLE, "Toggle Construction"), + (HOTKEY_LABEL_UNDO, "Undo"), + (HOTKEY_LABEL_REDO, "Redo"), + (HOTKEY_LABEL_NEW, "New"), + (HOTKEY_LABEL_OPEN, "Open"), + (HOTKEY_LABEL_SAVE, "Save"), + (HOTKEY_LABEL_SELECT_ALL, "Select All"), + (HOTKEY_LABEL_DELETE_SELECTION, "Delete"), + (HOTKEY_LABEL_ZOOM_FIT, "Zoom to Fit"), + (HOTKEY_LABEL_SHORTCUT_BAR, "Shortcut Bar"), + (HOTKEY_LABEL_QUIT, "Quit"), + (NOTIFY_COMING_SOON, "Coming soon"), + ( + NOTIFY_MIRROR_SELECTION_HINT, + "Select entities to mirror plus one axis line.", + ), + ( + NOTIFY_HOTKEY_CONFLICT, + "Shortcut already used by another command.", + ), + (SHORTCUT_BAR_TITLE, "Shortcut Bar"), (PROPERTY_PANE_NO_SELECTION, "Nothing selected"), (PROPERTY_ROW_KIND, "Type"), (PROPERTY_ROW_X, "X"), @@ -407,6 +467,7 @@ const AR_XB: &[(StringKey, &str)] = &[ (TOOL_CONCENTRIC, "[!! Concêntric !!]"), (TOOL_FIX, "[!! Fîx !!]"), (TOOL_MIDPOINT, "[!! Mîdpoint !!]"), + (TOOL_SYMMETRIC, "[!! Symmêtric !!]"), (REL_HINT_ONE_LINE, "[!! Sêlect a lîne !!]"), (REL_HINT_TWO_LINES, "[!! Sêlect twô lînes !!]"), ( @@ -421,6 +482,8 @@ const AR_XB: &[(StringKey, &str)] = &[ (REL_HINT_EQUAL, "[!! Sêlect twô lînes ôr twô cûrves !!]"), (REL_HINT_MIDPOINT, "[!! Sêlect a pôint ând a lîne !!]"), (REL_HINT_ENTITY, "[!! Sêlect an êntity !!]"), + (REL_HINT_SYMMETRIC, "[!! Sêlect twô pôints ând a lîne !!]"), + (PROPERTY_ROW_AXIS, "[!! Âxis !!]"), (TOOL_SMART_DIMENSION, "[!! Smârt Dimensiôn !!]"), (TOOL_RADIUS, "[!! Râdius !!]"), (TOOL_DIAMETER, "[!! Dîameter !!]"), @@ -509,6 +572,8 @@ const AR_XB: &[(StringKey, &str)] = &[ (MENU_VIEW_ZOOM_FIT, "[!! Zôom to Fît !!]"), (MENU_PLACEHOLDER_COMING_SOON, "[!! Côming Sôon !!]"), (MENU_TOOLS_OPTIONS, "[!! Ôptions... !!]"), + (MENU_TOOLS_KEYBOARD, "[!! Custômize Kêyboard... !!]"), + (KEYBOARD_DIALOG_TITLE, "[!! Custômize Kêyboard !!]"), (SETTINGS_DIALOG_TITLE, "[!! Sêlection ôptions !!]"), (SETTINGS_PICK_APERTURE_LABEL, "[!! Pîck âperture !!]"), ( @@ -517,10 +582,43 @@ const AR_XB: &[(StringKey, &str)] = &[ ), (SETTINGS_RESET, "[!! Resêt !!]"), (SETTINGS_CLOSE, "[!! Clôse !!]"), - (SHORTCUT_QUIT, "Ctrl+Q"), - (SHORTCUT_UNDO, "Ctrl+Z"), - (SHORTCUT_REDO, "Ctrl+Y"), - (SHORTCUT_ZOOM_FIT, "F"), + (HOTKEY_SECTION_HEADING, "[!! Kêyboard shortcûts !!]"), + ( + HOTKEY_RECORDING_HINT, + "[!! Clîck to recôrd, Êsc to cancêl !!]", + ), + (HOTKEY_RECORDING_PROMPT, "[!! Prêss a kêy... !!]"), + (HOTKEY_UNBOUND_LABEL, "[!! Unbôund !!]"), + (HOTKEY_LABEL_SKETCH, "[!! Skêtch !!]"), + (HOTKEY_LABEL_ESCAPE, "[!! Cancêl / Êxit Skêtch !!]"), + (HOTKEY_LABEL_SMART_DIMENSION, "[!! Smârt Dîmension !!]"), + (HOTKEY_LABEL_TRIM, "[!! Trîm Êntities !!]"), + (HOTKEY_LABEL_EXTEND, "[!! Extênd Êntities !!]"), + (HOTKEY_LABEL_MIRROR, "[!! Mîrror Êntities !!]"), + ( + HOTKEY_LABEL_CONSTRUCTION_TOGGLE, + "[!! Tôggle Constrûction !!]", + ), + (HOTKEY_LABEL_UNDO, "[!! Undô !!]"), + (HOTKEY_LABEL_REDO, "[!! Redô !!]"), + (HOTKEY_LABEL_NEW, "[!! Néw !!]"), + (HOTKEY_LABEL_OPEN, "[!! Ôpen !!]"), + (HOTKEY_LABEL_SAVE, "[!! Sâve !!]"), + (HOTKEY_LABEL_SELECT_ALL, "[!! Sêlect Âll !!]"), + (HOTKEY_LABEL_DELETE_SELECTION, "[!! Delête !!]"), + (HOTKEY_LABEL_ZOOM_FIT, "[!! Zôom to Fît !!]"), + (HOTKEY_LABEL_SHORTCUT_BAR, "[!! Shortcût Bâr !!]"), + (HOTKEY_LABEL_QUIT, "[!! Quît !!]"), + (NOTIFY_COMING_SOON, "[!! Côming sôon !!]"), + ( + NOTIFY_MIRROR_SELECTION_HINT, + "[!! Sêlect entîties tô mîrror plûs ône âxis lîne. !!]", + ), + ( + NOTIFY_HOTKEY_CONFLICT, + "[!! Shortcût alrêady ûsed by anôther commând. !!]", + ), + (SHORTCUT_BAR_TITLE, "[!! Shortcût Bâr !!]"), (PROPERTY_PANE_NO_SELECTION, "[!! Nôthing sêlected !!]"), (PROPERTY_ROW_KIND, "[!! Týpe !!]"), (PROPERTY_ROW_X, "X"), diff --git a/crates/bone-document/src/sketch/mod.rs b/crates/bone-document/src/sketch/mod.rs index 0e5a6f4..4ba7684 100644 --- a/crates/bone-document/src/sketch/mod.rs +++ b/crates/bone-document/src/sketch/mod.rs @@ -276,6 +276,12 @@ impl Sketch { } _ => false, }, + SketchRelation::Symmetric { a, b, axis } => { + self.kind_of(a)? == K::Point + && self.kind_of(b)? == K::Point + && self.kind_of(axis)? == K::Line + && a != b + } SketchRelation::Fix(a) => { self.kind_of(a)?; true diff --git a/crates/bone-document/src/sketch/relation.rs b/crates/bone-document/src/sketch/relation.rs index 88ba13a..56c4b80 100644 --- a/crates/bone-document/src/sketch/relation.rs +++ b/crates/bone-document/src/sketch/relation.rs @@ -15,6 +15,11 @@ pub enum SketchRelation { point: SketchEntityId, line: SketchEntityId, }, + Symmetric { + a: SketchEntityId, + b: SketchEntityId, + axis: SketchEntityId, + }, Fix(SketchEntityId), } @@ -27,9 +32,12 @@ impl SketchRelation { | Self::Perpendicular(a, b) | Self::Tangent(a, b) | Self::Equal(a, b) - | Self::Concentric(a, b) => RelationRefs([Some(a), Some(b)]), - Self::Midpoint { point, line } => RelationRefs([Some(point), Some(line)]), - Self::Horizontal(a) | Self::Vertical(a) | Self::Fix(a) => RelationRefs([Some(a), None]), + | Self::Concentric(a, b) => RelationRefs([Some(a), Some(b), None]), + Self::Midpoint { point, line } => RelationRefs([Some(point), Some(line), None]), + Self::Symmetric { a, b, axis } => RelationRefs([Some(a), Some(b), Some(axis)]), + Self::Horizontal(a) | Self::Vertical(a) | Self::Fix(a) => { + RelationRefs([Some(a), None, None]) + } } } @@ -43,17 +51,17 @@ impl SketchRelation { | Self::Equal(a, b) | Self::Concentric(a, b) => Some((a, b)), Self::Midpoint { point, line } => Some((point, line)), - Self::Horizontal(_) | Self::Vertical(_) | Self::Fix(_) => None, + Self::Symmetric { .. } | Self::Horizontal(_) | Self::Vertical(_) | Self::Fix(_) => None, } } } #[derive(Copy, Clone, Debug, PartialEq)] -pub struct RelationRefs([Option; 2]); +pub struct RelationRefs([Option; 3]); impl IntoIterator for RelationRefs { type Item = SketchEntityId; - type IntoIter = core::iter::Flatten, 2>>; + type IntoIter = core::iter::Flatten, 3>>; fn into_iter(self) -> Self::IntoIter { self.0.into_iter().flatten() diff --git a/crates/bone-document/src/sketch/solve.rs b/crates/bone-document/src/sketch/solve.rs index 978597d..3783176 100644 --- a/crates/bone-document/src/sketch/solve.rs +++ b/crates/bone-document/src/sketch/solve.rs @@ -418,10 +418,32 @@ fn lower_relation( SketchRelation::Equal(a, b) => lower_equal(sketch, a, b, points, radii), SketchRelation::Concentric(a, b) => lower_concentric(sketch, a, b, points), SketchRelation::Midpoint { point, line } => lower_midpoint(sketch, point, line, points), + SketchRelation::Symmetric { a, b, axis } => lower_symmetric(sketch, a, b, axis, points), SketchRelation::Fix(id) => lower_fix(sketch, id, points, radii, parameters), } } +fn lower_symmetric( + sketch: &Sketch, + a: SketchEntityId, + b: SketchEntityId, + axis: SketchEntityId, + points: &BTreeMap, +) -> Vec { + let (Some(a_handle), Some(b_handle), Some(line)) = ( + points.get(&a).copied(), + points.get(&b).copied(), + line_handle(sketch, axis, points), + ) else { + return Vec::new(); + }; + vec![Residual::Symmetric { + a: a_handle, + b: b_handle, + axis: line, + }] +} + fn lower_coincident( sketch: &Sketch, a: SketchEntityId, diff --git a/crates/bone-render/assets/relation_glyphs.png b/crates/bone-render/assets/relation_glyphs.png index 8e8be9537fe798b0ae0bbb8828ab9fe36e40bd89..41c7a8c5aad07092aabadf2b447f064768a96eb8 100644 GIT binary patch delta 889 zcmZpax+ptAnTc=4M%6i7^=I$3Y1eiApF1IU-7oV>=?CgNALTaAe^f7@GJEwuUps*x zV(We=Jc#~zpZ(^uRrQnkS@yMR+j0B|`}f^(+s;?_pD;J(KkVXX+BfxoE!c>k4iBPt zJZf&pe~Q(J`jyjJ_O)o+asG(@mtCvypf%s2<_Ue2HKfum(|B$FWOO5sixY(cbO^ARXHXQ0f0df0}_DpfV zKi7X0KX|(1k$%JU=l1tbGvpr--Ou(%^!zt?{A}LK^MNhqWA=ggkM_c#2(IVbBNP6I z^}{F32(II-NM83N^?~mX^a%FetMWlL=A-9<`=8=p*TaIG$OK1Ln!U&N!*g9l^n>L; zAjy97_J>{bAJwa;%w9dYkVSNI0gD(O%qjLyP}_cuo@%k=ItB&?22WQ%mvv4FO#m1- B+4}$h delta 538 zcmca8+bA_bnThAbM%6i7lMVP(>VL++Qf<_K*yYdkZ`$gATPNiHoBm7nLGR8-(;Lkn zuBz8fnf-t1KbZ>4bw9WtJl**!p8Mvr_>+4I9_>FipIQI7Xg%W|oAAHcinsr0&lLCj zbNxs0gQq(l={HP&4%S<*`uR|2{ru@H`&cXv?|%c5`Z0Yjs~zW$=yki~H=W(DIjQpD zk^SfO8S~H7_r~DVkp4XXG1wu$6(_&umZ>kg`gcO^zo}=q z)&FJxvHVaJ^HKc3>Ef&V#WtPY4>GB~*PcDDPx~KJMdi9**PXWgjDK~$ar)z~{S0yQ zaQQAy=!ej{A1V*>A--GnfBS^ob-(JlZ$86d)?*i@n`jgsw-N1#AMWB+VS1Spqhc5s P7#KWV{an^LB{Ts5%kw}G diff --git a/crates/bone-render/src/pipelines/glyph.rs b/crates/bone-render/src/pipelines/glyph.rs index c93fb36..76007d1 100644 --- a/crates/bone-render/src/pipelines/glyph.rs +++ b/crates/bone-render/src/pipelines/glyph.rs @@ -409,6 +409,10 @@ mod tests { segment(x, y, 6.0, 16.0, 26.0, 16.0, 1.8), segment(x, y, 16.0, 11.0, 16.0, 21.0, 1.8), ), + RelationGlyphKind::Symmetric => combine( + segment(x, y, 16.0, 4.0, 16.0, 28.0, 1.4), + combine(disc(x, y, 9.0, 16.0, 3.5), disc(x, y, 23.0, 16.0, 3.5)), + ), } } diff --git a/crates/bone-render/src/scene.rs b/crates/bone-render/src/scene.rs index 9c24694..2764dd9 100644 --- a/crates/bone-render/src/scene.rs +++ b/crates/bone-render/src/scene.rs @@ -149,6 +149,7 @@ pub enum RelationGlyphKind { Concentric = 7, Fix = 8, Midpoint = 9, + Symmetric = 10, } impl RelationGlyphKind { @@ -170,6 +171,7 @@ impl RelationGlyphKind { 7 => Some(Self::Concentric), 8 => Some(Self::Fix), 9 => Some(Self::Midpoint), + 10 => Some(Self::Symmetric), _ => None, } } @@ -186,12 +188,13 @@ impl RelationGlyphKind { SketchRelation::Equal(_, _) => Self::Equal, SketchRelation::Concentric(_, _) => Self::Concentric, SketchRelation::Midpoint { .. } => Self::Midpoint, + SketchRelation::Symmetric { .. } => Self::Symmetric, SketchRelation::Fix(_) => Self::Fix, } } #[must_use] - pub const fn all() -> [Self; 10] { + pub const fn all() -> [Self; 11] { [ Self::Coincident, Self::Horizontal, @@ -203,6 +206,7 @@ impl RelationGlyphKind { Self::Concentric, Self::Fix, Self::Midpoint, + Self::Symmetric, ] } } diff --git a/crates/bone-render/tests/goldens/relations_256.png b/crates/bone-render/tests/goldens/relations_256.png index 33a11ac6bd025d56d8d8c71f701b7ee8a136db88..8da95872570a46bbab35c4e73a93679a371feeaa 100644 GIT binary patch delta 4222 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 II;Vst0R3AoLI3~& delta 4090 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 JS?83{1OSrZ>iz%# diff --git a/crates/bone-render/tests/relations.rs b/crates/bone-render/tests/relations.rs index ec87215..50704c7 100644 --- a/crates/bone-render/tests/relations.rs +++ b/crates/bone-render/tests/relations.rs @@ -132,6 +132,20 @@ fn one_per_kind_scene() -> SketchScene { }, ); + let (s, sym_a0) = add_point(s, 6.5, -2.5); + let (s, sym_a1) = add_point(s, 9.0, -2.5); + let (s, sym_axis) = add_line(s, sym_a0, sym_a1); + let (s, sym_p) = add_point(s, 7.5, -1.5); + let (s, sym_q) = add_point(s, 7.5, -3.5); + let s = add_relation( + s, + SketchRelation::Symmetric { + a: sym_p, + b: sym_q, + axis: sym_axis, + }, + ); + let Ok(scene) = SketchScene::extract(&s) else { panic!("scene extract"); }; diff --git a/crates/bone-solver/src/residual.rs b/crates/bone-solver/src/residual.rs index a55cef1..1fa2493 100644 --- a/crates/bone-solver/src/residual.rs +++ b/crates/bone-solver/src/residual.rs @@ -80,6 +80,11 @@ pub enum Residual { curve: CurveRadius, value_mm: f64, }, + Symmetric { + a: PointHandle, + b: PointHandle, + axis: LineHandle, + }, } pub type Triplet = (ResidualIndex, ParameterIndex, f64); @@ -88,7 +93,9 @@ impl Residual { #[must_use] pub fn rows(&self) -> usize { match self { - Self::CoincidentPointPoint(..) | Self::MidpointPointLine { .. } => 2, + Self::CoincidentPointPoint(..) + | Self::MidpointPointLine { .. } + | Self::Symmetric { .. } => 2, _ => 1, } } @@ -128,6 +135,11 @@ impl Residual { line_params(a).into_iter().chain(line_params(b)).collect() } Self::RadiusCurve { curve, .. } => curve_params(curve), + Self::Symmetric { a, b, axis } => point_params(a) + .into_iter() + .chain(point_params(b)) + .chain(line_params(axis)) + .collect(), } } @@ -219,6 +231,17 @@ impl Residual { Self::RadiusCurve { curve, value_mm } => { out[0] = radius_squared(params, curve) - value_mm * value_mm; } + Self::Symmetric { a, b, axis } => { + let (ax, ay) = (get(params, a.x), get(params, a.y)); + let (bx, by) = (get(params, b.x), get(params, b.y)); + let (dx, dy) = delta(params, axis); + let lax = get(params, axis.a.x); + let lay = get(params, axis.a.y); + out[0] = (bx - ax) * dx + (by - ay) * dy; + let mx = 0.5 * (ax + bx) - lax; + let my = 0.5 * (ay + by) - lay; + out[1] = mx * dy - my * dx; + } } } @@ -277,10 +300,49 @@ impl Residual { Self::RadiusCurve { curve, .. } => { add_radius_squared_jacobian(params, row, curve, sink); } + Self::Symmetric { a, b, axis } => { + jacobian_symmetric(params, row, a, b, axis, sink); + } } } } +fn jacobian_symmetric( + params: &[f64], + row: ResidualIndex, + a: PointHandle, + b: PointHandle, + axis: LineHandle, + sink: &mut Vec, +) { + let (dx, dy) = delta(params, axis); + let (ax, ay) = (get(params, a.x), get(params, a.y)); + let (bx, by) = (get(params, b.x), get(params, b.y)); + let lax = get(params, axis.a.x); + let lay = get(params, axis.a.y); + let mx = 0.5 * (ax + bx) - lax; + let my = 0.5 * (ay + by) - lay; + let bxa_x = bx - ax; + let bxa_y = by - ay; + sink.push((row, a.x, -dx)); + sink.push((row, a.y, -dy)); + sink.push((row, b.x, dx)); + sink.push((row, b.y, dy)); + sink.push((row, axis.a.x, -bxa_x)); + sink.push((row, axis.a.y, -bxa_y)); + sink.push((row, axis.b.x, bxa_x)); + sink.push((row, axis.b.y, bxa_y)); + let row2 = row.next(); + sink.push((row2, a.x, 0.5 * dy)); + sink.push((row2, a.y, -0.5 * dx)); + sink.push((row2, b.x, 0.5 * dy)); + sink.push((row2, b.y, -0.5 * dx)); + sink.push((row2, axis.a.x, -dy + my)); + sink.push((row2, axis.a.y, -mx + dx)); + sink.push((row2, axis.b.x, -my)); + sink.push((row2, axis.b.y, mx)); +} + fn jacobian_parallel( params: &[f64], row: ResidualIndex, diff --git a/crates/bone-solver/src/system.rs b/crates/bone-solver/src/system.rs index 8825ef3..95243a2 100644 --- a/crates/bone-solver/src/system.rs +++ b/crates/bone-solver/src/system.rs @@ -254,5 +254,10 @@ fn remap_residual(r: &Residual, m: &BTreeMap) -> curve: curve(c), value_mm, }, + Residual::Symmetric { a, b, axis } => Residual::Symmetric { + a: point(a), + b: point(b), + axis: line(axis), + }, } } diff --git a/crates/bone-solver/tests/core.rs b/crates/bone-solver/tests/core.rs index 9507a7c..945196f 100644 --- a/crates/bone-solver/tests/core.rs +++ b/crates/bone-solver/tests/core.rs @@ -174,6 +174,99 @@ fn decomposed_singular_index_is_translated_to_global_parameter() { } } +#[test] +fn symmetric_residual_is_zero_when_b_is_reflection_of_a() { + let system = ConstraintSystem::new( + parameters(&[0.0, 1.0, 0.0, -1.0, -2.0, 0.0, 2.0, 0.0]), + vec![Residual::Symmetric { + a: point(0, 1), + b: point(2, 3), + axis: LineHandle { + a: point(4, 5), + b: point(6, 7), + }, + }], + ); + assert_eq!(system.row_count(), 2); + let values = evaluate_residuals(&system, &[0.0, 1.0, 0.0, -1.0, -2.0, 0.0, 2.0, 0.0]); + assert!(values[0].abs() < 1e-15, "perpendicularity: {}", values[0]); + assert!(values[1].abs() < 1e-15, "midpoint-on-axis: {}", values[1]); +} + +#[test] +fn symmetric_residual_is_nonzero_when_b_drifts_off_reflection() { + let system = ConstraintSystem::new( + parameters(&[0.0, 1.0, 0.5, -1.0, -2.0, 0.0, 2.0, 0.0]), + vec![Residual::Symmetric { + a: point(0, 1), + b: point(2, 3), + axis: LineHandle { + a: point(4, 5), + b: point(6, 7), + }, + }], + ); + let values = evaluate_residuals(&system, &[0.0, 1.0, 0.5, -1.0, -2.0, 0.0, 2.0, 0.0]); + assert!( + values[0].abs() > 0.0 || values[1].abs() > 0.0, + "drift must surface in at least one residual", + ); +} + +#[test] +fn symmetric_solves_to_b_as_reflection_of_a_about_x_axis() { + let system = ConstraintSystem::new( + parameters(&[0.0, 1.0, 0.5, 1.5, -2.0, 0.0, 2.0, 0.0]), + vec![ + Residual::Pin { + param: p(0), + target: 0.0, + }, + Residual::Pin { + param: p(1), + target: 1.0, + }, + Residual::Pin { + param: p(4), + target: -2.0, + }, + Residual::Pin { + param: p(5), + target: 0.0, + }, + Residual::Pin { + param: p(6), + target: 2.0, + }, + Residual::Pin { + param: p(7), + target: 0.0, + }, + Residual::Symmetric { + a: point(0, 1), + b: point(2, 3), + axis: LineHandle { + a: point(4, 5), + b: point(6, 7), + }, + }, + ], + ); + let Ok(out) = solve_newton(&system, NewtonConfig::DEFAULT) else { + panic!("symmetric over fixed axis must converge"); + }; + assert!( + (out[2].value() - 0.0).abs() < 1e-6, + "b.x = a.x for reflection over x-axis: got {}", + out[2].value() + ); + assert!( + (out[3].value() - -1.0).abs() < 1e-6, + "b.y = -a.y for reflection over x-axis: got {}", + out[3].value() + ); +} + #[test] fn assemble_jacobian_and_triplets_agree_on_dense_shape() { let system = ConstraintSystem::new(