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_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: RefSelection,
146 pub tool: Option<String>,
148 pub work_plane: WorkPlane,
150 pub colors: SketchColors,
152}
153
154impl Sketch3dSession {
155 pub fn new(doc: Sketch3dDoc) -> Result<Self, String> {
157 let mut session = Self {
158 doc,
159 diagnostics: Sketch3dDiagnostics::default(),
160 selection: RefSelection::default(),
161 tool: None,
162 work_plane: WorkPlane::default(),
163 colors: SketchColors::default(),
164 };
165 session.resolve()?;
166 Ok(session)
167 }
168
169 pub fn resolve(&mut self) -> Result<(), String> {
171 let diagnostics = brep_kernel::solve_sketch3d_doc(
172 &mut self.doc,
173 &brep_kernel::Sketch3dSolveOptions::default(),
174 )?;
175 self.diagnostics = diagnostics;
176 Ok(())
177 }
178
179 pub fn is_selected(&self, entity_ref: &Value) -> bool {
182 self.selection.is_selected(entity_ref)
183 }
184
185 pub fn toggle_selection(&mut self, entity_ref: Value) {
186 self.selection.toggle(entity_ref)
187 }
188
189 pub fn clear_selection(&mut self) {
190 self.selection.clear()
191 }
192
193 pub fn set_hover(&mut self, entity_ref: Option<Value>) {
194 self.selection.hovered = entity_ref;
195 }
196
197 pub fn selection_by_kind(&self) -> (Vec<Value>, Vec<Value>, Vec<Value>) {
200 let mut points = Vec::new();
201 let mut geometries = Vec::new();
202 let mut constraints = Vec::new();
203 for r in &self.selection.items {
204 let Some(id) = r.get("id").cloned() else { continue };
205 match r.get("kind").and_then(Value::as_str) {
206 Some("point") => points.push(id),
207 Some("geometry") => geometries.push(id),
208 Some("constraint") => constraints.push(id),
209 _ => {}
210 }
211 }
212 (points, geometries, constraints)
213 }
214
215 pub fn point_world(&self, id: &Value) -> Option<[f64; 3]> {
219 self.doc.point(id).map(|p| [p.x, p.y, p.z])
220 }
221
222 pub fn sample_geometry(&self, geometry: &Sketch3dGeometry, world_per_pixel: f64) -> Option<Vec<[f64; 3]>> {
226 let positions = self.doc.geometry_positions(geometry)?;
227 let out: Vec<[f64; 3]> = match (geometry.geom_type.as_str(), positions.len()) {
228 ("line", n) if n >= 2 => vec![arr(positions[0]), arr(positions[1])],
229 ("arc", n) if n >= 3 => sample_arc(positions[0], positions[1], positions[2])?,
230 ("bezier", n) if n >= 4 && (n - 1) % 3 == 0 => sample_bezier(&positions),
231 ("plane", n) if n >= 2 => {
232 let frame = plane_frame_of(positions[0], positions[1])?;
233 plane_square(&frame, REF_PLANE_HALF_PX * world_per_pixel)
234 }
235 _ => return None,
236 };
237 (out.len() >= 2).then_some(out)
238 }
239
240 pub fn pick_entity(&self, camera: &ViewCamera, x: f64, y: f64, radius_px: f64) -> Option<Value> {
246 let mut best_pt: Option<(f64, &Value)> = None;
247 for p in &self.doc.points {
248 let Some((sx, sy)) = project(camera, [p.x, p.y, p.z]) else { continue };
249 let d = (sx - x).hypot(sy - y);
250 if d <= radius_px && best_pt.map_or(true, |(bd, _)| d < bd) {
251 best_pt = Some((d, &p.id));
252 }
253 }
254 if let Some((_, id)) = best_pt {
255 return Some(point_ref(id));
256 }
257 let wpp = camera.world_per_pixel();
258 let mut best_geo: Option<(f64, &Value)> = None;
259 for g in &self.doc.geometries {
260 let Some(poly) = self.sample_geometry(g, wpp) else { continue };
261 let screen: Vec<Option<(f64, f64)>> = poly.iter().map(|w| project(camera, *w)).collect();
262 let mut dmin = f64::INFINITY;
263 for pair in screen.windows(2) {
264 let (Some(a), Some(b)) = (pair[0], pair[1]) else { continue };
265 let d = crate::geometry2d::point_segment_distance((x, y), a, b).0;
266 if d < dmin {
267 dmin = d;
268 }
269 }
270 if dmin <= radius_px && best_geo.map_or(true, |(bd, _)| dmin < bd) {
271 best_geo = Some((dmin, &g.id));
272 }
273 }
274 best_geo.map(|(_, id)| geometry_ref(id))
275 }
276
277 pub fn pick_draggable_point(&self, camera: &ViewCamera, x: f64, y: f64, radius_px: f64) -> Option<(Value, bool)> {
280 let mut best: Option<(f64, &Sketch3dPoint)> = None;
281 for p in &self.doc.points {
282 if self.diagnostics.point_movable(&p.id) != Some(true) {
283 continue;
284 }
285 let Some((sx, sy)) = project(camera, [p.x, p.y, p.z]) else { continue };
286 let d = (sx - x).hypot(sy - y);
287 if d <= radius_px && best.map_or(true, |(bd, _)| d < bd) {
288 best = Some((d, p));
289 }
290 }
291 best.map(|(_, p)| (p.id.clone(), p.fixed))
292 }
293
294 pub fn drag_points_from_ref(&self, entity_ref: &Value) -> Option<Vec<(Value, [f64; 3], bool)>> {
298 let id = entity_ref.get("id")?;
299 match entity_ref.get("kind").and_then(Value::as_str) {
300 Some("point") => {
301 if self.diagnostics.point_movable(id) != Some(true) {
302 return None;
303 }
304 let p = self.doc.point(id)?;
305 Some(vec![(p.id.clone(), [p.x, p.y, p.z], p.fixed)])
306 }
307 Some("geometry") => {
308 let g = self.doc.geometry(id)?;
309 if g.geom_type == "plane" {
310 return None;
311 }
312 let mut out: Vec<(Value, [f64; 3], bool)> = Vec::new();
313 for pid in &g.points {
314 if out.iter().any(|(seen, _, _)| refs_equal(&point_ref(seen), &point_ref(pid))) {
315 continue;
316 }
317 let Some(p) = self.doc.point(pid) else { continue };
318 if self.diagnostics.point_movable(pid) != Some(true) {
319 continue;
320 }
321 out.push((p.id.clone(), [p.x, p.y, p.z], p.fixed));
322 }
323 (!out.is_empty()).then_some(out)
324 }
325 _ => None,
326 }
327 }
328
329 pub fn tessellation(&self, world_per_pixel: f64) -> SketchTessellation {
333 let mut out = SketchTessellation::default();
334 let colors = &self.colors;
335 let hovered = self.selection.hovered.as_ref();
336 let selection = &self.selection.items;
337 let dash_len = (8.0 * world_per_pixel).max(0.02);
338 let gap_len = (6.0 * world_per_pixel).max(0.01);
339 for g in &self.doc.geometries {
340 let Some(pts) = self.sample_geometry(g, world_per_pixel) else { continue };
341 let mobility = match self.diagnostics.geometry_movable(&g.id) {
342 None => colors.geometry,
343 Some(true) => colors.movable,
344 Some(false) => colors.locked,
345 };
346 let color = interaction_color(colors, mobility, hovered, selection, "geometry", &g.id);
347 let rgb = rgb_of(color);
348 if g.construction() {
349 push_dashed(&mut out, &pts, rgb, dash_len, gap_len);
350 } else {
351 push_solid(&mut out, &pts, rgb);
352 }
353 }
354 for p in &self.doc.points {
355 let base = if p.construction {
356 colors.construction_point
357 } else {
358 match self.diagnostics.point_movable(&p.id) {
359 Some(true) => colors.movable,
360 Some(false) => colors.locked,
361 None => colors.point,
362 }
363 };
364 let color = interaction_color(colors, base, hovered, selection, "point", &p.id);
365 out.point_positions.extend_from_slice(&[p.x as f32, p.y as f32, p.z as f32]);
366 out.point_colors.extend_from_slice(&rgb_of(color));
367 }
368 out
369 }
370
371 pub fn overlay_json(&self, world_per_pixel: f64) -> String {
373 let tess = self.tessellation(world_per_pixel);
374 json!({
375 "groups": [
376 {
377 "name": OVERLAY_GEOMETRY,
378 "renderOrder": 10000,
379 "lines": { "positions": tess.line_positions, "colors": tess.line_colors },
380 },
381 {
382 "name": OVERLAY_POINTS,
383 "renderOrder": 10001,
384 "points": {
385 "positions": tess.point_positions,
386 "colors": tess.point_colors,
387 "size": POINT_SIZE_PX,
388 },
389 },
390 ]
391 })
392 .to_string()
393 }
394
395 pub fn preview_overlay_json(&self, pending: &[Value], hover: Option<[f64; 3]>) -> String {
399 let mut pts: Vec<[f64; 3]> = pending.iter().filter_map(|id| self.point_world(id)).collect();
400 if let Some(h) = hover {
401 pts.push(h);
402 }
403 let mut out = SketchTessellation::default();
404 let rgb = rgb_of(self.colors.preview);
405 match self.tool.as_deref() {
406 Some("arc") if pts.len() >= 3 => {
407 if let Some(arc) = sample_arc(vec(pts[0]), vec(pts[1]), vec(pts[2])) {
409 push_solid(&mut out, &arc, rgb);
410 }
411 push_solid(&mut out, &pts[..2], rgb);
412 }
413 Some("bezier") if pts.len() == 4 => {
414 let controls: Vec<Vec3> = pts.iter().map(|p| vec(*p)).collect();
415 push_solid(&mut out, &sample_bezier(&controls), rgb);
416 push_solid(&mut out, &pts, rgb);
417 }
418 _ if pts.len() >= 2 => push_solid(&mut out, &pts, rgb),
419 _ => {}
420 }
421 json!({
422 "groups": [{
423 "name": OVERLAY_PREVIEW,
424 "renderOrder": 10002,
425 "lines": { "positions": out.line_positions, "colors": out.line_colors },
426 }]
427 })
428 .to_string()
429 }
430
431 pub fn workplane_overlay_json(&self, world_per_pixel: f64) -> String {
436 let mut out = SketchTessellation::default();
437 if self.tool.is_some() {
438 let frame = self.work_plane.frame();
439 let half = WORKPLANE_HALF_PX * world_per_pixel;
440 let rgb = rgb_of(self.colors.preview);
441 let square = plane_square(&frame, half);
442 push_dashed(&mut out, &square, rgb, (8.0 * world_per_pixel).max(0.02), (6.0 * world_per_pixel).max(0.01));
443 let o = frame.origin;
444 push_solid(&mut out, &[frame.to_world(-half, 0.0), frame.to_world(half, 0.0)], rgb);
445 push_solid(&mut out, &[frame.to_world(0.0, -half), frame.to_world(0.0, half)], rgb);
446 let _ = o;
447 }
448 json!({
449 "groups": [{
450 "name": OVERLAY_WORKPLANE,
451 "renderOrder": 9999,
452 "lines": { "positions": out.line_positions, "colors": out.line_colors },
453 }]
454 })
455 .to_string()
456 }
457
458 pub fn dimension_labels(&self) -> Vec<Sketch3dDimLabel> {
463 let mut out = Vec::new();
464 for c in &self.doc.constraints {
465 let (Some(ctype), Some(id)) = (c.ctype(), c.id()) else { continue };
466 let Some(spec) = sketch3d_constraint_type(ctype) else { continue };
467 if !spec.dimensional {
468 continue;
469 }
470 let pts: Vec<[f64; 3]> = c.points().iter().filter_map(|pid| self.point_world(pid)).collect();
471 if pts.len() < spec.arity {
472 continue;
473 }
474 let (world, mode) = match ctype {
475 "distance" => (mid(pts[0], pts[1]), "distance"),
476 "angle" => (mid(mid(pts[0], pts[1]), mid(pts[2], pts[3])), "angle"),
477 _ => (pts[0], "distance"),
478 };
479 let value = c.value();
480 let text = match (mode, value) {
481 ("angle", Some(v)) => format!("{v:.2}°"),
482 (_, Some(v)) => format!("{v:.3}"),
483 _ => "?".to_string(),
484 };
485 out.push(Sketch3dDimLabel {
486 id: id.clone(),
487 text,
488 world,
489 value,
490 value_expr: c.raw.get("valueExpr").and_then(Value::as_str).map(String::from),
491 mode,
492 conflicting: self.diagnostics.constraint_conflicting(id),
493 });
494 }
495 out
496 }
497
498 pub fn applicable_constraints(&self) -> Vec<ApplicableConstraint> {
503 let (points, geometries, _) = self.selection_by_kind();
504 let geos: Vec<&Sketch3dGeometry> = geometries.iter().filter_map(|id| self.doc.geometry(id)).collect();
505 let lines: Vec<&Sketch3dGeometry> = geos.iter().copied().filter(|g| g.geom_type == "line" && g.points.len() >= 2).collect();
506 let arcs: Vec<&Sketch3dGeometry> = geos.iter().copied().filter(|g| g.geom_type == "arc" && g.points.len() >= 3).collect();
507 let planes: Vec<&Sketch3dGeometry> = geos.iter().copied().filter(|g| g.geom_type == "plane" && g.points.len() >= 2).collect();
508 let others = geos.len() - lines.len() - arcs.len() - planes.len();
509 let mut out = Vec::new();
510 let mut push = |ctype: &'static str, label: &'static str, ids: Vec<Value>| {
511 let dimensional = sketch3d_constraint_type(ctype).map_or(false, |t| t.dimensional);
512 out.push(ApplicableConstraint { ctype, label, points: ids, dimensional });
513 };
514 let line_pts = |g: &Sketch3dGeometry| vec![g.points[0].clone(), g.points[1].clone()];
515 let plane_pts = |g: &Sketch3dGeometry| vec![g.points[0].clone(), g.points[1].clone()];
516 if others > 0 {
517 return out;
518 }
519 match (points.len(), lines.len(), arcs.len(), planes.len()) {
520 (1, 0, 0, 0) => {
521 let fixed = self.doc.point(&points[0]).map_or(false, |p| p.fixed);
522 if !fixed {
523 push("fixed", "Fix", vec![points[0].clone()]);
524 }
525 }
526 (2, 0, 0, 0) => {
527 push("coincident", "Coincident", vec![points[0].clone(), points[1].clone()]);
528 push("distance", "Distance", vec![points[0].clone(), points[1].clone()]);
529 }
530 (0, 1, 0, 0) => {
531 let ids = line_pts(lines[0]);
532 push("distance", "Length", ids.clone());
533 push("alongX", "Along X", ids.clone());
534 push("alongY", "Along Y", ids.clone());
535 push("alongZ", "Along Z", ids);
536 }
537 (0, 2, 0, 0) => {
538 let mut ids = line_pts(lines[0]);
539 ids.extend(line_pts(lines[1]));
540 push("parallel", "Parallel", ids.clone());
541 push("perpendicular", "Perpendicular", ids.clone());
542 push("angle", "Angle", ids.clone());
543 push("equal", "Equal length", ids);
544 }
545 (1, 1, 0, 0) => {
546 let mut ids = vec![points[0].clone()];
547 ids.extend(line_pts(lines[0]));
548 push("onLine", "On line", ids.clone());
549 push("lineDistance", "Distance to line", ids);
550 }
551 (1, 0, 0, 1) => {
552 let mut ids = vec![points[0].clone()];
553 ids.extend(plane_pts(planes[0]));
554 push("onPlane", "On plane", ids.clone());
555 push("planeDistance", "Distance to plane", ids);
556 }
557 (0, 1, 0, 1) => {
558 let mut ids = line_pts(lines[0]);
559 ids.extend(plane_pts(planes[0]));
560 push("parallel", "Normal to plane", ids.clone());
561 push("perpendicular", "Parallel to plane", ids);
562 }
563 (0, 1, 1, 0) => {
564 let mut ids = line_pts(lines[0]);
565 ids.extend(arcs[0].points[..3].iter().cloned());
566 push("tangent", "Tangent", ids);
567 }
568 (0, 0, 1, 0) => {
569 let a = &arcs[0].points;
570 push("distance", "Radius", vec![a[0].clone(), a[1].clone()]);
571 }
572 (0, 0, 2, 0) => {
573 let (a, b) = (&arcs[0].points, &arcs[1].points);
574 push("equal", "Equal radius", vec![a[0].clone(), a[1].clone(), b[0].clone(), b[1].clone()]);
575 }
576 _ => {}
577 }
578 out
579 }
580
581 pub fn measure(&self, ctype: &str, ids: &[Value]) -> Option<f64> {
585 let p: Vec<Vec3> = ids.iter().filter_map(|id| self.point_world(id)).map(vec).collect();
586 match ctype {
587 "distance" if p.len() >= 2 => Some(p[1].sub(p[0]).length()),
588 "lineDistance" if p.len() >= 3 => {
589 let d = p[2].sub(p[1]).normalized().ok()?;
590 let v = p[0].sub(p[1]);
591 Some(v.sub(d.scale(d.dot(v))).length())
592 }
593 "planeDistance" if p.len() >= 3 => {
594 let n = p[2].sub(p[1]).normalized().ok()?;
595 Some(n.dot(p[0].sub(p[1])))
596 }
597 "angle" if p.len() >= 4 => {
598 let a = p[1].sub(p[0]).normalized().ok()?;
599 let b = p[3].sub(p[2]).normalized().ok()?;
600 Some(a.dot(b).clamp(-1.0, 1.0).acos().to_degrees())
601 }
602 _ => None,
603 }
604 }
605
606 pub fn entity_rows_json(&self) -> Value {
609 let points: Vec<Value> = self
610 .doc
611 .points
612 .iter()
613 .map(|p| {
614 json!({
615 "id": p.id,
616 "fixed": p.fixed,
617 "construction": p.construction,
618 "externalReference": p.external_reference,
619 "movable": self.diagnostics.point_movable(&p.id),
620 "position": [p.x, p.y, p.z],
621 })
622 })
623 .collect();
624 let geometries: Vec<Value> = self
625 .doc
626 .geometries
627 .iter()
628 .map(|g| {
629 json!({
630 "id": g.id,
631 "type": g.geom_type,
632 "construction": g.construction(),
633 "movable": self.diagnostics.geometry_movable(&g.id),
634 "points": g.points,
635 })
636 })
637 .collect();
638 let constraints: Vec<Value> = self
639 .doc
640 .constraints
641 .iter()
642 .map(|c| {
643 json!({
644 "id": c.id(),
645 "type": c.ctype(),
646 "points": c.points(),
647 "value": c.value(),
648 "conflicting": c.id().map_or(false, |id| self.diagnostics.constraint_conflicting(id)),
649 })
650 })
651 .collect();
652 json!({ "points": points, "geometries": geometries, "constraints": constraints })
653 }
654}
655
656pub fn entity_point_ref(id: &Value) -> Value {
661 point_ref(id)
662}
663
664pub fn entity_geometry_ref(id: &Value) -> Value {
665 geometry_ref(id)
666}
667
668pub fn entity_constraint_ref(id: &Value) -> Value {
669 constraint_ref(id)
670}
671
672fn arr(v: Vec3) -> [f64; 3] {
677 [v.x, v.y, v.z]
678}
679
680fn vec(a: [f64; 3]) -> Vec3 {
681 Vec3::new(a[0], a[1], a[2])
682}
683
684fn mid(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
685 [(a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5, (a[2] + b[2]) * 0.5]
686}
687
688pub fn project(camera: &ViewCamera, world: [f64; 3]) -> Option<(f64, f64)> {
690 if !camera.projectable(world) {
691 return None;
692 }
693 let (x, y, _) = camera.project(world);
694 Some((x, y))
695}
696
697pub fn sample_arc(center: Vec3, start: Vec3, end: Vec3) -> Option<Vec<[f64; 3]>> {
700 let rs = start.sub(center);
701 let re = end.sub(center);
702 let radius = rs.length();
703 if radius <= 1e-12 {
704 return None;
705 }
706 let normal = rs.cross(re);
707 if normal.length() <= 1e-12 * radius * radius {
708 return None;
709 }
710 let x_axis = rs.normalized().ok()?;
711 let y_axis = normal.normalized().ok()?.cross(x_axis).normalized().ok()?;
712 let sweep = re.dot(y_axis).atan2(re.dot(x_axis));
713 let sweep = if sweep <= 0.0 { sweep + std::f64::consts::TAU } else { sweep };
714 let r_end = re.length();
715 let mut out = Vec::with_capacity(ARC_SEGMENTS + 1);
716 for i in 0..=ARC_SEGMENTS {
717 let t = i as f64 / ARC_SEGMENTS as f64;
718 let a = sweep * t;
719 let r = radius + (r_end - radius) * t;
722 out.push(arr(center.add(x_axis.scale(r * a.cos())).add(y_axis.scale(r * a.sin()))));
723 }
724 Some(out)
725}
726
727pub fn sample_bezier(controls: &[Vec3]) -> Vec<[f64; 3]> {
729 let span_count = controls.len().saturating_sub(1) / 3;
730 let mut out = Vec::with_capacity(span_count * BEZIER_SEGMENTS_PER_SPAN + 1);
731 for span in 0..span_count {
732 let p = &controls[span * 3..span * 3 + 4];
733 let first = if span == 0 { 0 } else { 1 };
734 for i in first..=BEZIER_SEGMENTS_PER_SPAN {
735 let t = i as f64 / BEZIER_SEGMENTS_PER_SPAN as f64;
736 let u = 1.0 - t;
737 let point = p[0]
738 .scale(u * u * u)
739 .add(p[1].scale(3.0 * u * u * t))
740 .add(p[2].scale(3.0 * u * t * t))
741 .add(p[3].scale(t * t * t));
742 out.push(arr(point));
743 }
744 }
745 out
746}
747
748pub fn plane_frame_of(origin: Vec3, tip: Vec3) -> Option<PlaneFrame> {
750 let normal = tip.sub(origin);
751 if normal.length() <= 1e-12 {
752 return None;
753 }
754 Some(PlaneFrame::from_normal(arr(origin), arr(normal)))
755}
756
757fn plane_square(frame: &PlaneFrame, half: f64) -> Vec<[f64; 3]> {
759 vec![
760 frame.to_world(-half, -half),
761 frame.to_world(half, -half),
762 frame.to_world(half, half),
763 frame.to_world(-half, half),
764 frame.to_world(-half, -half),
765 ]
766}
767
768fn rgb_of(color: u32) -> [f32; 3] {
769 [
770 ((color >> 16) & 0xff) as f32 / 255.0,
771 ((color >> 8) & 0xff) as f32 / 255.0,
772 (color & 0xff) as f32 / 255.0,
773 ]
774}
775
776fn push_seg(out: &mut SketchTessellation, a: [f64; 3], b: [f64; 3], rgb: [f32; 3]) {
777 out.line_positions.extend_from_slice(&[
778 a[0] as f32, a[1] as f32, a[2] as f32, b[0] as f32, b[1] as f32, b[2] as f32,
779 ]);
780 out.line_colors.extend_from_slice(&[rgb[0], rgb[1], rgb[2], rgb[0], rgb[1], rgb[2]]);
781}
782
783fn push_solid(out: &mut SketchTessellation, pts: &[[f64; 3]], rgb: [f32; 3]) {
784 for pair in pts.windows(2) {
785 push_seg(out, pair[0], pair[1], rgb);
786 }
787}
788
789fn push_dashed(out: &mut SketchTessellation, pts: &[[f64; 3]], rgb: [f32; 3], dl: f64, gl: f64) {
790 let mut on = true;
791 let mut need = dl;
792 for pair in pts.windows(2) {
793 let (a, b) = (vec(pair[0]), vec(pair[1]));
794 let seg = b.sub(a);
795 let len = seg.length();
796 if len <= 0.0 {
797 continue;
798 }
799 let dir = seg.scale(1.0 / len);
800 let mut t = 0.0;
801 while t < len {
802 let step = need.min(len - t);
803 if on {
804 push_seg(out, arr(a.add(dir.scale(t))), arr(a.add(dir.scale(t + step))), rgb);
805 }
806 t += step;
807 need -= step;
808 if need <= 1e-12 {
809 on = !on;
810 need = if on { dl } else { gl };
811 }
812 }
813 }
814}
815