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};
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_GROUPS: &[&str] =
40 &[OVERLAY_GEOMETRY, OVERLAY_POINTS, OVERLAY_PREVIEW, OVERLAY_WORKPLANE];
41
42const ARC_SEGMENTS: usize = 48;
44const BEZIER_SEGMENTS_PER_SPAN: usize = 24;
45const WORKPLANE_HALF_PX: f64 = 90.0;
47const REF_PLANE_HALF_PX: f64 = 40.0;
49
50pub const TOOLS: &[&str] = &["select", "point", "line", "arc", "bezier", "pickRef"];
53
54pub const WORK_PLANES: &[&str] = &["XY", "YZ", "XZ"];
56
57#[derive(Clone, Debug, PartialEq)]
59pub struct WorkPlane {
60 pub name: String,
62 pub anchor: [f64; 3],
64}
65
66impl WorkPlane {
67 pub fn new(name: &str, anchor: [f64; 3]) -> Option<Self> {
68 if !WORK_PLANES.contains(&name) {
69 return None;
70 }
71 Some(Self {
72 name: name.to_string(),
73 anchor,
74 })
75 }
76
77 pub fn frame(&self) -> PlaneFrame {
79 let mut frame = match self.name.as_str() {
80 "YZ" => PlaneFrame::yz(),
81 "XZ" => PlaneFrame::xz(),
82 _ => PlaneFrame::xy(),
83 };
84 frame.origin = self.anchor;
85 frame
86 }
87
88 pub fn next_name(&self) -> &'static str {
90 match self.name.as_str() {
91 "XY" => "YZ",
92 "YZ" => "XZ",
93 _ => "XY",
94 }
95 }
96}
97
98impl Default for WorkPlane {
99 fn default() -> Self {
100 Self {
101 name: "XY".into(),
102 anchor: [0.0; 3],
103 }
104 }
105}
106
107#[derive(Clone, Debug, serde::Serialize)]
111pub struct Sketch3dDimLabel {
112 pub id: Value,
113 pub text: String,
114 pub world: [f64; 3],
115 pub value: Option<f64>,
116 #[serde(rename = "valueExpr")]
117 pub value_expr: Option<String>,
118 pub mode: &'static str,
119 pub conflicting: bool,
120}
121
122#[derive(Clone, Debug, PartialEq, serde::Serialize)]
125pub struct ApplicableConstraint {
126 #[serde(rename = "type")]
128 pub ctype: &'static str,
129 pub label: &'static str,
131 pub points: Vec<Value>,
133 pub dimensional: bool,
135}
136
137pub struct Sketch3dSession {
139 pub doc: Sketch3dDoc,
141 pub diagnostics: Sketch3dDiagnostics,
143 pub selection: Vec<Value>,
145 pub hovered: Option<Value>,
147 pub tool: Option<String>,
149 pub work_plane: WorkPlane,
151 pub colors: SketchColors,
153}
154
155impl Sketch3dSession {
156 pub fn new(doc: Sketch3dDoc) -> Result<Self, String> {
158 let mut session = Self {
159 doc,
160 diagnostics: Sketch3dDiagnostics::default(),
161 selection: Vec::new(),
162 hovered: None,
163 tool: None,
164 work_plane: WorkPlane::default(),
165 colors: SketchColors::default(),
166 };
167 session.resolve()?;
168 Ok(session)
169 }
170
171 pub fn resolve(&mut self) -> Result<(), String> {
173 let diagnostics = brep_kernel::solve_sketch3d_doc(
174 &mut self.doc,
175 &brep_kernel::Sketch3dSolveOptions::default(),
176 )?;
177 self.diagnostics = diagnostics;
178 Ok(())
179 }
180
181 pub fn is_selected(&self, entity_ref: &Value) -> bool {
184 self.selection.iter().any(|r| refs_equal(r, entity_ref))
185 }
186
187 pub fn toggle_selection(&mut self, entity_ref: Value) {
188 if let Some(pos) = self.selection.iter().position(|r| refs_equal(r, &entity_ref)) {
189 self.selection.remove(pos);
190 } else {
191 self.selection.push(entity_ref);
192 }
193 }
194
195 pub fn clear_selection(&mut self) {
196 self.selection.clear();
197 }
198
199 pub fn set_hover(&mut self, entity_ref: Option<Value>) {
200 self.hovered = entity_ref;
201 }
202
203 pub fn selection_by_kind(&self) -> (Vec<Value>, Vec<Value>, Vec<Value>) {
206 let mut points = Vec::new();
207 let mut geometries = Vec::new();
208 let mut constraints = Vec::new();
209 for r in &self.selection {
210 let Some(id) = r.get("id").cloned() else { continue };
211 match r.get("kind").and_then(Value::as_str) {
212 Some("point") => points.push(id),
213 Some("geometry") => geometries.push(id),
214 Some("constraint") => constraints.push(id),
215 _ => {}
216 }
217 }
218 (points, geometries, constraints)
219 }
220
221 pub fn point_world(&self, id: &Value) -> Option<[f64; 3]> {
225 self.doc.point(id).map(|p| [p.x, p.y, p.z])
226 }
227
228 pub fn sample_geometry(&self, geometry: &Sketch3dGeometry, world_per_pixel: f64) -> Option<Vec<[f64; 3]>> {
232 let positions = self.doc.geometry_positions(geometry)?;
233 let out: Vec<[f64; 3]> = match (geometry.geom_type.as_str(), positions.len()) {
234 ("line", n) if n >= 2 => vec![arr(positions[0]), arr(positions[1])],
235 ("arc", n) if n >= 3 => sample_arc(positions[0], positions[1], positions[2])?,
236 ("bezier", n) if n >= 4 && (n - 1) % 3 == 0 => sample_bezier(&positions),
237 ("plane", n) if n >= 2 => {
238 let frame = plane_frame_of(positions[0], positions[1])?;
239 plane_square(&frame, REF_PLANE_HALF_PX * world_per_pixel)
240 }
241 _ => return None,
242 };
243 (out.len() >= 2).then_some(out)
244 }
245
246 pub fn pick_entity(&self, camera: &ViewCamera, x: f64, y: f64, radius_px: f64) -> Option<Value> {
252 let mut best_pt: Option<(f64, &Value)> = None;
253 for p in &self.doc.points {
254 let Some((sx, sy)) = project(camera, [p.x, p.y, p.z]) else { continue };
255 let d = (sx - x).hypot(sy - y);
256 if d <= radius_px && best_pt.map_or(true, |(bd, _)| d < bd) {
257 best_pt = Some((d, &p.id));
258 }
259 }
260 if let Some((_, id)) = best_pt {
261 return Some(point_ref(id));
262 }
263 let wpp = camera.world_per_pixel();
264 let mut best_geo: Option<(f64, &Value)> = None;
265 for g in &self.doc.geometries {
266 let Some(poly) = self.sample_geometry(g, wpp) else { continue };
267 let screen: Vec<Option<(f64, f64)>> = poly.iter().map(|w| project(camera, *w)).collect();
268 let mut dmin = f64::INFINITY;
269 for pair in screen.windows(2) {
270 let (Some(a), Some(b)) = (pair[0], pair[1]) else { continue };
271 let d = crate::geometry2d::point_segment_distance((x, y), a, b).0;
272 if d < dmin {
273 dmin = d;
274 }
275 }
276 if dmin <= radius_px && best_geo.map_or(true, |(bd, _)| dmin < bd) {
277 best_geo = Some((dmin, &g.id));
278 }
279 }
280 best_geo.map(|(_, id)| geometry_ref(id))
281 }
282
283 pub fn pick_draggable_point(&self, camera: &ViewCamera, x: f64, y: f64, radius_px: f64) -> Option<(Value, bool)> {
286 let mut best: Option<(f64, &Sketch3dPoint)> = None;
287 for p in &self.doc.points {
288 if self.diagnostics.point_movable(&p.id) != Some(true) {
289 continue;
290 }
291 let Some((sx, sy)) = project(camera, [p.x, p.y, p.z]) else { continue };
292 let d = (sx - x).hypot(sy - y);
293 if d <= radius_px && best.map_or(true, |(bd, _)| d < bd) {
294 best = Some((d, p));
295 }
296 }
297 best.map(|(_, p)| (p.id.clone(), p.fixed))
298 }
299
300 pub fn drag_points_from_ref(&self, entity_ref: &Value) -> Option<Vec<(Value, [f64; 3], bool)>> {
304 let id = entity_ref.get("id")?;
305 match entity_ref.get("kind").and_then(Value::as_str) {
306 Some("point") => {
307 if self.diagnostics.point_movable(id) != Some(true) {
308 return None;
309 }
310 let p = self.doc.point(id)?;
311 Some(vec![(p.id.clone(), [p.x, p.y, p.z], p.fixed)])
312 }
313 Some("geometry") => {
314 let g = self.doc.geometry(id)?;
315 if g.geom_type == "plane" {
316 return None;
317 }
318 let mut out: Vec<(Value, [f64; 3], bool)> = Vec::new();
319 for pid in &g.points {
320 if out.iter().any(|(seen, _, _)| refs_equal(&point_ref(seen), &point_ref(pid))) {
321 continue;
322 }
323 let Some(p) = self.doc.point(pid) else { continue };
324 if self.diagnostics.point_movable(pid) != Some(true) {
325 continue;
326 }
327 out.push((p.id.clone(), [p.x, p.y, p.z], p.fixed));
328 }
329 (!out.is_empty()).then_some(out)
330 }
331 _ => None,
332 }
333 }
334
335 pub fn tessellation(&self, world_per_pixel: f64) -> SketchTessellation {
339 let mut out = SketchTessellation::default();
340 let colors = &self.colors;
341 let hovered = self.hovered.as_ref();
342 let selection = &self.selection;
343 let dash_len = (8.0 * world_per_pixel).max(0.02);
344 let gap_len = (6.0 * world_per_pixel).max(0.01);
345 for g in &self.doc.geometries {
346 let Some(pts) = self.sample_geometry(g, world_per_pixel) else { continue };
347 let mobility = match self.diagnostics.geometry_movable(&g.id) {
348 None => colors.geometry,
349 Some(true) => colors.movable,
350 Some(false) => colors.locked,
351 };
352 let color = interaction_color(colors, mobility, hovered, selection, "geometry", &g.id);
353 let rgb = rgb_of(color);
354 if g.construction() {
355 push_dashed(&mut out, &pts, rgb, dash_len, gap_len);
356 } else {
357 push_solid(&mut out, &pts, rgb);
358 }
359 }
360 for p in &self.doc.points {
361 let base = if p.construction {
362 colors.construction_point
363 } else {
364 match self.diagnostics.point_movable(&p.id) {
365 Some(true) => colors.movable,
366 Some(false) => colors.locked,
367 None => colors.point,
368 }
369 };
370 let color = interaction_color(colors, base, hovered, selection, "point", &p.id);
371 out.point_positions.extend_from_slice(&[p.x as f32, p.y as f32, p.z as f32]);
372 out.point_colors.extend_from_slice(&rgb_of(color));
373 }
374 out
375 }
376
377 pub fn overlay_json(&self, world_per_pixel: f64) -> String {
379 let tess = self.tessellation(world_per_pixel);
380 json!({
381 "groups": [
382 {
383 "name": OVERLAY_GEOMETRY,
384 "renderOrder": 10000,
385 "lines": { "positions": tess.line_positions, "colors": tess.line_colors },
386 },
387 {
388 "name": OVERLAY_POINTS,
389 "renderOrder": 10001,
390 "points": {
391 "positions": tess.point_positions,
392 "colors": tess.point_colors,
393 "size": POINT_SIZE_PX,
394 },
395 },
396 ]
397 })
398 .to_string()
399 }
400
401 pub fn preview_overlay_json(&self, pending: &[Value], hover: Option<[f64; 3]>) -> String {
405 let mut pts: Vec<[f64; 3]> = pending.iter().filter_map(|id| self.point_world(id)).collect();
406 if let Some(h) = hover {
407 pts.push(h);
408 }
409 let mut out = SketchTessellation::default();
410 let rgb = rgb_of(self.colors.preview);
411 match self.tool.as_deref() {
412 Some("arc") if pts.len() >= 3 => {
413 if let Some(arc) = sample_arc(vec(pts[0]), vec(pts[1]), vec(pts[2])) {
415 push_solid(&mut out, &arc, rgb);
416 }
417 push_solid(&mut out, &pts[..2], rgb);
418 }
419 Some("bezier") if pts.len() == 4 => {
420 let controls: Vec<Vec3> = pts.iter().map(|p| vec(*p)).collect();
421 push_solid(&mut out, &sample_bezier(&controls), rgb);
422 push_solid(&mut out, &pts, rgb);
423 }
424 _ if pts.len() >= 2 => push_solid(&mut out, &pts, rgb),
425 _ => {}
426 }
427 json!({
428 "groups": [{
429 "name": OVERLAY_PREVIEW,
430 "renderOrder": 10002,
431 "lines": { "positions": out.line_positions, "colors": out.line_colors },
432 }]
433 })
434 .to_string()
435 }
436
437 pub fn workplane_overlay_json(&self, world_per_pixel: f64) -> String {
442 let mut out = SketchTessellation::default();
443 if self.tool.is_some() {
444 let frame = self.work_plane.frame();
445 let half = WORKPLANE_HALF_PX * world_per_pixel;
446 let rgb = rgb_of(self.colors.preview);
447 let square = plane_square(&frame, half);
448 push_dashed(&mut out, &square, rgb, (8.0 * world_per_pixel).max(0.02), (6.0 * world_per_pixel).max(0.01));
449 let o = frame.origin;
450 push_solid(&mut out, &[frame.to_world(-half, 0.0), frame.to_world(half, 0.0)], rgb);
451 push_solid(&mut out, &[frame.to_world(0.0, -half), frame.to_world(0.0, half)], rgb);
452 let _ = o;
453 }
454 json!({
455 "groups": [{
456 "name": OVERLAY_WORKPLANE,
457 "renderOrder": 9999,
458 "lines": { "positions": out.line_positions, "colors": out.line_colors },
459 }]
460 })
461 .to_string()
462 }
463
464 pub fn dimension_labels(&self) -> Vec<Sketch3dDimLabel> {
469 let mut out = Vec::new();
470 for c in &self.doc.constraints {
471 let (Some(ctype), Some(id)) = (c.ctype(), c.id()) else { continue };
472 let Some(spec) = sketch3d_constraint_type(ctype) else { continue };
473 if !spec.dimensional {
474 continue;
475 }
476 let pts: Vec<[f64; 3]> = c.points().iter().filter_map(|pid| self.point_world(pid)).collect();
477 if pts.len() < spec.arity {
478 continue;
479 }
480 let (world, mode) = match ctype {
481 "distance" => (mid(pts[0], pts[1]), "distance"),
482 "angle" => (mid(mid(pts[0], pts[1]), mid(pts[2], pts[3])), "angle"),
483 _ => (pts[0], "distance"),
484 };
485 let value = c.value();
486 let text = match (mode, value) {
487 ("angle", Some(v)) => format!("{v:.2}°"),
488 (_, Some(v)) => format!("{v:.3}"),
489 _ => "?".to_string(),
490 };
491 out.push(Sketch3dDimLabel {
492 id: id.clone(),
493 text,
494 world,
495 value,
496 value_expr: c.raw.get("valueExpr").and_then(Value::as_str).map(String::from),
497 mode,
498 conflicting: self.diagnostics.constraint_conflicting(id),
499 });
500 }
501 out
502 }
503
504 pub fn applicable_constraints(&self) -> Vec<ApplicableConstraint> {
509 let (points, geometries, _) = self.selection_by_kind();
510 let geos: Vec<&Sketch3dGeometry> = geometries.iter().filter_map(|id| self.doc.geometry(id)).collect();
511 let lines: Vec<&Sketch3dGeometry> = geos.iter().copied().filter(|g| g.geom_type == "line" && g.points.len() >= 2).collect();
512 let arcs: Vec<&Sketch3dGeometry> = geos.iter().copied().filter(|g| g.geom_type == "arc" && g.points.len() >= 3).collect();
513 let planes: Vec<&Sketch3dGeometry> = geos.iter().copied().filter(|g| g.geom_type == "plane" && g.points.len() >= 2).collect();
514 let others = geos.len() - lines.len() - arcs.len() - planes.len();
515 let mut out = Vec::new();
516 let mut push = |ctype: &'static str, label: &'static str, ids: Vec<Value>| {
517 let dimensional = sketch3d_constraint_type(ctype).map_or(false, |t| t.dimensional);
518 out.push(ApplicableConstraint { ctype, label, points: ids, dimensional });
519 };
520 let line_pts = |g: &Sketch3dGeometry| vec![g.points[0].clone(), g.points[1].clone()];
521 let plane_pts = |g: &Sketch3dGeometry| vec![g.points[0].clone(), g.points[1].clone()];
522 if others > 0 {
523 return out;
524 }
525 match (points.len(), lines.len(), arcs.len(), planes.len()) {
526 (1, 0, 0, 0) => {
527 let fixed = self.doc.point(&points[0]).map_or(false, |p| p.fixed);
528 if !fixed {
529 push("fixed", "Fix", vec![points[0].clone()]);
530 }
531 }
532 (2, 0, 0, 0) => {
533 push("coincident", "Coincident", vec![points[0].clone(), points[1].clone()]);
534 push("distance", "Distance", vec![points[0].clone(), points[1].clone()]);
535 }
536 (0, 1, 0, 0) => {
537 let ids = line_pts(lines[0]);
538 push("distance", "Length", ids.clone());
539 push("alongX", "Along X", ids.clone());
540 push("alongY", "Along Y", ids.clone());
541 push("alongZ", "Along Z", ids);
542 }
543 (0, 2, 0, 0) => {
544 let mut ids = line_pts(lines[0]);
545 ids.extend(line_pts(lines[1]));
546 push("parallel", "Parallel", ids.clone());
547 push("perpendicular", "Perpendicular", ids.clone());
548 push("angle", "Angle", ids.clone());
549 push("equal", "Equal length", ids);
550 }
551 (1, 1, 0, 0) => {
552 let mut ids = vec![points[0].clone()];
553 ids.extend(line_pts(lines[0]));
554 push("onLine", "On line", ids.clone());
555 push("lineDistance", "Distance to line", ids);
556 }
557 (1, 0, 0, 1) => {
558 let mut ids = vec![points[0].clone()];
559 ids.extend(plane_pts(planes[0]));
560 push("onPlane", "On plane", ids.clone());
561 push("planeDistance", "Distance to plane", ids);
562 }
563 (0, 1, 0, 1) => {
564 let mut ids = line_pts(lines[0]);
565 ids.extend(plane_pts(planes[0]));
566 push("parallel", "Normal to plane", ids.clone());
567 push("perpendicular", "Parallel to plane", ids);
568 }
569 (0, 1, 1, 0) => {
570 let mut ids = line_pts(lines[0]);
571 ids.extend(arcs[0].points[..3].iter().cloned());
572 push("tangent", "Tangent", ids);
573 }
574 (0, 0, 1, 0) => {
575 let a = &arcs[0].points;
576 push("distance", "Radius", vec![a[0].clone(), a[1].clone()]);
577 }
578 (0, 0, 2, 0) => {
579 let (a, b) = (&arcs[0].points, &arcs[1].points);
580 push("equal", "Equal radius", vec![a[0].clone(), a[1].clone(), b[0].clone(), b[1].clone()]);
581 }
582 _ => {}
583 }
584 out
585 }
586
587 pub fn measure(&self, ctype: &str, ids: &[Value]) -> Option<f64> {
591 let p: Vec<Vec3> = ids.iter().filter_map(|id| self.point_world(id)).map(vec).collect();
592 match ctype {
593 "distance" if p.len() >= 2 => Some(p[1].sub(p[0]).length()),
594 "lineDistance" if p.len() >= 3 => {
595 let d = p[2].sub(p[1]).normalized().ok()?;
596 let v = p[0].sub(p[1]);
597 Some(v.sub(d.scale(d.dot(v))).length())
598 }
599 "planeDistance" if p.len() >= 3 => {
600 let n = p[2].sub(p[1]).normalized().ok()?;
601 Some(n.dot(p[0].sub(p[1])))
602 }
603 "angle" if p.len() >= 4 => {
604 let a = p[1].sub(p[0]).normalized().ok()?;
605 let b = p[3].sub(p[2]).normalized().ok()?;
606 Some(a.dot(b).clamp(-1.0, 1.0).acos().to_degrees())
607 }
608 _ => None,
609 }
610 }
611
612 pub fn entity_rows_json(&self) -> Value {
615 let points: Vec<Value> = self
616 .doc
617 .points
618 .iter()
619 .map(|p| {
620 json!({
621 "id": p.id,
622 "fixed": p.fixed,
623 "construction": p.construction,
624 "externalReference": p.external_reference,
625 "movable": self.diagnostics.point_movable(&p.id),
626 "position": [p.x, p.y, p.z],
627 })
628 })
629 .collect();
630 let geometries: Vec<Value> = self
631 .doc
632 .geometries
633 .iter()
634 .map(|g| {
635 json!({
636 "id": g.id,
637 "type": g.geom_type,
638 "construction": g.construction(),
639 "movable": self.diagnostics.geometry_movable(&g.id),
640 "points": g.points,
641 })
642 })
643 .collect();
644 let constraints: Vec<Value> = self
645 .doc
646 .constraints
647 .iter()
648 .map(|c| {
649 json!({
650 "id": c.id(),
651 "type": c.ctype(),
652 "points": c.points(),
653 "value": c.value(),
654 "conflicting": c.id().map_or(false, |id| self.diagnostics.constraint_conflicting(id)),
655 })
656 })
657 .collect();
658 json!({ "points": points, "geometries": geometries, "constraints": constraints })
659 }
660}
661
662pub fn entity_point_ref(id: &Value) -> Value {
667 point_ref(id)
668}
669
670pub fn entity_geometry_ref(id: &Value) -> Value {
671 geometry_ref(id)
672}
673
674pub fn entity_constraint_ref(id: &Value) -> Value {
675 constraint_ref(id)
676}
677
678fn arr(v: Vec3) -> [f64; 3] {
683 [v.x, v.y, v.z]
684}
685
686fn vec(a: [f64; 3]) -> Vec3 {
687 Vec3::new(a[0], a[1], a[2])
688}
689
690fn mid(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
691 [(a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5, (a[2] + b[2]) * 0.5]
692}
693
694pub fn project(camera: &ViewCamera, world: [f64; 3]) -> Option<(f64, f64)> {
696 if !camera.projectable(world) {
697 return None;
698 }
699 let (x, y, _) = camera.project(world);
700 Some((x, y))
701}
702
703pub fn sample_arc(center: Vec3, start: Vec3, end: Vec3) -> Option<Vec<[f64; 3]>> {
706 let rs = start.sub(center);
707 let re = end.sub(center);
708 let radius = rs.length();
709 if radius <= 1e-12 {
710 return None;
711 }
712 let normal = rs.cross(re);
713 if normal.length() <= 1e-12 * radius * radius {
714 return None;
715 }
716 let x_axis = rs.normalized().ok()?;
717 let y_axis = normal.normalized().ok()?.cross(x_axis).normalized().ok()?;
718 let sweep = re.dot(y_axis).atan2(re.dot(x_axis));
719 let sweep = if sweep <= 0.0 { sweep + std::f64::consts::TAU } else { sweep };
720 let r_end = re.length();
721 let mut out = Vec::with_capacity(ARC_SEGMENTS + 1);
722 for i in 0..=ARC_SEGMENTS {
723 let t = i as f64 / ARC_SEGMENTS as f64;
724 let a = sweep * t;
725 let r = radius + (r_end - radius) * t;
728 out.push(arr(center.add(x_axis.scale(r * a.cos())).add(y_axis.scale(r * a.sin()))));
729 }
730 Some(out)
731}
732
733pub fn sample_bezier(controls: &[Vec3]) -> Vec<[f64; 3]> {
735 let span_count = controls.len().saturating_sub(1) / 3;
736 let mut out = Vec::with_capacity(span_count * BEZIER_SEGMENTS_PER_SPAN + 1);
737 for span in 0..span_count {
738 let p = &controls[span * 3..span * 3 + 4];
739 let first = if span == 0 { 0 } else { 1 };
740 for i in first..=BEZIER_SEGMENTS_PER_SPAN {
741 let t = i as f64 / BEZIER_SEGMENTS_PER_SPAN as f64;
742 let u = 1.0 - t;
743 let point = p[0]
744 .scale(u * u * u)
745 .add(p[1].scale(3.0 * u * u * t))
746 .add(p[2].scale(3.0 * u * t * t))
747 .add(p[3].scale(t * t * t));
748 out.push(arr(point));
749 }
750 }
751 out
752}
753
754pub fn plane_frame_of(origin: Vec3, tip: Vec3) -> Option<PlaneFrame> {
756 let normal = tip.sub(origin);
757 if normal.length() <= 1e-12 {
758 return None;
759 }
760 Some(PlaneFrame::from_normal(arr(origin), arr(normal)))
761}
762
763fn plane_square(frame: &PlaneFrame, half: f64) -> Vec<[f64; 3]> {
765 vec![
766 frame.to_world(-half, -half),
767 frame.to_world(half, -half),
768 frame.to_world(half, half),
769 frame.to_world(-half, half),
770 frame.to_world(-half, -half),
771 ]
772}
773
774fn rgb_of(color: u32) -> [f32; 3] {
775 [
776 ((color >> 16) & 0xff) as f32 / 255.0,
777 ((color >> 8) & 0xff) as f32 / 255.0,
778 (color & 0xff) as f32 / 255.0,
779 ]
780}
781
782fn push_seg(out: &mut SketchTessellation, a: [f64; 3], b: [f64; 3], rgb: [f32; 3]) {
783 out.line_positions.extend_from_slice(&[
784 a[0] as f32, a[1] as f32, a[2] as f32, b[0] as f32, b[1] as f32, b[2] as f32,
785 ]);
786 out.line_colors.extend_from_slice(&[rgb[0], rgb[1], rgb[2], rgb[0], rgb[1], rgb[2]]);
787}
788
789fn push_solid(out: &mut SketchTessellation, pts: &[[f64; 3]], rgb: [f32; 3]) {
790 for pair in pts.windows(2) {
791 push_seg(out, pair[0], pair[1], rgb);
792 }
793}
794
795fn push_dashed(out: &mut SketchTessellation, pts: &[[f64; 3]], rgb: [f32; 3], dl: f64, gl: f64) {
796 let mut on = true;
797 let mut need = dl;
798 for pair in pts.windows(2) {
799 let (a, b) = (vec(pair[0]), vec(pair[1]));
800 let seg = b.sub(a);
801 let len = seg.length();
802 if len <= 0.0 {
803 continue;
804 }
805 let dir = seg.scale(1.0 / len);
806 let mut t = 0.0;
807 while t < len {
808 let step = need.min(len - t);
809 if on {
810 push_seg(out, arr(a.add(dir.scale(t))), arr(a.add(dir.scale(t + step))), rgb);
811 }
812 t += step;
813 need -= step;
814 if need <= 1e-12 {
815 on = !on;
816 need = if on { dl } else { gl };
817 }
818 }
819 }
820}
821