1use super::*;
10use crate::pick::{PickCandidate, PickKind};
11
12#[derive(Clone, Copy, Debug, PartialEq)]
14pub(crate) struct ContextDisplay {
15 pub shown: bool,
16 pub opacity: f32,
17}
18
19impl Default for ContextDisplay {
20 fn default() -> Self {
21 Self { shown: true, opacity: crate::render::WORK_PART_CONTEXT_RGBA[3] }
22 }
23}
24
25impl EngineState {
26 pub fn context_display(&self) -> (bool, f32) {
28 (self.context_display.shown, self.context_display.opacity)
29 }
30
31 pub fn set_context_display(&mut self, shown: bool, opacity: f32) {
35 let display = ContextDisplay { shown, opacity: opacity.clamp(0.05, 1.0) };
36 if display != self.context_display {
37 self.context_display = display;
38 if !shown {
39 self.context_selected = None;
40 self.context_hovered = None;
41 }
42 self.sync_context_highlight();
43 }
44 }
45
46 pub fn context_hit_at(&self, x: f64, y: f64) -> Option<PickCandidate> {
49 if self.context_scene.solids().is_empty() {
50 return None;
51 }
52 let options = self.pick_options();
53 let front = pick::pick(&self.context_scene, &self.camera, x, y, &options)
54 .into_iter()
55 .filter(|candidate| candidate.kind == PickKind::Face)
56 .min_by(|a, b| a.depth.total_cmp(&b.depth))?;
57 let own = pick::nearest_face_depth(&self.scene, &self.camera, x, y, &options);
58 let tolerance = 1e-6 * front.depth.abs().max(1.0) + 1e-4;
60 match own {
61 Some(depth) if depth <= front.depth + tolerance => None,
62 _ => Some(front),
63 }
64 }
65
66 pub fn context_edge_at(&self, x: f64, y: f64) -> Option<PickCandidate> {
70 if self.context_scene.solids().is_empty() {
71 return None;
72 }
73 let options = self.pick_options();
74 let edge = pick::pick(&self.context_scene, &self.camera, x, y, &options)
75 .into_iter()
76 .filter(|candidate| candidate.kind == PickKind::Edge && !candidate.name.is_empty())
77 .min_by(|a, b| a.screen_dist.total_cmp(&b.screen_dist).then(a.depth.total_cmp(&b.depth)))?;
78 let own = pick::nearest_face_depth(&self.scene, &self.camera, x, y, &options);
79 let tolerance = 1e-6 * edge.depth.abs().max(1.0) + 1e-4;
80 match own {
81 Some(depth) if depth + tolerance < edge.depth => None,
82 _ => Some(edge),
83 }
84 }
85
86 pub fn sketch_copy_context_edge(&mut self, edge: &PickCandidate) -> bool {
92 let Some(polyline) = self
93 .context_scene
94 .solid(&edge.solid)
95 .and_then(|solid| solid.edges.iter().find(|e| e.name == edge.name))
96 .map(|e| e.polyline.iter().map(|p| p.map(f64::from)).collect::<Vec<_>>())
97 .filter(|polyline| polyline.len() >= 2)
98 else {
99 return false;
100 };
101 let part = part_label(&edge.solid);
102 let Some(sketch) = self.sketch_edit.as_mut() else {
103 return false;
104 };
105 let uv = crate::sketch::external_ref::project_polyline(&sketch.session.plane, &polyline);
106 let link = crate::sketch::external_ref::classify_uv(&uv);
107 sketch.record_undo();
108 let added = crate::sketch::external_ref::add_copied_edge(&mut sketch.session.doc, &link, &format!("copied from {part}, not linked"));
109 if added.is_empty() {
110 sketch.undo_stack.pop();
111 return false;
112 }
113 self.resolve_active_sketch("copy context edge");
114 self.refresh_sketch_overlay();
115 self.hover_context(None);
116 self.push_notice_as(
117 NoticeSeverity::Info,
118 format!("Copied an edge of {part} into the sketch as a {}: copied, not linked (it stays where it is if {part} moves)", link.kind()),
119 );
120 self.dirty = true;
121 true
122 }
123
124 pub fn select_context(&mut self, candidate: Option<PickCandidate>) -> bool {
127 let changed = !same_entity(self.context_selected.as_ref(), candidate.as_ref());
128 self.context_selected = candidate;
129 if changed {
130 self.sync_context_highlight();
131 }
132 changed
133 }
134
135 pub fn hover_context(&mut self, candidate: Option<PickCandidate>) -> bool {
138 let changed = !same_entity(self.context_hovered.as_ref(), candidate.as_ref());
139 self.context_hovered = candidate;
140 if changed {
141 self.sync_context_highlight();
142 }
143 changed
144 }
145
146 pub fn context_selection(&self) -> Option<&PickCandidate> {
148 self.context_selected.as_ref()
149 }
150
151 pub fn sync_context_highlight(&mut self) {
155 let has = |scene: &RenderScene, c: &PickCandidate| {
156 scene.solid(&c.solid).is_some_and(|solid| match c.kind {
157 PickKind::Edge => solid.edges.iter().any(|e| e.name == c.name),
158 _ => solid.faces.iter().any(|f| f.name == c.name),
159 })
160 };
161 if self.context_selected.as_ref().is_some_and(|c| !has(&self.context_scene, c)) {
162 self.context_selected = None;
163 }
164 if self.context_hovered.as_ref().is_some_and(|c| !has(&self.context_scene, c)) {
165 self.context_hovered = None;
166 }
167 let mut highlight = Vec::new();
168 if let Some(c) = &self.context_hovered {
169 highlight.push((c.solid.clone(), c.name.clone(), false));
170 }
171 if let Some(c) = &self.context_selected {
172 highlight.push((c.solid.clone(), c.name.clone(), true));
173 }
174 for name in &self.context_pick_lit {
176 if let Some(ghost) = self.context_scene.solid(name) {
177 highlight.extend(ghost.faces.iter().map(|face| (name.clone(), face.name.clone(), true)));
178 }
179 }
180 if self.context_scene.highlight != highlight {
181 self.context_scene.highlight = highlight;
182 self.dirty = true;
183 }
184 let [r, g, b, _] = crate::render::WORK_PART_CONTEXT_RGBA;
187 let tint = Some([r, g, b, self.context_display.opacity]);
188 if self.context_scene.tint != tint {
189 self.context_scene.tint = tint;
190 self.dirty = true;
191 }
192 let shown = self.context_display.shown;
193 let flip: Vec<String> = self.context_scene.solids().iter().filter(|solid| solid.visible != shown).map(|solid| solid.name.clone()).collect();
194 for name in flip {
195 if let Some(solid) = self.context_scene.solid_mut(&name) {
196 solid.visible = shown;
197 self.dirty = true;
198 }
199 }
200 }
201
202 pub fn context_tangent_plane(&self) -> Result<([f64; 3], [f64; 3]), String> {
206 let picked = self.context_selected.as_ref().ok_or("select a face of another part first")?;
207 let solid = self.context_scene.solid(&picked.solid).ok_or("that part is no longer shown")?;
208 let face = solid.faces.iter().find(|face| face.name == picked.name).ok_or("that face is no longer shown")?;
209 let mesh = &solid.mesh;
210 let at = |index: u32| mesh.positions[index as usize].map(f64::from);
211 let mut best: Option<(f64, [f64; 3])> = None;
212 for triangle in face.tri_start..face.tri_start + face.tri_count {
213 let base = triangle as usize * 3;
214 let Some(&[i0, i1, i2]) = mesh.indices.get(base..base + 3).map(|s| <&[u32; 3]>::try_from(s).unwrap()) else { continue };
215 let (a, b, c) = (at(i0), at(i1), at(i2));
216 let mut normal = cross(sub(b, a), sub(c, a));
217 let length = dot(normal, normal).sqrt();
218 if length < 1e-12 {
219 continue;
220 }
221 normal = normal.map(|v| v / length);
222 let shading = [i0, i1, i2].iter().filter_map(|&i| mesh.normals.get(i as usize)).fold([0.0; 3], |acc, n| [acc[0] + n[0] as f64, acc[1] + n[1] as f64, acc[2] + n[2] as f64]);
224 if dot(normal, shading) < 0.0 {
225 normal = normal.map(|v| -v);
226 }
227 let distance = distance_to_triangle(picked.position, a, b, c);
228 if best.is_none_or(|(d, _)| distance < d) {
229 best = Some((distance, normal));
230 }
231 }
232 let (_, normal) = best.ok_or("that face has no shape to place a plane on")?;
233 Ok((picked.position, normal))
234 }
235
236 pub fn datum_on_context(&mut self) -> Result<String, String> {
240 let picked = self.context_selected.clone().ok_or("select a face of another part first")?;
241 let (origin, normal) = self.context_tangent_plane()?;
242 let part = part_label(&picked.solid);
243 let id = self.history.next_feature_id(&format!("Copied{}", part.chars().filter(|c| c.is_ascii_alphanumeric()).collect::<String>()));
244 let (a, b) = datum_angles(normal);
245 let feature = serde_json::json!({
246 "type": "D",
247 "inputParams": {
248 "id": id,
249 "transform": {
250 "position": origin,
251 "rotationEuler": [a.to_degrees(), b.to_degrees(), 0.0],
252 "scale": [1.0, 1.0, 1.0],
253 },
254 },
255 "persistentData": {},
256 });
257 self.add_feature(&feature.to_string())?;
258 self.select_context(None);
259 self.hover_context(None);
260 self.push_notice_as(
261 NoticeSeverity::Info,
262 format!("Datum {id} placed on a face of {part}: copied, not linked (it stays where it is if {part} moves)"),
263 );
264 Ok(id)
265 }
266
267 pub fn sketch_on_context(&mut self) -> Result<String, String> {
270 let (origin, normal) = self.context_tangent_plane()?;
271 let datum = self.datum_on_context()?;
272 let (a, b) = datum_angles(normal);
275 let frame = crate::sketch::PlaneFrame {
276 origin,
277 x_axis: [b.cos(), a.sin() * b.sin(), -a.cos() * b.sin()],
278 y_axis: [0.0, a.cos(), a.sin()],
279 z_axis: [b.sin(), -b.cos() * a.sin(), a.cos() * b.cos()],
280 };
281 self.new_sketch_on(&format!("{datum}:XY"), frame)
282 }
283
284 pub fn context_selection_json(&self) -> serde_json::Value {
286 entity_json(self.context_selected.as_ref())
287 }
288
289 pub fn context_hover_json(&self) -> serde_json::Value {
291 entity_json(self.context_hovered.as_ref())
292 }
293}
294
295fn datum_angles(normal: [f64; 3]) -> (f64, f64) {
299 ((-normal[1]).atan2(normal[2]), normal[0].clamp(-1.0, 1.0).asin())
300}
301
302fn part_label(solid: &str) -> String {
304 solid.rsplit(':').next().unwrap_or(solid).to_string()
305}
306
307use crate::geometry3d::sub3 as sub;
308
309use crate::geometry3d::dot3 as dot;
310
311use crate::geometry3d::cross3 as cross;
312
313fn distance_to_triangle(p: [f64; 3], a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
315 let (ab, ac, ap) = (sub(b, a), sub(c, a), sub(p, a));
316 let (d1, d2) = (dot(ab, ap), dot(ac, ap));
317 let closest = if d1 <= 0.0 && d2 <= 0.0 {
318 a
319 } else {
320 let bp = sub(p, b);
321 let (d3, d4) = (dot(ab, bp), dot(ac, bp));
322 let cp = sub(p, c);
323 let (d5, d6) = (dot(ab, cp), dot(ac, cp));
324 let (vc, vb, va) = (d1 * d4 - d3 * d2, d5 * d2 - d1 * d6, d3 * d6 - d5 * d4);
325 if d3 >= 0.0 && d4 <= d3 {
326 b
327 } else if d6 >= 0.0 && d5 <= d6 {
328 c
329 } else if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
330 let v = d1 / (d1 - d3);
331 [a[0] + ab[0] * v, a[1] + ab[1] * v, a[2] + ab[2] * v]
332 } else if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
333 let w = d2 / (d2 - d6);
334 [a[0] + ac[0] * w, a[1] + ac[1] * w, a[2] + ac[2] * w]
335 } else if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
336 let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
337 let bc = sub(c, b);
338 [b[0] + bc[0] * w, b[1] + bc[1] * w, b[2] + bc[2] * w]
339 } else {
340 let denom = 1.0 / (va + vb + vc);
341 let (v, w) = (vb * denom, vc * denom);
342 [a[0] + ab[0] * v + ac[0] * w, a[1] + ab[1] * v + ac[1] * w, a[2] + ab[2] * v + ac[2] * w]
343 }
344 };
345 let d = sub(p, closest);
346 dot(d, d).sqrt()
347}
348
349fn entity_json(candidate: Option<&PickCandidate>) -> serde_json::Value {
350 match candidate {
351 Some(c) if c.kind == PickKind::Edge => serde_json::json!({ "solid": c.solid, "edge": c.name }),
352 Some(c) => serde_json::json!({ "solid": c.solid, "face": c.name }),
353 None => serde_json::Value::Null,
354 }
355}
356
357fn same_entity(a: Option<&PickCandidate>, b: Option<&PickCandidate>) -> bool {
358 match (a, b) {
359 (Some(a), Some(b)) => a.solid == b.solid && a.name == b.name && a.kind == b.kind,
360 (None, None) => true,
361 _ => false,
362 }
363}
364