1use serde_json::{json, Value};
19
20pub use brep_kernel::{
21 Sketch3dConstraint, Sketch3dDiagnostics, Sketch3dDoc, Sketch3dGeometry, Sketch3dPoint,
22 SKETCH3D_CONSTRAINT_TYPES,
23};
24use brep_kernel::{sketch3d_constraint_type, Vec3};
25
26use crate::sketch::session::{constraint_ref, geometry_ref, point_ref, refs_equal, RefSelection};
27use crate::sketch::tessellate::{interaction_color, SketchTessellation, POINT_SIZE_PX};
28use crate::sketch::PlaneFrame;
29use crate::style::SketchColors;
30use crate::view::ViewCamera;
31
32pub const OVERLAY_GEOMETRY: &str = "sketch3d-geometry";
35pub const OVERLAY_POINTS: &str = "sketch3d-points";
36pub const OVERLAY_PREVIEW: &str = "sketch3d-preview";
37pub const OVERLAY_WORKPLANE: &str = "sketch3d-workplane";
38pub const OVERLAY_GLYPHS: &str = "sketch3d-constraints";
40pub const OVERLAY_GROUPS: &[&str] =
41 &[OVERLAY_GEOMETRY, OVERLAY_POINTS, OVERLAY_PREVIEW, OVERLAY_WORKPLANE, OVERLAY_GLYPHS];
42
43const GLYPH_HALF_PX: f64 = 6.0;
46const GLYPH_STEP_PX: f64 = 16.0;
47const GLYPH_OFFSET_DIR: [f64; 3] = [0.76, 0.56, 0.33];
50
51const ARC_SEGMENTS: usize = 48;
53const BEZIER_SEGMENTS_PER_SPAN: usize = 24;
54const WORKPLANE_HALF_PX: f64 = 90.0;
56const REF_PLANE_HALF_PX: f64 = 40.0;
58
59pub const TOOLS: &[&str] = &["select", "point", "line", "arc", "bezier", "pickRef"];
62
63pub const WORK_PLANES: &[&str] = &["XY", "YZ", "XZ"];
65
66#[derive(Clone, Debug, PartialEq)]
68pub struct WorkPlane {
69 pub name: String,
71 pub anchor: [f64; 3],
73}
74
75impl WorkPlane {
76 pub fn new(name: &str, anchor: [f64; 3]) -> Option<Self> {
77 if !WORK_PLANES.contains(&name) {
78 return None;
79 }
80 Some(Self {
81 name: name.to_string(),
82 anchor,
83 })
84 }
85
86 pub fn frame(&self) -> PlaneFrame {
88 let mut frame = match self.name.as_str() {
89 "YZ" => PlaneFrame::yz(),
90 "XZ" => PlaneFrame::xz(),
91 _ => PlaneFrame::xy(),
92 };
93 frame.origin = self.anchor;
94 frame
95 }
96
97 pub fn next_name(&self) -> &'static str {
99 match self.name.as_str() {
100 "XY" => "YZ",
101 "YZ" => "XZ",
102 _ => "XY",
103 }
104 }
105}
106
107impl Default for WorkPlane {
108 fn default() -> Self {
109 Self {
110 name: "XY".into(),
111 anchor: [0.0; 3],
112 }
113 }
114}
115
116#[derive(Clone, Debug, serde::Serialize)]
120pub struct Sketch3dDimLabel {
121 pub id: Value,
122 pub text: String,
123 pub world: [f64; 3],
124 pub value: Option<f64>,
125 #[serde(rename = "valueExpr")]
126 pub value_expr: Option<String>,
127 pub mode: &'static str,
128 pub conflicting: bool,
129}
130
131#[derive(Clone, Debug, PartialEq, serde::Serialize)]
134pub struct ApplicableConstraint {
135 #[serde(rename = "type")]
137 pub ctype: &'static str,
138 pub label: &'static str,
140 pub points: Vec<Value>,
142 pub dimensional: bool,
144}
145
146pub struct Sketch3dSession {
148 pub doc: Sketch3dDoc,
150 pub diagnostics: Sketch3dDiagnostics,
152 pub selection: RefSelection,
155 pub tool: Option<String>,
157 pub work_plane: WorkPlane,
159 pub colors: SketchColors,
161 pub lost_references: Vec<(String, Vec<Value>)>,
164}
165
166impl Sketch3dSession {
167 pub fn new(doc: Sketch3dDoc) -> Result<Self, String> {
169 let mut session = Self {
170 doc,
171 diagnostics: Sketch3dDiagnostics::default(),
172 selection: RefSelection::default(),
173 tool: None,
174 work_plane: WorkPlane::default(),
175 colors: SketchColors::default(),
176 lost_references: Vec::new(),
177 };
178 session.resolve()?;
179 Ok(session)
180 }
181
182 pub fn resolve(&mut self) -> Result<(), String> {
184 let diagnostics = brep_kernel::solve_sketch3d_doc(
185 &mut self.doc,
186 &brep_kernel::Sketch3dSolveOptions::default(),
187 )?;
188 self.diagnostics = diagnostics;
189 Ok(())
190 }
191
192 pub fn is_selected(&self, entity_ref: &Value) -> bool {
195 self.selection.is_selected(entity_ref)
196 }
197
198 pub fn toggle_selection(&mut self, entity_ref: Value) {
199 self.selection.toggle(entity_ref)
200 }
201
202 pub fn clear_selection(&mut self) {
203 self.selection.clear()
204 }
205
206 pub fn set_hover(&mut self, entity_ref: Option<Value>) {
207 self.selection.hovered = entity_ref;
208 }
209
210 pub fn selection_by_kind(&self) -> (Vec<Value>, Vec<Value>, Vec<Value>) {
213 let mut points = Vec::new();
214 let mut geometries = Vec::new();
215 let mut constraints = Vec::new();
216 for r in &self.selection.items {
217 let Some(id) = r.get("id").cloned() else { continue };
218 match r.get("kind").and_then(Value::as_str) {
219 Some("point") => points.push(id),
220 Some("geometry") => geometries.push(id),
221 Some("constraint") => constraints.push(id),
222 _ => {}
223 }
224 }
225 (points, geometries, constraints)
226 }
227
228 pub fn point_world(&self, id: &Value) -> Option<[f64; 3]> {
232 self.doc.point(id).map(|p| [p.x, p.y, p.z])
233 }
234
235 pub fn sample_geometry(&self, geometry: &Sketch3dGeometry, world_per_pixel: f64) -> Option<Vec<[f64; 3]>> {
239 let positions = self.doc.geometry_positions(geometry)?;
240 let out: Vec<[f64; 3]> = match (geometry.geom_type.as_str(), positions.len()) {
241 ("line", n) if n >= 2 => vec![arr(positions[0]), arr(positions[1])],
242 ("arc", n) if n >= 3 => sample_arc(positions[0], positions[1], positions[2])?,
243 ("bezier", n) if n >= 4 && (n - 1) % 3 == 0 => sample_bezier(&positions),
244 ("plane", n) if n >= 2 => {
245 let frame = plane_frame_of(positions[0], positions[1])?;
246 plane_square(&frame, REF_PLANE_HALF_PX * world_per_pixel)
247 }
248 _ => return None,
249 };
250 (out.len() >= 2).then_some(out)
251 }
252
253 pub fn pick_entity(&self, camera: &ViewCamera, x: f64, y: f64, radius_px: f64) -> Option<Value> {
259 let mut best_pt: Option<(f64, &Value)> = None;
260 for p in &self.doc.points {
261 let Some((sx, sy)) = project(camera, [p.x, p.y, p.z]) else { continue };
262 let d = (sx - x).hypot(sy - y);
263 if d <= radius_px && best_pt.map_or(true, |(bd, _)| d < bd) {
264 best_pt = Some((d, &p.id));
265 }
266 }
267 if let Some((_, id)) = best_pt {
268 return Some(point_ref(id));
269 }
270 let wpp = camera.world_per_pixel();
271 let mut best_glyph: Option<(f64, Value)> = None;
272 for (id, centre, _) in self.constraint_glyphs(wpp) {
273 let Some((sx, sy)) = project(camera, centre) else { continue };
274 let d = (sx - x).hypot(sy - y);
275 if d <= radius_px.min(GLYPH_HALF_PX) && best_glyph.as_ref().map_or(true, |(bd, _)| d < *bd) {
276 best_glyph = Some((d, id));
277 }
278 }
279 let mut best_geo: Option<(f64, &Value)> = None;
280 for g in &self.doc.geometries {
281 let Some(poly) = self.sample_geometry(g, wpp) else { continue };
282 let screen: Vec<Option<(f64, f64)>> = poly.iter().map(|w| project(camera, *w)).collect();
283 let mut dmin = f64::INFINITY;
284 for pair in screen.windows(2) {
285 let (Some(a), Some(b)) = (pair[0], pair[1]) else { continue };
286 let d = crate::geometry2d::point_segment_distance((x, y), a, b).0;
287 if d < dmin {
288 dmin = d;
289 }
290 }
291 if dmin <= radius_px && best_geo.map_or(true, |(bd, _)| dmin < bd) {
292 best_geo = Some((dmin, &g.id));
293 }
294 }
295 match (best_glyph, best_geo) {
297 (Some((glyph, id)), Some((geo, _))) if glyph < geo => Some(entity_constraint_ref(&id)),
298 (Some((_, id)), None) => Some(entity_constraint_ref(&id)),
299 (_, geo) => geo.map(|(_, id)| geometry_ref(id)),
300 }
301 }
302
303 pub fn constraint_glyphs(&self, world_per_pixel: f64) -> Vec<(Value, [f64; 3], &'static str)> {
306 let mut stacks: std::collections::HashMap<[i64; 3], usize> = std::collections::HashMap::new();
307 let step = GLYPH_STEP_PX * world_per_pixel;
308 let mut out = Vec::new();
309 for constraint in &self.doc.constraints {
310 let (Some(id), Some(ctype)) = (constraint.id(), constraint.ctype()) else { continue };
311 if sketch3d_constraint_type(ctype).map_or(true, |t| t.dimensional) {
312 continue;
313 }
314 let ids = constraint.points();
315 let at = |i: usize| ids.get(i).and_then(|id| self.point_world(id));
316 let (anchor, shape) = match ctype {
317 "fixed" | "coincident" => (at(0), "star"),
318 "onLine" | "onPlane" | "onArc" | "onCircle" | "onCurve" | "midpoint" => (at(0), "diamond"),
319 "symmetric" => (at(0).zip(at(1)).map(|(a, b)| [0.5 * (a[0] + b[0]), 0.5 * (a[1] + b[1]), 0.5 * (a[2] + b[2])]), "diamond"),
320 _ => (at(0).zip(at(1)).map(|(a, b)| [0.5 * (a[0] + b[0]), 0.5 * (a[1] + b[1]), 0.5 * (a[2] + b[2])]), "box"),
321 };
322 let Some(anchor) = anchor else { continue };
323 let key = anchor.map(|c| (c / step.max(1e-9)).round() as i64);
324 let slot = stacks.entry(key).or_insert(0);
325 *slot += 1;
326 let distance = step * *slot as f64;
327 let centre = [
328 anchor[0] + GLYPH_OFFSET_DIR[0] * distance,
329 anchor[1] + GLYPH_OFFSET_DIR[1] * distance,
330 anchor[2] + GLYPH_OFFSET_DIR[2] * distance,
331 ];
332 out.push((id.clone(), centre, shape));
333 }
334 out
335 }
336
337 fn glyph_tessellation(&self, world_per_pixel: f64) -> SketchTessellation {
338 let mut out = SketchTessellation::default();
339 let half = GLYPH_HALF_PX * world_per_pixel;
340 let colors = &self.colors;
341 for (id, c, shape) in self.constraint_glyphs(world_per_pixel) {
342 let color = interaction_color(colors, colors.preview, self.selection.hovered.as_ref(), &self.selection.items, "constraint", &id);
343 let rgb = rgb_of(color);
344 let at = |x: f64, y: f64, z: f64| [c[0] + x * half, c[1] + y * half, c[2] + z * half];
345 let segments: Vec<([f64; 3], [f64; 3])> = match shape {
346 "star" => vec![(at(-1.0, 0.0, 0.0), at(1.0, 0.0, 0.0)), (at(0.0, -1.0, 0.0), at(0.0, 1.0, 0.0)), (at(0.0, 0.0, -1.0), at(0.0, 0.0, 1.0))],
347 "diamond" => {
348 let ring = [at(1.0, 0.0, 0.0), at(0.0, 1.0, 0.0), at(-1.0, 0.0, 0.0), at(0.0, -1.0, 0.0)];
349 (0..4)
350 .flat_map(|i| [(ring[i], ring[(i + 1) % 4]), (ring[i], at(0.0, 0.0, 1.0)), (ring[i], at(0.0, 0.0, -1.0))])
351 .collect()
352 }
353 _ => {
354 let corner = |i: usize| at(if i & 1 == 0 { -0.8 } else { 0.8 }, if i & 2 == 0 { -0.8 } else { 0.8 }, if i & 4 == 0 { -0.8 } else { 0.8 });
355 (0..8usize)
356 .flat_map(|i| [1usize, 2, 4].map(|bit| (i, i | bit)))
357 .filter(|(i, j)| i != j)
358 .map(|(i, j)| (corner(i), corner(j)))
359 .collect()
360 }
361 };
362 for (a, b) in segments {
363 push_seg(&mut out, a, b, rgb);
364 }
365 }
366 out
367 }
368
369 pub fn pick_draggable_point(&self, camera: &ViewCamera, x: f64, y: f64, radius_px: f64) -> Option<(Value, bool)> {
372 let mut best: Option<(f64, &Sketch3dPoint)> = None;
373 for p in &self.doc.points {
374 if self.diagnostics.point_movable(&p.id) != Some(true) {
375 continue;
376 }
377 let Some((sx, sy)) = project(camera, [p.x, p.y, p.z]) else { continue };
378 let d = (sx - x).hypot(sy - y);
379 if d <= radius_px && best.map_or(true, |(bd, _)| d < bd) {
380 best = Some((d, p));
381 }
382 }
383 best.map(|(_, p)| (p.id.clone(), p.fixed))
384 }
385
386 pub fn drag_points_from_ref(&self, entity_ref: &Value) -> Option<Vec<(Value, [f64; 3], bool)>> {
390 let id = entity_ref.get("id")?;
391 match entity_ref.get("kind").and_then(Value::as_str) {
392 Some("point") => {
393 if self.diagnostics.point_movable(id) != Some(true) {
394 return None;
395 }
396 let p = self.doc.point(id)?;
397 Some(vec![(p.id.clone(), [p.x, p.y, p.z], p.fixed)])
398 }
399 Some("geometry") => {
400 let g = self.doc.geometry(id)?;
401 if g.geom_type == "plane" {
402 return None;
403 }
404 let mut out: Vec<(Value, [f64; 3], bool)> = Vec::new();
405 for pid in &g.points {
406 if out.iter().any(|(seen, _, _)| refs_equal(&point_ref(seen), &point_ref(pid))) {
407 continue;
408 }
409 let Some(p) = self.doc.point(pid) else { continue };
410 if self.diagnostics.point_movable(pid) != Some(true) {
411 continue;
412 }
413 out.push((p.id.clone(), [p.x, p.y, p.z], p.fixed));
414 }
415 (!out.is_empty()).then_some(out)
416 }
417 _ => None,
418 }
419 }
420
421 pub fn tessellation(&self, world_per_pixel: f64) -> SketchTessellation {
425 let mut out = SketchTessellation::default();
426 let colors = &self.colors;
427 let hovered = self.selection.hovered.as_ref();
428 let selection = &self.selection.items;
429 let dash_len = (8.0 * world_per_pixel).max(0.02);
430 let gap_len = (6.0 * world_per_pixel).max(0.01);
431 for g in &self.doc.geometries {
432 let Some(pts) = self.sample_geometry(g, world_per_pixel) else { continue };
433 let mobility = match self.diagnostics.geometry_movable(&g.id) {
434 None => colors.geometry,
435 Some(true) => colors.movable,
436 Some(false) => colors.locked,
437 };
438 let color = interaction_color(colors, mobility, hovered, selection, "geometry", &g.id);
439 let rgb = rgb_of(color);
440 if g.construction() {
441 push_dashed(&mut out, &pts, rgb, dash_len, gap_len);
442 } else {
443 push_solid(&mut out, &pts, rgb);
444 }
445 }
446 for p in &self.doc.points {
447 let base = if p.construction {
448 colors.construction_point
449 } else {
450 match self.diagnostics.point_movable(&p.id) {
451 Some(true) => colors.movable,
452 Some(false) => colors.locked,
453 None => colors.point,
454 }
455 };
456 let color = interaction_color(colors, base, hovered, selection, "point", &p.id);
457 out.point_positions.extend_from_slice(&[p.x as f32, p.y as f32, p.z as f32]);
458 out.point_colors.extend_from_slice(&rgb_of(color));
459 }
460 out
461 }
462
463 pub fn overlay_json(&self, world_per_pixel: f64) -> String {
465 let tess = self.tessellation(world_per_pixel);
466 let glyphs = self.glyph_tessellation(world_per_pixel);
467 json!({
468 "groups": [
469 {
470 "name": OVERLAY_GLYPHS,
471 "renderOrder": 10001,
472 "lines": { "positions": glyphs.line_positions, "colors": glyphs.line_colors },
473 },
474 {
475 "name": OVERLAY_GEOMETRY,
476 "renderOrder": 10000,
477 "lines": { "positions": tess.line_positions, "colors": tess.line_colors },
478 },
479 {
480 "name": OVERLAY_POINTS,
481 "renderOrder": 10001,
482 "points": {
483 "positions": tess.point_positions,
484 "colors": tess.point_colors,
485 "size": POINT_SIZE_PX,
486 },
487 },
488 ]
489 })
490 .to_string()
491 }
492
493 pub fn preview_overlay_json(&self, pending: &[Value], hover: Option<[f64; 3]>) -> String {
497 let mut pts: Vec<[f64; 3]> = pending.iter().filter_map(|id| self.point_world(id)).collect();
498 if let Some(h) = hover {
499 pts.push(h);
500 }
501 let mut out = SketchTessellation::default();
502 let rgb = rgb_of(self.colors.preview);
503 match self.tool.as_deref() {
504 Some("arc") if pts.len() >= 3 => {
505 if let Some(arc) = sample_arc(vec(pts[0]), vec(pts[1]), vec(pts[2])) {
507 push_solid(&mut out, &arc, rgb);
508 }
509 push_solid(&mut out, &pts[..2], rgb);
510 }
511 Some("bezier") if pts.len() == 4 => {
512 let controls: Vec<Vec3> = pts.iter().map(|p| vec(*p)).collect();
513 push_solid(&mut out, &sample_bezier(&controls), rgb);
514 push_solid(&mut out, &pts, rgb);
515 }
516 _ if pts.len() >= 2 => push_solid(&mut out, &pts, rgb),
517 _ => {}
518 }
519 json!({
520 "groups": [{
521 "name": OVERLAY_PREVIEW,
522 "renderOrder": 10002,
523 "lines": { "positions": out.line_positions, "colors": out.line_colors },
524 }]
525 })
526 .to_string()
527 }
528
529 pub fn workplane_overlay_json(&self, world_per_pixel: f64) -> String {
534 let mut out = SketchTessellation::default();
535 if self.tool.is_some() {
536 let frame = self.work_plane.frame();
537 let half = WORKPLANE_HALF_PX * world_per_pixel;
538 let rgb = rgb_of(self.colors.preview);
539 let square = plane_square(&frame, half);
540 push_dashed(&mut out, &square, rgb, (8.0 * world_per_pixel).max(0.02), (6.0 * world_per_pixel).max(0.01));
541 let o = frame.origin;
542 push_solid(&mut out, &[frame.to_world(-half, 0.0), frame.to_world(half, 0.0)], rgb);
543 push_solid(&mut out, &[frame.to_world(0.0, -half), frame.to_world(0.0, half)], rgb);
544 let _ = o;
545 }
546 json!({
547 "groups": [{
548 "name": OVERLAY_WORKPLANE,
549 "renderOrder": 9999,
550 "lines": { "positions": out.line_positions, "colors": out.line_colors },
551 }]
552 })
553 .to_string()
554 }
555
556 pub fn dimension_labels(&self) -> Vec<Sketch3dDimLabel> {
561 let mut out = Vec::new();
562 for c in &self.doc.constraints {
563 let (Some(ctype), Some(id)) = (c.ctype(), c.id()) else { continue };
564 let Some(spec) = sketch3d_constraint_type(ctype) else { continue };
565 if !spec.dimensional {
566 continue;
567 }
568 let pts: Vec<[f64; 3]> = c.points().iter().filter_map(|pid| self.point_world(pid)).collect();
569 if pts.len() < spec.arity {
570 continue;
571 }
572 let (world, mode) = match ctype {
573 "distance" => (mid(pts[0], pts[1]), "distance"),
574 "angle" => (mid(mid(pts[0], pts[1]), mid(pts[2], pts[3])), "angle"),
575 _ => (pts[0], "distance"),
576 };
577 let value = c.value();
578 let text = match (mode, value) {
579 ("angle", Some(v)) => format!("{v:.2}°"),
580 (_, Some(v)) => format!("{v:.3}"),
581 _ => "?".to_string(),
582 };
583 out.push(Sketch3dDimLabel {
584 id: id.clone(),
585 text,
586 world,
587 value,
588 value_expr: c.raw.get("valueExpr").and_then(Value::as_str).map(String::from),
589 mode,
590 conflicting: self.diagnostics.constraint_conflicting(id),
591 });
592 }
593 out
594 }
595
596 pub fn applicable_constraints(&self) -> Vec<ApplicableConstraint> {
601 let (points, geometries, _) = self.selection_by_kind();
602 let geos: Vec<&Sketch3dGeometry> = geometries.iter().filter_map(|id| self.doc.geometry(id)).collect();
603 let lines: Vec<&Sketch3dGeometry> = geos.iter().copied().filter(|g| g.geom_type == "line" && g.points.len() >= 2).collect();
604 let arcs: Vec<&Sketch3dGeometry> = geos.iter().copied().filter(|g| g.geom_type == "arc" && g.points.len() >= 3).collect();
605 let planes: Vec<&Sketch3dGeometry> = geos.iter().copied().filter(|g| g.geom_type == "plane" && g.points.len() >= 2).collect();
606 let others = geos.len() - lines.len() - arcs.len() - planes.len();
607 let mut out = Vec::new();
608 let mut push = |ctype: &'static str, label: &'static str, ids: Vec<Value>| {
609 let dimensional = sketch3d_constraint_type(ctype).map_or(false, |t| t.dimensional);
610 out.push(ApplicableConstraint { ctype, label, points: ids, dimensional });
611 };
612 let line_pts = |g: &Sketch3dGeometry| vec![g.points[0].clone(), g.points[1].clone()];
613 let plane_pts = |g: &Sketch3dGeometry| vec![g.points[0].clone(), g.points[1].clone()];
614 if let ([point], [curve]) = (points.as_slice(), geos.as_slice()) {
615 if curve.geom_type == "bezier" && curve.points.len() >= 4 {
616 let mut ids = vec![point.clone()];
617 ids.extend(curve.points.iter().cloned());
618 push("onCurve", "On curve", ids);
619 return out;
620 }
621 }
622 if others > 0 {
623 return out;
624 }
625 match (points.len(), lines.len(), arcs.len(), planes.len()) {
626 (1, 0, 0, 0) => {
627 let fixed = self.doc.point(&points[0]).map_or(false, |p| p.fixed);
628 if !fixed {
629 push("fixed", "Fix", vec![points[0].clone()]);
630 }
631 }
632 (2, 0, 0, 0) => {
633 push("coincident", "Coincident", vec![points[0].clone(), points[1].clone()]);
634 push("distance", "Distance", vec![points[0].clone(), points[1].clone()]);
635 }
636 (0, 1, 0, 0) => {
637 let ids = line_pts(lines[0]);
638 push("distance", "Length", ids.clone());
639 push("alongX", "Along X", ids.clone());
640 push("alongY", "Along Y", ids.clone());
641 push("alongZ", "Along Z", ids);
642 }
643 (0, 2, 0, 0) => {
644 let mut ids = line_pts(lines[0]);
645 ids.extend(line_pts(lines[1]));
646 push("parallel", "Parallel", ids.clone());
647 push("perpendicular", "Perpendicular", ids.clone());
648 push("angle", "Angle", ids.clone());
649 push("equal", "Equal length", ids);
650 }
651 (1, 1, 0, 0) if lines[0].extra.get("circle").and_then(Value::as_bool) == Some(true) => {
653 let mut ids = vec![points[0].clone()];
654 ids.extend(line_pts(lines[0]));
655 push("coincident", "At centre", vec![points[0].clone(), lines[0].points[0].clone()]);
656 push("onCircle", "On circle", ids);
657 }
658 (1, 1, 0, 0) => {
659 let mut ids = vec![points[0].clone()];
660 ids.extend(line_pts(lines[0]));
661 push("onLine", "On line", ids.clone());
662 push("lineDistance", "Distance to line", ids.clone());
663 push("midpoint", "At midpoint", ids);
664 }
665 (1, 0, 1, 0) => {
666 let mut ids = vec![points[0].clone()];
667 ids.extend(arcs[0].points[..3].iter().cloned());
668 push("coincident", "At centre", vec![points[0].clone(), arcs[0].points[0].clone()]);
669 push("onArc", "On arc", ids);
670 }
671 (2, 0, 0, 1) => {
672 let mut ids = vec![points[0].clone(), points[1].clone()];
673 ids.extend(plane_pts(planes[0]));
674 push("symmetric", "Symmetric", ids);
675 }
676 (1, 0, 0, 1) => {
677 let mut ids = vec![points[0].clone()];
678 ids.extend(plane_pts(planes[0]));
679 push("onPlane", "On plane", ids.clone());
680 push("planeDistance", "Distance to plane", ids);
681 }
682 (0, 1, 0, 1) => {
683 let mut ids = line_pts(lines[0]);
684 ids.extend(plane_pts(planes[0]));
685 push("parallel", "Normal to plane", ids.clone());
686 push("perpendicular", "Parallel to plane", ids);
687 }
688 (0, 1, 1, 0) => {
689 let mut ids = line_pts(lines[0]);
690 ids.extend(arcs[0].points[..3].iter().cloned());
691 push("tangent", "Tangent", ids);
692 }
693 (0, 0, 1, 0) => {
694 let a = &arcs[0].points;
695 push("distance", "Radius", vec![a[0].clone(), a[1].clone()]);
696 }
697 (0, 0, 2, 0) => {
698 let (a, b) = (&arcs[0].points, &arcs[1].points);
699 push("equal", "Equal radius", vec![a[0].clone(), a[1].clone(), b[0].clone(), b[1].clone()]);
700 }
701 _ => {}
702 }
703 out
704 }
705
706 pub fn measure(&self, ctype: &str, ids: &[Value]) -> Option<f64> {
710 let p: Vec<Vec3> = ids.iter().filter_map(|id| self.point_world(id)).map(vec).collect();
711 match ctype {
712 "distance" if p.len() >= 2 => Some(p[1].sub(p[0]).length()),
713 "lineDistance" if p.len() >= 3 => {
714 let d = p[2].sub(p[1]).normalized().ok()?;
715 let v = p[0].sub(p[1]);
716 Some(v.sub(d.scale(d.dot(v))).length())
717 }
718 "planeDistance" if p.len() >= 3 => {
719 let n = p[2].sub(p[1]).normalized().ok()?;
720 Some(n.dot(p[0].sub(p[1])))
721 }
722 "angle" if p.len() >= 4 => {
723 let a = p[1].sub(p[0]).normalized().ok()?;
724 let b = p[3].sub(p[2]).normalized().ok()?;
725 Some(a.dot(b).clamp(-1.0, 1.0).acos().to_degrees())
726 }
727 _ => None,
728 }
729 }
730
731 pub fn lost_reference_of(&self, constraint: &Sketch3dConstraint) -> Option<&str> {
733 let key = brep_kernel::sketch_id_key;
734 self.lost_references
735 .iter()
736 .find(|(_, ids)| constraint.points().iter().any(|p| ids.iter().any(|id| key(id) == key(p))))
737 .map(|(name, _)| name.as_str())
738 }
739
740 pub fn lost_reference_messages(&self) -> Vec<String> {
742 self.lost_references
743 .iter()
744 .map(|(name, _)| {
745 let needed: Vec<String> = self
746 .doc
747 .constraints
748 .iter()
749 .filter(|c| self.lost_reference_of(c) == Some(name.as_str()))
750 .map(|c| format!("{} {}", c.ctype().unwrap_or("?"), c.id().map(brep_kernel::sketch_id_key).unwrap_or_default()))
751 .collect();
752 if needed.is_empty() {
753 format!("3D sketch: reference '{name}' no longer resolves; it keeps its last position")
754 } else {
755 format!(
756 "3D sketch: reference '{name}' no longer resolves; it keeps its last position, needed by constraint {}",
757 needed.join(", ")
758 )
759 }
760 })
761 .collect()
762 }
763
764 pub fn entity_rows_json(&self) -> Value {
767 let points: Vec<Value> = self
768 .doc
769 .points
770 .iter()
771 .map(|p| {
772 json!({
773 "id": p.id,
774 "fixed": p.fixed,
775 "construction": p.construction,
776 "externalReference": p.external_reference,
777 "movable": self.diagnostics.point_movable(&p.id),
778 "position": [p.x, p.y, p.z],
779 })
780 })
781 .collect();
782 let geometries: Vec<Value> = self
783 .doc
784 .geometries
785 .iter()
786 .map(|g| {
787 json!({
788 "id": g.id,
789 "type": g.geom_type,
790 "construction": g.construction(),
791 "movable": self.diagnostics.geometry_movable(&g.id),
792 "points": g.points,
793 })
794 })
795 .collect();
796 let constraints: Vec<Value> = self
797 .doc
798 .constraints
799 .iter()
800 .map(|c| {
801 json!({
802 "id": c.id(),
803 "type": c.ctype(),
804 "points": c.points(),
805 "value": c.value(),
806 "conflicting": c.id().map_or(false, |id| self.diagnostics.constraint_conflicting(id)),
807 "lostReference": self.lost_reference_of(c),
808 })
809 })
810 .collect();
811 json!({ "points": points, "geometries": geometries, "constraints": constraints })
812 }
813}
814
815pub fn entity_point_ref(id: &Value) -> Value {
820 point_ref(id)
821}
822
823pub fn entity_geometry_ref(id: &Value) -> Value {
824 geometry_ref(id)
825}
826
827pub fn entity_constraint_ref(id: &Value) -> Value {
828 constraint_ref(id)
829}
830
831fn arr(v: Vec3) -> [f64; 3] {
836 [v.x, v.y, v.z]
837}
838
839fn vec(a: [f64; 3]) -> Vec3 {
840 Vec3::new(a[0], a[1], a[2])
841}
842
843fn mid(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
844 [(a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5, (a[2] + b[2]) * 0.5]
845}
846
847pub fn project(camera: &ViewCamera, world: [f64; 3]) -> Option<(f64, f64)> {
849 if !camera.projectable(world) {
850 return None;
851 }
852 let (x, y, _) = camera.project(world);
853 Some((x, y))
854}
855
856pub fn sample_arc(center: Vec3, start: Vec3, end: Vec3) -> Option<Vec<[f64; 3]>> {
859 let rs = start.sub(center);
860 let re = end.sub(center);
861 let radius = rs.length();
862 if radius <= 1e-12 {
863 return None;
864 }
865 let normal = rs.cross(re);
866 if normal.length() <= 1e-12 * radius * radius {
867 return None;
868 }
869 let x_axis = rs.normalized().ok()?;
870 let y_axis = normal.normalized().ok()?.cross(x_axis).normalized().ok()?;
871 let sweep = re.dot(y_axis).atan2(re.dot(x_axis));
872 let sweep = if sweep <= 0.0 { sweep + std::f64::consts::TAU } else { sweep };
873 let r_end = re.length();
874 let mut out = Vec::with_capacity(ARC_SEGMENTS + 1);
875 for i in 0..=ARC_SEGMENTS {
876 let t = i as f64 / ARC_SEGMENTS as f64;
877 let a = sweep * t;
878 let r = radius + (r_end - radius) * t;
881 out.push(arr(center.add(x_axis.scale(r * a.cos())).add(y_axis.scale(r * a.sin()))));
882 }
883 Some(out)
884}
885
886pub fn sample_bezier(controls: &[Vec3]) -> Vec<[f64; 3]> {
888 let span_count = controls.len().saturating_sub(1) / 3;
889 let mut out = Vec::with_capacity(span_count * BEZIER_SEGMENTS_PER_SPAN + 1);
890 for span in 0..span_count {
891 let p = &controls[span * 3..span * 3 + 4];
892 let first = if span == 0 { 0 } else { 1 };
893 for i in first..=BEZIER_SEGMENTS_PER_SPAN {
894 let t = i as f64 / BEZIER_SEGMENTS_PER_SPAN as f64;
895 let u = 1.0 - t;
896 let point = p[0]
897 .scale(u * u * u)
898 .add(p[1].scale(3.0 * u * u * t))
899 .add(p[2].scale(3.0 * u * t * t))
900 .add(p[3].scale(t * t * t));
901 out.push(arr(point));
902 }
903 }
904 out
905}
906
907pub fn plane_frame_of(origin: Vec3, tip: Vec3) -> Option<PlaneFrame> {
909 let normal = tip.sub(origin);
910 if normal.length() <= 1e-12 {
911 return None;
912 }
913 Some(PlaneFrame::from_normal(arr(origin), arr(normal)))
914}
915
916fn plane_square(frame: &PlaneFrame, half: f64) -> Vec<[f64; 3]> {
918 vec![
919 frame.to_world(-half, -half),
920 frame.to_world(half, -half),
921 frame.to_world(half, half),
922 frame.to_world(-half, half),
923 frame.to_world(-half, -half),
924 ]
925}
926
927fn rgb_of(color: u32) -> [f32; 3] {
928 [
929 ((color >> 16) & 0xff) as f32 / 255.0,
930 ((color >> 8) & 0xff) as f32 / 255.0,
931 (color & 0xff) as f32 / 255.0,
932 ]
933}
934
935fn push_seg(out: &mut SketchTessellation, a: [f64; 3], b: [f64; 3], rgb: [f32; 3]) {
936 out.line_positions.extend_from_slice(&[
937 a[0] as f32, a[1] as f32, a[2] as f32, b[0] as f32, b[1] as f32, b[2] as f32,
938 ]);
939 out.line_colors.extend_from_slice(&[rgb[0], rgb[1], rgb[2], rgb[0], rgb[1], rgb[2]]);
940}
941
942fn push_solid(out: &mut SketchTessellation, pts: &[[f64; 3]], rgb: [f32; 3]) {
943 for pair in pts.windows(2) {
944 push_seg(out, pair[0], pair[1], rgb);
945 }
946}
947
948fn push_dashed(out: &mut SketchTessellation, pts: &[[f64; 3]], rgb: [f32; 3], dl: f64, gl: f64) {
949 let mut on = true;
950 let mut need = dl;
951 for pair in pts.windows(2) {
952 let (a, b) = (vec(pair[0]), vec(pair[1]));
953 let seg = b.sub(a);
954 let len = seg.length();
955 if len <= 0.0 {
956 continue;
957 }
958 let dir = seg.scale(1.0 / len);
959 let mut t = 0.0;
960 while t < len {
961 let step = need.min(len - t);
962 if on {
963 push_seg(out, arr(a.add(dir.scale(t))), arr(a.add(dir.scale(t + step))), rgb);
964 }
965 t += step;
966 need -= step;
967 if need <= 1e-12 {
968 on = !on;
969 need = if on { dl } else { gl };
970 }
971 }
972 }
973}
974