1use std::collections::{BTreeMap, BTreeSet};
15
16use geop_core_math::{
17 geop_error::{GeopError, GeopResult},
18 plain::{self, P2, P3},
19 primitives::{DatumComponent, DatumKind, FrameAxis, Motion, Ray},
20 scalars::Scalar,
21 vector::Vector3,
22};
23use geop_core_program::placed::{INSTANCE_SEPARATOR, Located};
24
25use super::{FRAME, PartView, ViewDatum, lit::Lighting, lit::Lit};
26use crate::{
27 editing::{Pointer, Reach, Shape, Style, TRIAD, Visual},
28 editor::SceneState,
29};
30
31pub const REACH_PX: f64 = 4.0;
36
37const SUPERSAMPLE: usize = 2;
39const MARGIN: f64 = 0.06;
42const DEPTH_BIAS_PX: f64 = 3.0;
47const EDGE_HALF_WIDTH: f64 = 0.7;
49const LIT_HALF_WIDTH: f64 = 1.2;
51const AMBIENT: f32 = 0.4;
54const DIFFUSE: f32 = 0.6;
55
56const BACKGROUND: u32 = 0x1b1f26;
58const FACE: u32 = 0xb0b4b8;
59const EDGE: u32 = 0x30343b;
60const SKETCH: u32 = 0xffa040;
61const CONSTRUCTION: u32 = 0x808080;
62const ANNOTATION: u32 = 0x2a2a2a;
63const HIGHLIGHT: u32 = 0xffc94a;
64const LIT_TINT: f32 = 0.7;
67const LIT_FACE_OPACITY: f32 = 0.35;
68const DATUM: u32 = 0xb58cff;
69const AXES: [u32; 3] = [0xff5555, 0x55dd55, 0x5599ff];
71const AXIS_TINTS: [u32; 3] = [0xff8080, 0x80e080, 0x80b0ff];
72const POINT_TRIAD: f64 = 32.0 / 9.0;
74
75#[derive(Clone, Debug, PartialEq)]
78pub struct Camera {
79 pub look: Vector3,
81 pub up: Vector3,
83 pub center: Vector3,
85 pub span: f64,
87 pub width: u32,
89 pub height: u32,
90 pub near: f64,
93}
94
95#[derive(Clone, Copy, Debug)]
98struct Frame {
99 right: P3,
100 up: P3,
101 look: P3,
102 center: P3,
103 per_px: f64,
104}
105
106impl Frame {
107 fn coordinates(&self, p: P3) -> P3 {
109 let d = plain::sub(p, self.center);
110 [
111 plain::dot(d, self.right),
112 plain::dot(d, self.up),
113 plain::dot(d, self.look),
114 ]
115 }
116
117 fn on_screen(&self, d: P3) -> P2 {
119 [plain::dot(d, self.right), -plain::dot(d, self.up)]
120 }
121}
122
123fn unit(k: usize) -> P3 {
125 std::array::from_fn(|i| f64::from(u8::from(i == k)))
126}
127
128fn square_up(look: P3, up: P3) -> P3 {
132 let square = |up: P3| {
133 let up = plain::normalize(up)?;
134 let square = plain::sub(up, plain::scale(look, plain::dot(up, look)));
135 (plain::norm(square) > 1e-3).then(|| plain::normalize(square))?
136 };
137 square(up).unwrap_or_else(|| {
138 let least = (look.iter().enumerate()).fold((0, f64::INFINITY), |least, (k, x)| {
139 match x.abs() < least.1 {
140 true => (k, x.abs()),
141 false => least,
142 }
143 });
144 let axis = unit(least.0);
145 square(axis).unwrap_or(axis)
146 })
147}
148
149impl Camera {
150 pub fn fitting(
156 scene: &SceneState,
157 look: Vector3,
158 up: Vector3,
159 width: u32,
160 height: u32,
161 ) -> GeopResult<Camera> {
162 if width == 0 || height == 0 {
163 return Err(GeopError::new(format!(
164 "a picture of {width} by {height} pixels shows nothing"
165 )));
166 }
167 let look = plain::normalize(look.to_f64_array()).ok_or_else(|| {
168 GeopError::new(format!("a camera looking along {look:?} looks nowhere"))
169 })?;
170 let up = square_up(look, up.to_f64_array());
171 let right = plain::cross(look, up);
172 let frame = Frame {
173 right,
174 up,
175 look,
176 center: [0.0; 3],
177 per_px: 1.0,
178 };
179 let mut low = [f64::INFINITY; 3];
180 let mut high = [f64::NEG_INFINITY; 3];
181 let hidden = hidden_of(scene);
182 for layer in &layers(scene, None, &hidden) {
183 layer.points(|p| {
184 let c = frame.coordinates(p);
185 for ((low, high), c) in low.iter_mut().zip(&mut high).zip(c) {
186 *low = low.min(c);
187 *high = high.max(c);
188 }
189 });
190 }
191 let extent = &scene.part.extent;
192 let size = extent.size.to_f64();
193 let aspect = f64::from(height) / f64::from(width);
194 let (middle, span) = match low[0] <= high[0] {
195 true => {
196 let middle = plain::scale(plain::add(low, high), 0.5);
197 let span = (high[1] - low[1]).max((high[0] - low[0]) * aspect);
198 (middle, span / (1.0 - 2.0 * MARGIN))
199 }
200 false => {
201 let c = frame.coordinates(extent.center.to_f64_array());
202 low[2] = c[2] - size / 2.0;
203 (c, size)
204 }
205 };
206 let span = match span > 0.0 && span.is_finite() {
207 true => span,
208 false => size,
209 };
210 let center = ([right, up, look].into_iter().zip(middle)).fold([0.0; 3], |c, (axis, x)| {
211 plain::add(c, plain::scale(axis, x))
212 });
213 let v = |p: P3| Vector3::from_array(p.map(Scalar::from_f64));
214 Ok(Camera {
215 look: v(look),
216 up: v(up),
217 center: v(center),
218 span,
219 width,
220 height,
221 near: middle[2] - low[2] + span,
222 })
223 }
224
225 fn frame(&self) -> Frame {
226 let look = self.look.to_f64_array();
227 let up = self.up.to_f64_array();
228 Frame {
229 right: plain::cross(look, up),
230 up,
231 look,
232 center: self.center.to_f64_array(),
233 per_px: self.span / f64::from(self.height),
234 }
235 }
236
237 fn reach(&self) -> Reach {
239 Reach::Tube {
240 radius: Scalar::from_f64(REACH_PX * self.frame().per_px),
241 }
242 }
243
244 pub fn pointer(&self, x: f64, y: f64) -> GeopResult<Pointer> {
250 let f = self.frame();
251 let across = (x - f64::from(self.width) / 2.0) * f.per_px;
252 let down = (y - f64::from(self.height) / 2.0) * f.per_px;
253 let at = plain::add(
254 f.center,
255 plain::sub(plain::scale(f.right, across), plain::scale(f.up, down)),
256 );
257 self.ray_through(at, &f)
258 }
259
260 pub fn through(&self, at: Vector3) -> GeopResult<Pointer> {
263 self.ray_through(at.to_f64_array(), &self.frame())
264 }
265
266 fn ray_through(&self, at: P3, f: &Frame) -> GeopResult<Pointer> {
267 let back = f.coordinates(at)[2] + self.near;
268 let origin = plain::sub(at, plain::scale(f.look, back));
269 let v = |p: P3| Vector3::from_array(p.map(Scalar::from_f64));
270 Ok(Pointer {
271 ray: Ray::try_new(v(origin), v(f.look))?,
272 reach: self.reach(),
273 cycle: 0,
274 })
275 }
276
277 pub fn pixel(&self, at: &Vector3) -> [f64; 2] {
280 let f = self.frame();
281 let c = f.coordinates(at.to_f64_array());
282 [
283 f64::from(self.width) / 2.0 + c[0] / f.per_px,
284 f64::from(self.height) / 2.0 - c[1] / f.per_px,
285 ]
286 }
287}
288
289pub fn picture(
297 scene: &SceneState,
298 lit: Option<&Lighting>,
299 visuals: &[Visual],
300 camera: &Camera,
301) -> GeopResult<Vec<u8>> {
302 let frame = camera.frame();
303 let (width, height) = (camera.width as usize, camera.height as usize);
304 let mut raster = Raster::new(width * SUPERSAMPLE, height * SUPERSAMPLE, frame);
305 let hidden = hidden_of(scene);
306 let layers = layers(scene, lit, &hidden);
307 let [own, ..] = layers.as_slice() else {
308 return Err(GeopError::new("a scene draws its part".to_string()));
309 };
310 for layer in &layers {
311 raster.faces(layer);
312 }
313 raster.datums(scene.part.as_ref(), own);
314 for layer in &layers {
315 raster.lines(layer);
316 }
317 for layer in &layers {
318 raster.lit(layer);
319 }
320 raster.frames(scene.part.as_ref(), own);
321 for visual in visuals {
322 raster.visual(visual);
323 }
324 raster.compass();
325 raster.png(width, height)
326}
327
328struct Layer<'s> {
330 view: &'s PartView,
331 instance: &'s str,
333 place: Place,
334 hidden: &'s BTreeSet<&'s str>,
335 lit: Option<&'s Lit>,
336 face: [f32; 3],
338 tinted: bool,
341}
342
343#[derive(Clone, Copy, Debug)]
346struct Place {
347 origin: P3,
348 axes: [P3; 3],
349}
350
351impl Place {
352 const IDENTITY: Place = Place {
353 origin: [0.0; 3],
354 axes: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
355 };
356
357 fn of(motion: &Motion) -> Self {
358 Self {
359 origin: motion.apply(&Vector3::zero()).to_f64_array(),
360 axes: [0, 1, 2].map(|k| motion.rotate(&Vector3::axis(k)).to_f64_array()),
361 }
362 }
363
364 fn rotate(&self, d: P3) -> P3 {
365 (self.axes.iter().zip(d)).fold([0.0; 3], |r, (axis, x)| {
366 plain::add(r, plain::scale(*axis, x))
367 })
368 }
369
370 fn apply(&self, p: &Vector3) -> P3 {
371 plain::add(self.origin, self.rotate(p.to_f64_array()))
372 }
373}
374
375fn layers<'s>(
378 scene: &'s SceneState,
379 lit: Option<&'s Lighting>,
380 hidden: &'s BTreeSet<&'s str>,
381) -> Vec<Layer<'s>> {
382 let part = scene.part.as_ref();
383 let own = |view: &PartView| parsed(view.color.as_ref()).unwrap_or(rgb(FACE));
384 let lit_of = |name: &str| lit.and_then(|l| l.get(name));
385 let colors: BTreeMap<&str, Option<&String>> = (scene.instances.iter())
386 .map(|i| (i.name.as_str(), i.color.as_ref()))
387 .collect();
388 let placed_hidden = |name: &str| {
389 let mut path = name;
390 loop {
391 if hidden.contains(path) {
392 return true;
393 }
394 match path.rsplit_once(INSTANCE_SEPARATOR) {
395 Some((outer, _)) => path = outer,
396 None => return false,
397 }
398 }
399 };
400 let mut layers = vec![Layer {
401 view: part,
402 instance: "",
403 place: Place::IDENTITY,
404 hidden,
405 lit: lit_of(""),
406 face: own(part),
407 tinted: false,
408 }];
409 for instance in part.instances.iter().filter(|i| !placed_hidden(&i.name)) {
410 let view = instance.view.as_ref();
411 let color = colors.get(instance.name.as_str()).copied();
412 let tint = parsed(color.unwrap_or(instance.color.as_ref()));
413 let face = own(view);
414 layers.push(Layer {
415 view,
416 instance: &instance.name,
417 place: Place::of(&instance.pose.motion()),
418 hidden,
419 lit: lit_of(&instance.name),
420 face: tint.map_or(face, |t| times(face, t)),
421 tinted: tint.is_some(),
422 });
423 }
424 layers
425}
426
427fn hidden_of(scene: &SceneState) -> BTreeSet<&str> {
429 scene.hidden.iter().flatten().map(String::as_str).collect()
430}
431
432impl Layer<'_> {
433 fn color_of(&self, face: &super::ViewFace) -> [f32; 3] {
436 match self.tinted {
437 true => self.face,
438 false => parsed(self.view.color_of_face(face)).unwrap_or(self.face),
439 }
440 }
441
442 fn hides(&self, name: &str) -> bool {
446 let key = Located::in_instance(self.instance, name).key();
447 self.hidden.contains(key.as_str())
448 }
449
450 fn shows_face(&self, solid: &Option<String>, name: &str) -> bool {
451 !self.hides(solid.as_deref().unwrap_or(name))
452 }
453
454 fn shows_boundary(&self, solid: &Option<String>, faces: &[String]) -> bool {
457 match solid {
458 Some(solid) => !self.hides(solid),
459 None => faces.is_empty() || faces.iter().any(|f| !self.hides(f)),
460 }
461 }
462
463 fn shows_sketch(&self, name: &str) -> bool {
464 self.lit.is_some_and(|l| l.sketches.contains(name)) || !self.hides(name)
465 }
466
467 fn points(&self, mut at: impl FnMut(P3)) {
470 let view = self.view;
471 for face in view
472 .faces
473 .iter()
474 .filter(|f| self.shows_face(&f.solid, &f.name))
475 {
476 for p in face.mesh.triangles.iter().flatten() {
477 at(self.place.apply(p));
478 }
479 }
480 for edge in &view.edges {
481 if self.shows_boundary(&edge.solid, &edge.faces) {
482 edge.polyline.iter().for_each(|p| at(self.place.apply(p)));
483 }
484 }
485 for a in &view.annotations {
486 a.polyline.iter().for_each(|p| at(self.place.apply(p)));
487 }
488 for sketch in view.sketches.iter().filter(|s| self.shows_sketch(&s.name)) {
489 for p in sketch.curves.iter().flat_map(|c| &c.polyline) {
490 at(self.place.apply(&sketch.plane.uv_to_xyz(p)));
491 }
492 for p in &sketch.points {
493 at(self.place.apply(&sketch.plane.uv_to_xyz(&p.at)));
494 }
495 }
496 }
497}
498
499fn rgb(hex: u32) -> [f32; 3] {
501 [16, 8, 0].map(|shift| ((hex >> shift) & 0xff) as f32 / 255.0)
502}
503
504fn parsed(color: Option<&String>) -> Option<[f32; 3]> {
506 let hex = color?.strip_prefix('#')?;
507 (hex.len() == 6)
508 .then(|| u32::from_str_radix(hex, 16).ok())
509 .flatten()
510 .map(rgb)
511}
512
513fn mix(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
515 let mut mixed = a;
516 for (a, b) in mixed.iter_mut().zip(b) {
517 *a += (b - *a) * t;
518 }
519 mixed
520}
521
522fn times(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
525 let mut product = a;
526 for (a, b) in product.iter_mut().zip(b) {
527 *a *= b;
528 }
529 product
530}
531
532#[derive(Clone, Copy, Debug)]
535struct Sample {
536 color: [f32; 3],
537 depth: f64,
538}
539
540#[derive(Clone, Copy, Debug)]
543enum Dashes {
544 Solid,
545 Dashed(f64, f64),
546}
547
548#[derive(Clone, Copy, Debug)]
551struct Pen {
552 color: [f32; 3],
553 alpha: f32,
554 half: f64,
556 tested: bool,
557 dashes: Dashes,
558}
559
560fn depth(t: [P3; 3], w: [f64; 3]) -> f64 {
563 t.iter().zip(w).map(|(p, w)| p[2] * w).sum()
564}
565
566struct Raster {
569 width: usize,
570 height: usize,
571 samples: Vec<Sample>,
572 frame: Frame,
573 light: P3,
576 bias: f64,
579}
580
581impl Raster {
582 fn new(width: usize, height: usize, frame: Frame) -> Self {
583 let mut frame = frame;
584 frame.per_px /= SUPERSAMPLE as f64;
585 let towards = plain::add(
586 plain::scale(frame.look, -1.0),
587 plain::add(plain::scale(frame.up, 0.6), plain::scale(frame.right, -0.4)),
588 );
589 Self {
590 width,
591 height,
592 samples: vec![
593 Sample {
594 color: rgb(BACKGROUND),
595 depth: f64::INFINITY,
596 };
597 width * height
598 ],
599 light: plain::normalize(towards).unwrap_or(frame.look),
600 bias: DEPTH_BIAS_PX * SUPERSAMPLE as f64 * frame.per_px,
601 frame,
602 }
603 }
604
605 fn px(pixels: f64) -> f64 {
607 pixels * SUPERSAMPLE as f64
608 }
609
610 fn project(&self, p: P3) -> P3 {
612 let c = self.frame.coordinates(p);
613 [
614 self.width as f64 / 2.0 + c[0] / self.frame.per_px,
615 self.height as f64 / 2.0 - c[1] / self.frame.per_px,
616 c[2],
617 ]
618 }
619
620 fn samples(&self, low: P2, high: P2) -> impl Iterator<Item = (usize, usize)> + use<> {
622 let clamp = |x: f64, n: usize| x.max(0.0).min(n as f64) as usize;
623 let (x0, x1) = (
624 clamp(low[0].floor(), self.width),
625 clamp(high[0].ceil(), self.width),
626 );
627 let (y0, y1) = (
628 clamp(low[1].floor(), self.height),
629 clamp(high[1].ceil(), self.height),
630 );
631 (y0..y1).flat_map(move |y| (x0..x1).map(move |x| (x, y)))
632 }
633
634 fn blend(&mut self, i: usize, color: [f32; 3], alpha: f32, depth: Option<f64>) {
638 let bias = self.bias;
639 if let Some(sample) = self.samples.get_mut(i)
640 && depth.is_none_or(|z| z <= sample.depth + bias)
641 {
642 sample.color = mix(sample.color, color, alpha.clamp(0.0, 1.0));
643 }
644 }
645
646 fn cover(&self, t: [P3; 3], mut at: impl FnMut(usize, [f64; 3])) {
649 let edge =
650 |a: P3, b: P3, p: P2| (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
651 let area = edge(t[0], t[1], [t[2][0], t[2][1]]);
652 if area == 0.0 || !area.is_finite() {
653 return;
654 }
655 let [low, high] = t.iter().fold(
656 [[f64::INFINITY; 2], [f64::NEG_INFINITY; 2]],
657 |[low, high], p| {
658 [
659 [low[0].min(p[0]), low[1].min(p[1])],
660 [high[0].max(p[0]), high[1].max(p[1])],
661 ]
662 },
663 );
664 for (x, y) in self.samples(low, high) {
665 let p = [x as f64 + 0.5, y as f64 + 0.5];
666 let w = [
667 edge(t[1], t[2], p) / area,
668 edge(t[2], t[0], p) / area,
669 edge(t[0], t[1], p) / area,
670 ];
671 if w.iter().all(|&w| w >= 0.0) {
672 at(y * self.width + x, w);
673 }
674 }
675 }
676
677 fn shaded(&mut self, t: [P3; 3], n: [P3; 3], color: [f32; 3]) {
680 let t = t.map(|p| self.project(p));
681 let mut drawn = Vec::new();
682 self.cover(t, |i, w| drawn.push((i, w)));
683 for (i, w) in drawn {
684 let z = depth(t, w);
685 let light = self.light;
686 let look = self.frame.look;
687 let Some(sample) = self.samples.get_mut(i).filter(|s| z < s.depth) else {
688 continue;
689 };
690 sample.depth = z;
691 let normal =
692 (n.iter().zip(w)).fold([0.0; 3], |s, (n, w)| plain::add(s, plain::scale(*n, w)));
693 let shade = match plain::normalize(normal) {
694 Some(normal) => {
695 let facing = match plain::dot(normal, look) > 0.0 {
696 true => plain::scale(normal, -1.0),
697 false => normal,
698 };
699 AMBIENT + DIFFUSE * plain::dot(facing, light).max(0.0) as f32
700 }
701 None => AMBIENT + DIFFUSE / 2.0,
702 };
703 sample.color = color.map(|c| (c * shade).min(1.0));
704 }
705 }
706
707 fn filled(&mut self, t: [P3; 3], color: [f32; 3], alpha: f32, tested: bool) {
710 let mut drawn = Vec::new();
711 self.cover(t, |i, w| drawn.push((i, w)));
712 for (i, w) in drawn {
713 self.blend(i, color, alpha, tested.then(|| depth(t, w)));
714 }
715 }
716
717 fn segment(&mut self, a: P3, b: P3, pen: &Pen, run: f64) -> f64 {
721 let d = [b[0] - a[0], b[1] - a[1]];
722 let length_sq = d[0] * d[0] + d[1] * d[1];
723 let reach = pen.half + 1.0;
724 let low = [a[0].min(b[0]) - reach, a[1].min(b[1]) - reach];
725 let high = [a[0].max(b[0]) + reach, a[1].max(b[1]) + reach];
726 let length = length_sq.sqrt();
727 if !(length.is_finite() && low[0].is_finite() && low[1].is_finite()) {
728 return run;
729 }
730 for (x, y) in self.samples(low, high) {
731 let p = [x as f64 + 0.5 - a[0], y as f64 + 0.5 - a[1]];
732 let s = match length_sq > 0.0 {
733 true => ((p[0] * d[0] + p[1] * d[1]) / length_sq).clamp(0.0, 1.0),
734 false => 0.0,
735 };
736 if let Dashes::Dashed(dash, gap) = pen.dashes
737 && (run + s * length) % (dash + gap) > dash
738 {
739 continue;
740 }
741 let off = [p[0] - s * d[0], p[1] - s * d[1]];
742 let distance = (off[0] * off[0] + off[1] * off[1]).sqrt();
743 let coverage = (pen.half + 0.5 - distance).clamp(0.0, 1.0) as f32;
744 if coverage <= 0.0 {
745 continue;
746 }
747 let i = y * self.width + x;
748 let z = pen.tested.then(|| a[2] + s * (b[2] - a[2]));
749 self.blend(i, pen.color, pen.alpha * coverage, z);
750 }
751 run + length
752 }
753
754 fn polyline(&mut self, points: impl IntoIterator<Item = P3>, pen: &Pen) {
756 let mut run = 0.0;
757 let mut last: Option<P3> = None;
758 for p in points {
759 let p = self.project(p);
760 if let Some(a) = last {
761 run = self.segment(a, p, pen, run);
762 }
763 last = Some(p);
764 }
765 }
766
767 fn disc(&mut self, at: P3, pen: &Pen) {
770 self.segment(at, at, pen, 0.0);
771 }
772
773 fn arrowhead(&mut self, base: P3, tip: P3, radius: f64, color: [f32; 3]) {
776 let d = [tip[0] - base[0], tip[1] - base[1]];
777 let Some(d) = plain::normalize(d) else {
778 return;
779 };
780 let side = [-d[1] * radius, d[0] * radius];
781 let corner = |sign: f64| [base[0] + sign * side[0], base[1] + sign * side[1], base[2]];
782 self.filled([corner(1.0), corner(-1.0), tip], color, 1.0, false);
783 }
784
785 fn faces(&mut self, layer: &Layer<'_>) {
788 let highlight = rgb(HIGHLIGHT);
789 for face in &layer.view.faces {
790 if !layer.shows_face(&face.solid, &face.name) {
791 continue;
792 }
793 let lit = (layer.lit).is_some_and(|l| {
794 l.face(&face.name, face.solid.as_deref(), face.feature.as_deref())
795 });
796 let own = layer.color_of(face);
797 let color = match lit {
798 true => mix(own, highlight, LIT_TINT),
799 false => own,
800 };
801 for (t, n) in face.mesh.triangles.iter().zip(&face.mesh.normals) {
802 let t = t.map(|p| layer.place.apply(&p));
803 let n = n.map(|n| layer.place.rotate(n.to_f64_array()));
804 self.shaded(t, n, color);
805 }
806 }
807 }
808
809 fn lines(&mut self, layer: &Layer<'_>) {
811 let pen = |color: u32| Pen {
812 color: rgb(color),
813 alpha: 1.0,
814 half: Self::px(EDGE_HALF_WIDTH),
815 tested: true,
816 dashes: Dashes::Solid,
817 };
818 let place = &layer.place;
819 for edge in &layer.view.edges {
820 if !edge.within_a_surface && layer.shows_boundary(&edge.solid, &edge.faces) {
821 self.polyline(edge.polyline.iter().map(|p| place.apply(p)), &pen(EDGE));
822 }
823 }
824 for a in &layer.view.annotations {
825 self.polyline(a.polyline.iter().map(|p| place.apply(p)), &pen(ANNOTATION));
826 }
827 for sketch in &layer.view.sketches {
828 if !layer.shows_sketch(&sketch.name) {
829 continue;
830 }
831 let lit = layer.lit.is_some_and(|l| l.sketches.contains(&sketch.name));
832 let in_plane = |p| place.apply(&sketch.plane.uv_to_xyz(p));
833 for curve in &sketch.curves {
834 let pen = match (lit, curve.construction) {
835 (true, _) => Pen {
836 tested: false,
837 ..pen(HIGHLIGHT)
838 },
839 (false, true) => pen(CONSTRUCTION),
840 (false, false) => pen(SKETCH),
841 };
842 self.polyline(curve.polyline.iter().map(in_plane), &pen);
843 }
844 for point in &sketch.points {
845 let at = self.project(in_plane(&point.at));
846 let color = if lit { HIGHLIGHT } else { SKETCH };
847 let pen = Pen {
848 half: Self::px(2.5),
849 tested: !lit,
850 ..pen(color)
851 };
852 self.disc(at, &pen);
853 }
854 }
855 }
856
857 fn lit(&mut self, layer: &Layer<'_>) {
861 let Some(lit) = layer.lit else {
862 return;
863 };
864 let place = &layer.place;
865 let highlight = rgb(HIGHLIGHT);
866 let view = layer.view;
867 if !lit.whole {
868 for face in &view.faces {
869 if !lit.face(&face.name, face.solid.as_deref(), face.feature.as_deref()) {
870 continue;
871 }
872 for t in &face.mesh.triangles {
873 let t = t.map(|p| self.project(place.apply(&p)));
874 self.filled(t, highlight, LIT_FACE_OPACITY, false);
875 }
876 }
877 }
878 let pen = Pen {
879 color: highlight,
880 alpha: 1.0,
881 half: Self::px(LIT_HALF_WIDTH),
882 tested: false,
883 dashes: Dashes::Solid,
884 };
885 let dot = Pen {
886 half: Self::px(4.5),
887 ..pen
888 };
889 for edge in view.edges.iter().filter(|e| lit.edges.contains(&e.name)) {
890 self.polyline(edge.polyline.iter().map(|p| place.apply(p)), &pen);
891 }
892 for vertex in view
893 .vertices
894 .iter()
895 .filter(|v| lit.vertices.contains(&v.name))
896 {
897 let at = self.project(place.apply(&vertex.at));
898 self.disc(at, &dot);
899 }
900 for sketch in &view.sketches {
901 let in_plane = |p| place.apply(&sketch.plane.uv_to_xyz(p));
902 let named = |id: &String| (sketch.name.clone(), id.clone());
903 for curve in sketch
904 .curves
905 .iter()
906 .filter(|c| lit.curves.contains(&named(&c.id)))
907 {
908 self.polyline(curve.polyline.iter().map(in_plane), &pen);
909 }
910 let points = sketch.points.iter();
911 for point in points.filter(|p| lit.sketch_points.contains(&named(&p.id))) {
912 let at = self.project(in_plane(&point.at));
913 self.disc(at, &dot);
914 }
915 }
916 }
917
918 fn datum_color(layer: &Layer<'_>, d: &ViewDatum, name: &str, own: u32) -> [f32; 3] {
921 let lit = layer
922 .lit
923 .is_some_and(|l| l.datums.contains(&d.name) || l.datums.contains(name));
924 rgb(if lit { HIGHLIGHT } else { own })
925 }
926
927 fn shows_datum(layer: &Layer<'_>, d: &ViewDatum) -> bool {
930 let lit = layer.lit.is_some_and(|l| {
931 (l.datums.iter()).any(|name| {
932 name == &d.name
933 || (name.strip_prefix(d.name.as_str()))
934 .is_some_and(|rest| rest.starts_with(':'))
935 })
936 });
937 lit || !layer.hides(&d.name)
938 }
939
940 fn datums(&mut self, part: &PartView, layer: &Layer<'_>) {
943 let size = part.extent.size.to_f64();
944 let center = part.extent.center.to_f64_array();
945 for d in part.datums.iter().filter(|d| Self::shows_datum(layer, d)) {
946 let f = &d.frame;
947 let origin = f.origin().to_f64_array();
948 let [u, v, w] = [f.u(), f.v(), f.w()].map(|a| a.to_f64_array());
949 let offset = plain::sub(center, origin);
950 let color = Self::datum_color(layer, d, &d.name, DATUM);
951 let pen = Pen {
952 color,
953 alpha: 0.8,
954 half: Self::px(EDGE_HALF_WIDTH),
955 tested: true,
956 dashes: Dashes::Solid,
957 };
958 match d.kind {
959 DatumKind::Plane => {
960 let at = plain::sub(center, plain::scale(w, plain::dot(offset, w)));
961 let corner = |a: f64, b: f64| {
962 let along = plain::add(plain::scale(u, a), plain::scale(v, b));
963 plain::add(at, plain::scale(along, size / 2.0))
964 };
965 let corners = [
966 corner(-1.0, -1.0),
967 corner(1.0, -1.0),
968 corner(1.0, 1.0),
969 corner(-1.0, 1.0),
970 ];
971 let [a, b, c, e] = corners.map(|p| self.project(p));
972 self.filled([a, b, c], color, 0.12, true);
973 self.filled([a, c, e], color, 0.12, true);
974 self.polyline(corners.into_iter().chain([corners[0]]), &pen);
975 }
976 DatumKind::Axis => {
977 let at = plain::add(origin, plain::scale(w, plain::dot(offset, w)));
978 let ends = [-1.0, 1.0].map(|s| plain::add(at, plain::scale(w, s * size)));
979 let dash = size / 30.0 / self.frame.per_px;
980 let pen = Pen {
981 alpha: 0.9,
982 dashes: Dashes::Dashed(dash, dash / 2.0),
983 ..pen
984 };
985 self.polyline(ends, &pen);
986 }
987 DatumKind::Point | DatumKind::Frame => {}
988 }
989 }
990 }
991
992 fn frames(&mut self, part: &PartView, layer: &Layer<'_>) {
997 let reach = Self::px(REACH_PX);
998 for d in part.datums.iter().filter(|d| Self::shows_datum(layer, d)) {
999 let f = &d.frame;
1000 let o = self.project(f.origin().to_f64_array());
1001 let axes = FrameAxis::ALL.map(|a| self.frame.on_screen(a.of(f).to_f64_array()));
1002 let at = |size: f64, x: [f64; 3]| {
1003 let along = (axes.iter().zip(x)).fold([0.0; 2], |s, (axis, x)| {
1004 plain::add(s, plain::scale(*axis, x))
1005 });
1006 [o[0] + along[0] * size, o[1] + along[1] * size, o[2]]
1007 };
1008 let pen = |color: [f32; 3], half: f64| Pen {
1009 color,
1010 alpha: 1.0,
1011 half: half.max(Self::px(0.6)),
1012 tested: false,
1013 dashes: Dashes::Solid,
1014 };
1015 let unit = |k: usize, length: f64| plain::scale(unit(k), length);
1016 match d.kind {
1017 DatumKind::Frame => {
1018 let size = FRAME * reach;
1019 let component = |c: DatumComponent| {
1020 (d.components.iter())
1021 .find(|(of, _)| *of == c)
1022 .map(|(_, name)| name.as_str())
1023 };
1024 let axes = FrameAxis::ALL.into_iter().zip(AXES).enumerate();
1025 for (k, (axis, own)) in axes.clone() {
1026 let name = component(DatumComponent::Plane(axis)).unwrap_or_default();
1027 let color = Self::datum_color(layer, d, name, own);
1028 let mut others = (0..3).filter(|&j| j != k).map(|j| unit(j, 1.0));
1029 let (Some(a), Some(b)) = (others.next(), others.next()) else {
1030 continue;
1031 };
1032 let corner = |s: f64, t: f64| {
1033 at(size, plain::add(plain::scale(a, s), plain::scale(b, t)))
1034 };
1035 let square = [
1036 corner(0.3, 0.3),
1037 corner(0.6, 0.3),
1038 corner(0.6, 0.6),
1039 corner(0.3, 0.6),
1040 ];
1041 let [p, q, r, s] = square;
1042 self.filled([p, q, r], color, 0.3, false);
1043 self.filled([p, r, s], color, 0.3, false);
1044 for (a, b) in [(p, q), (q, r), (r, s), (s, p)] {
1045 self.segment(a, b, &pen(color, Self::px(0.5)), 0.0);
1046 }
1047 }
1048 for (k, (axis, own)) in axes {
1049 let name = component(DatumComponent::Axis(axis)).unwrap_or_default();
1050 let color = Self::datum_color(layer, d, name, own);
1051 let (base, tip) = (at(size, unit(k, 0.85)), at(size, unit(k, 1.0)));
1052 self.segment(at(size, unit(k, 0.1)), base, &pen(color, 0.015 * size), 0.0);
1053 self.arrowhead(base, tip, 0.05 * size, color);
1054 }
1055 let ball = Self::datum_color(layer, d, &d.name, 0xffffff);
1056 self.disc(o, &pen(ball, 0.07 * size));
1057 }
1058 DatumKind::Point => {
1059 let size = POINT_TRIAD * reach;
1060 for (k, tint) in AXIS_TINTS.into_iter().enumerate() {
1061 let color = Self::datum_color(layer, d, &d.name, tint);
1062 self.segment(o, at(size, unit(k, 1.0)), &pen(color, Self::px(0.6)), 0.0);
1063 }
1064 let ball = Self::datum_color(layer, d, &d.name, DATUM);
1065 self.disc(o, &pen(ball, 0.12 * size));
1066 }
1067 DatumKind::Plane | DatumKind::Axis => {}
1068 }
1069 }
1070 }
1071
1072 fn visual(&mut self, visual: &Visual) {
1076 let color = rgb(match visual.style {
1077 Style::Free | Style::Region => 0x6cb4ff,
1078 Style::Fixed => 0xe6e6e6,
1079 Style::Selected => 0xffd84a,
1080 Style::Hover => 0xffe0a0,
1081 Style::Failed => 0xff5d5d,
1082 Style::Construction => 0x8a8a8a,
1083 Style::Reference => 0xb48cf0,
1084 Style::Draft | Style::Handle => 0xffa040,
1085 Style::Guide => 0x777777,
1086 Style::Snap => 0x58e07a,
1087 Style::Removed => 0xff4f9a,
1088 Style::Paper => 0xbbbbbb,
1089 });
1090 let reach = Self::px(REACH_PX);
1091 let pen = Pen {
1092 color,
1093 alpha: 1.0,
1094 half: Self::px(EDGE_HALF_WIDTH),
1095 tested: false,
1096 dashes: Dashes::Solid,
1097 };
1098 let disc = |radius: f64| Pen {
1099 half: Self::px(radius),
1100 ..pen
1101 };
1102 match &visual.shape {
1103 Shape::Point { at } => {
1104 let at = self.project(at.to_f64_array());
1105 match visual.style {
1106 Style::Snap => {
1107 let ring = Self::px(6.0);
1108 let around = (0..=24).map(|i| {
1109 let angle = f64::from(i) * std::f64::consts::TAU / 24.0;
1110 [
1111 at[0] + ring * angle.cos(),
1112 at[1] + ring * angle.sin(),
1113 at[2],
1114 ]
1115 });
1116 let mut last = None;
1117 for p in around {
1118 if let Some(a) = last {
1119 self.segment(a, p, &pen, 0.0);
1120 }
1121 last = Some(p);
1122 }
1123 }
1124 Style::Selected | Style::Hover => self.disc(at, &disc(5.0)),
1125 _ => self.disc(at, &disc(3.5)),
1126 }
1127 }
1128 Shape::Polyline { points } => {
1129 let dashes = match visual.style {
1130 Style::Reference => Dashes::Dashed(Self::px(1.5), Self::px(3.0)),
1131 Style::Construction | Style::Draft | Style::Guide => {
1132 Dashes::Dashed(Self::px(6.0), Self::px(4.0))
1133 }
1134 _ => Dashes::Solid,
1135 };
1136 let pen = Pen { dashes, ..pen };
1137 self.polyline(points.iter().map(|p| p.to_f64_array()), &pen);
1138 }
1139 Shape::Triangles { triangles } => {
1140 for t in triangles {
1141 let t = t.map(|p| self.project(p.to_f64_array()));
1142 self.filled(t, color, 0.15, false);
1143 }
1144 }
1145 Shape::Label { at, offset, .. } => {
1146 let shift = self.frame.on_screen(offset.to_f64_array());
1147 let at = self.project(at.to_f64_array());
1148 let at = [at[0] + shift[0] * reach, at[1] + shift[1] * reach, at[2]];
1149 self.disc(at, &disc(2.0));
1150 }
1151 Shape::Handle { at, direction } => {
1152 let unit = 0.8 * reach;
1153 let at = self.project(at.to_f64_array());
1154 self.disc(at, &Pen { half: unit, ..pen });
1155 if let Some(d) = plain::normalize(self.frame.on_screen(direction.to_f64_array())) {
1156 for sign in [1.0, -1.0] {
1157 let along = |x: f64| {
1158 [
1159 at[0] + sign * d[0] * x * unit,
1160 at[1] + sign * d[1] * x * unit,
1161 at[2],
1162 ]
1163 };
1164 self.arrowhead(along(1.4), along(3.0), unit, color);
1165 }
1166 }
1167 }
1168 Shape::Instance { .. } => {}
1169 Shape::Triad { at, axes } => {
1170 let size = TRIAD * reach;
1171 let at = self.project(at.to_f64_array());
1172 for (axis, color) in axes.iter().zip(AXES.map(rgb)) {
1173 let Some(d) = plain::normalize(axis.to_f64_array()) else {
1174 continue;
1175 };
1176 let d = self.frame.on_screen(d);
1177 let along = |x: f64| [at[0] + d[0] * x * size, at[1] + d[1] * x * size, at[2]];
1178 let pen = Pen {
1179 color,
1180 half: (0.02 * size).max(Self::px(0.6)),
1181 ..pen
1182 };
1183 self.segment(along(0.0), along(0.8), &pen, 0.0);
1184 self.arrowhead(along(0.8), along(1.0), 0.06 * size, color);
1185 }
1186 }
1187 }
1188 }
1189
1190 fn compass(&mut self) {
1194 let origin = [Self::px(34.0), self.height as f64 - Self::px(34.0), 0.0];
1195 let length = Self::px(24.0);
1196 let mut order: Vec<(usize, u32)> = AXES.into_iter().enumerate().collect();
1197 let into = |k: usize| plain::dot(unit(k), self.frame.look);
1198 order.sort_by(|a, b| into(b.0).total_cmp(&into(a.0)));
1199 for (k, color) in order {
1200 let d = self.frame.on_screen(unit(k));
1201 let tip = [origin[0] + d[0] * length, origin[1] + d[1] * length, 0.0];
1202 let pen = Pen {
1203 color: rgb(color),
1204 alpha: 1.0,
1205 half: Self::px(1.0),
1206 tested: false,
1207 dashes: Dashes::Solid,
1208 };
1209 self.segment(origin, tip, &pen, 0.0);
1210 let towards = plain::normalize(d).unwrap_or([0.0, 0.0]);
1211 let letter = [
1212 tip[0] + towards[0] * Self::px(9.0),
1213 tip[1] + towards[1] * Self::px(9.0),
1214 ];
1215 self.letter(k, letter, rgb(color));
1216 }
1217 }
1218
1219 fn letter(&mut self, k: usize, at: P2, color: [f32; 3]) {
1221 const GLYPHS: [[&str; 5]; 3] = [
1222 ["#.#", "#.#", ".#.", "#.#", "#.#"],
1223 ["#.#", "#.#", ".#.", ".#.", ".#."],
1224 ["###", "..#", ".#.", "#..", "###"],
1225 ];
1226 let cell = Self::px(2.0);
1227 for (row, line) in GLYPHS.get(k).into_iter().flatten().enumerate() {
1228 for (column, c) in line.chars().enumerate() {
1229 if c != '#' {
1230 continue;
1231 }
1232 let x = at[0] + (column as f64 - 1.5) * cell;
1233 let y = at[1] + (row as f64 - 2.5) * cell;
1234 for (sx, sy) in self.samples([x, y], [x + cell, y + cell]) {
1235 self.blend(sy * self.width + sx, color, 1.0, None);
1236 }
1237 }
1238 }
1239 }
1240
1241 fn png(&self, width: usize, height: usize) -> GeopResult<Vec<u8>> {
1244 let mut sums = vec![[0.0f32; 3]; width * height];
1245 for (i, sample) in self.samples.iter().enumerate() {
1246 let (x, y) = (i % self.width / SUPERSAMPLE, i / self.width / SUPERSAMPLE);
1247 if let Some(sum) = sums.get_mut(y * width + x) {
1248 for (s, c) in sum.iter_mut().zip(sample.color) {
1249 *s += c;
1250 }
1251 }
1252 }
1253 let samples = (SUPERSAMPLE * SUPERSAMPLE) as f32;
1254 let data: Vec<u8> = (sums.iter().flatten())
1255 .map(|s| (s / samples * 255.0).round().clamp(0.0, 255.0) as u8)
1256 .collect();
1257 let failed =
1258 |e: png::EncodingError| GeopError::new(format!("writing a picture as a PNG: {e}"));
1259 let mut out = Vec::new();
1260 let mut encoder = png::Encoder::new(&mut out, width as u32, height as u32);
1261 encoder.set_color(png::ColorType::Rgb);
1262 encoder.set_depth(png::BitDepth::Eight);
1263 let mut writer = encoder.write_header().map_err(failed)?;
1264 writer.write_image_data(&data).map_err(failed)?;
1265 writer.finish().map_err(failed)?;
1266 Ok(out)
1267 }
1268}
1269
1270#[cfg(test)]
1271mod tests {
1272 use std::{collections::BTreeMap, rc::Rc};
1273
1274 use geop_core_math::primitives::{CoordinateSystem, Pose};
1275
1276 use super::*;
1277 use crate::view::{FaceMesh, ViewEdge, ViewFace, ViewInstance, drawn};
1278
1279 fn v3(p: P3) -> Vector3 {
1280 Vector3::from_array(p.map(Scalar::from_f64))
1281 }
1282
1283 fn block(low: P3) -> PartView {
1286 let mut view = PartView::new();
1287 view.model = "model".into();
1288 view.solids = vec!["block".into()];
1289 let corner = |c: [usize; 3]| v3(std::array::from_fn(|k| low[k] + c[k] as f64));
1290 for k in 0..3 {
1291 let (a, b) = ((k + 1) % 3, (k + 2) % 3);
1292 for side in 0..2 {
1293 let at = |i: usize, j: usize| {
1294 let mut c = [0; 3];
1295 (c[k], c[a], c[b]) = (side, i, j);
1296 corner(c)
1297 };
1298 let quad = [at(0, 0), at(1, 0), at(1, 1), at(0, 1)];
1299 let mut normal = [0.0; 3];
1300 normal[k] = if side == 0 { -1.0 } else { 1.0 };
1301 view.faces.push(ViewFace {
1302 also: Vec::new(),
1303 name: format!("{}{}", ["x", "y", "z"][k], ["-", "+"][side]),
1304 solid: Some("block".into()),
1305 feature: None,
1306 mesh: FaceMesh::new(
1307 vec![[quad[0], quad[1], quad[2]], [quad[0], quad[2], quad[3]]],
1308 vec![[v3(normal); 3]; 2],
1309 ),
1310 });
1311 for i in 0..2 {
1312 let mut c = [0; 3];
1313 (c[k], c[a]) = (side, i);
1314 let mut d = c;
1315 d[b] = 1;
1316 view.edges.push(ViewEdge {
1317 name: format!("e{}", view.edges.len()),
1318 solid: Some("block".into()),
1319 faces: vec![],
1320 polyline: vec![corner(c), corner(d)],
1321 within_a_surface: false,
1322 ends: [String::new(), String::new()],
1323 });
1324 }
1325 }
1326 }
1327 view.datum_target = "datums".into();
1328 view.datums.push(ViewDatum {
1329 name: "origin".into(),
1330 kind: DatumKind::Frame,
1331 frame: CoordinateSystem::world_at(Vector3::zero()),
1332 components: FrameAxis::ALL
1333 .into_iter()
1334 .flat_map(|a| [DatumComponent::Axis(a), DatumComponent::Plane(a)])
1335 .map(|c| (c, format!("origin:{c}")))
1336 .collect(),
1337 });
1338 view
1339 }
1340
1341 fn scene(part: PartView) -> SceneState {
1342 SceneState {
1343 part: Rc::new(part.finish()),
1344 hidden: Some(vec![]),
1345 instances: vec![],
1346 all: true,
1347 removed: vec![],
1348 components: BTreeMap::new(),
1349 forgotten: vec![],
1350 meshes: BTreeMap::new(),
1351 forgotten_meshes: vec![],
1352 structure: None,
1353 }
1354 }
1355
1356 fn isometric(scene: &SceneState) -> Camera {
1357 let (look, up) = (v3([-1.0, -1.0, -1.0]), v3([0.0, 0.0, 1.0]));
1358 Camera::fitting(scene, look, up, 400, 300).unwrap()
1359 }
1360
1361 fn decoded(png: &[u8]) -> (Vec<[u8; 3]>, u32, u32) {
1363 let decoder = png::Decoder::new(png);
1364 let mut reader = decoder.read_info().unwrap();
1365 let mut data = vec![0; reader.output_buffer_size()];
1366 let info = reader.next_frame(&mut data).unwrap();
1367 assert_eq!(info.color_type, png::ColorType::Rgb);
1368 let pixels = (data[..info.buffer_size()].chunks(3))
1369 .map(|c| [c[0], c[1], c[2]])
1370 .collect();
1371 (pixels, info.width, info.height)
1372 }
1373
1374 fn at(pixels: &(Vec<[u8; 3]>, u32, u32), p: [f64; 2]) -> [u8; 3] {
1375 pixels.0[p[1] as usize * pixels.1 as usize + p[0] as usize]
1376 }
1377
1378 fn grey(c: [u8; 3]) -> bool {
1379 let (low, high) = (c.iter().min().unwrap(), c.iter().max().unwrap());
1380 *low > 60 && high - low < 16
1381 }
1382
1383 #[test]
1384 fn a_block_is_drawn_in_the_middle_of_its_picture() {
1385 let scene = scene(block([2.0, 2.0, 2.0]));
1386 let camera = isometric(&scene);
1387 let png = picture(&scene, None, &[], &camera).unwrap();
1388 let pixels = decoded(&png);
1389 assert_eq!((pixels.1, pixels.2), (400, 300));
1390 let middle = camera.pixel(&v3([2.5, 2.5, 2.5]));
1391 assert!((middle[0] - 200.0).abs() < 1.0 && (middle[1] - 150.0).abs() < 1.0);
1392 assert!(grey(at(&pixels, middle)), "{:?}", at(&pixels, middle));
1393 assert_eq!(at(&pixels, [399.0, 0.0]), [0x1b, 0x1f, 0x26]);
1394 let faces = [[2.5, 2.5, 3.0], [3.0, 2.5, 2.5], [2.5, 3.0, 2.5]];
1396 let shades = faces.map(|f| at(&pixels, camera.pixel(&v3(f)))[0]);
1397 assert!(shades[0] != shades[1] && shades[1] != shades[2] && shades[0] != shades[2]);
1398 assert_eq!(png, picture(&scene, None, &[], &camera).unwrap());
1400 }
1401
1402 #[test]
1403 fn a_pixel_points_where_it_is_drawn() {
1404 let scene = scene(block([2.0, 2.0, 2.0]));
1405 let camera = isometric(&scene);
1406 let pointer = camera.pointer(200.0, 150.0).unwrap();
1407 let accepts = [drawn::FACE.to_string()];
1408 let hit = scene.part.pick(&pointer, &accepts, &[]).unwrap();
1409 assert!(hit.element.name.ends_with('+'), "{:?}", hit.element);
1411 let p = v3([2.3, 2.9, 2.1]);
1412 let [x, y] = camera.pixel(&p);
1413 let through = camera.through(p).unwrap();
1414 let back = camera.pixel(through.ray.origin());
1415 assert!((back[0] - x).abs() < 1e-9 && (back[1] - y).abs() < 1e-9);
1416 let (distance, t) = camera.pointer(x, y).unwrap().ray.distance_to_point(&p);
1417 assert!(distance.to_f64() < 1e-9 && t.to_f64() > 0.0);
1418 let Reach::Tube { radius } = pointer.reach else {
1419 panic!("an orthographic pointer reaches as a tube");
1420 };
1421 assert!((radius.to_f64() - REACH_PX * camera.span / 300.0).abs() < 1e-12);
1422 }
1423
1424 #[test]
1425 fn a_placed_part_is_framed_and_drawn_in_its_colour_and_what_is_lit_lit() {
1426 let component = Rc::new(block([0.0, 0.0, 0.0]).finish());
1427 let at_x = v3([10.0, 0.0, 0.0]);
1428 let mut part = block([0.0, 0.0, 0.0]);
1429 part.instances.push(ViewInstance {
1430 name: "bolt".into(),
1431 component: "block".into(),
1432 frame: CoordinateSystem::world_at(at_x),
1433 pose: Pose::identity().with_position(at_x),
1434 view: component,
1435 state: None,
1436 fixed: true,
1437 color: None,
1438 });
1439 let mut scene = scene(part);
1440 scene.instances = vec![ViewInstance {
1441 color: Some("#ff0000".into()),
1442 ..scene.part.instances[0].clone()
1443 }];
1444 let (look, up) = (v3([0.0, 0.0, -1.0]), v3([0.0, 1.0, 0.0]));
1445 let camera = Camera::fitting(&scene, look, up, 400, 300).unwrap();
1446 let lit = Lighting::from([(
1447 String::new(),
1448 Lit {
1449 faces: BTreeSet::from(["z+".to_string()]),
1450 ..Lit::default()
1451 },
1452 )]);
1453 let pixels = decoded(&picture(&scene, Some(&lit), &[], &camera).unwrap());
1454 let [r, g, b] = at(&pixels, camera.pixel(&v3([10.5, 0.5, 1.0])));
1455 assert!(r > 60 && g < 20 && b < 20, "{:?}", [r, g, b]);
1456 let [r, g, b] = at(&pixels, camera.pixel(&v3([0.8, 0.25, 1.0])));
1458 assert!(r > b + 60 && g > b + 30, "{:?}", [r, g, b]);
1459 for x in [0.0, 11.0] {
1461 let [px, _] = camera.pixel(&v3([x, 0.5, 1.0]));
1462 assert!(px > 10.0 && px < 390.0, "{px}");
1463 }
1464 }
1465
1466 #[test]
1470 fn a_face_is_drawn_in_its_colour_and_a_placed_parts_colour_is_over_it() {
1471 let mut own = block([0.0, 0.0, 0.0]);
1472 own.color = Some("#0000ff".into());
1473 own.face_colors = BTreeMap::from([("z+".to_string(), "#00ff00".to_string())]);
1474 let component = Rc::new(own.finish());
1475 let placed = |name: &str, x: f64, color: Option<&str>| {
1476 let at = v3([x, 0.0, 0.0]);
1477 ViewInstance {
1478 name: name.into(),
1479 component: "block".into(),
1480 frame: CoordinateSystem::world_at(at),
1481 pose: Pose::identity().with_position(at),
1482 view: component.clone(),
1483 state: None,
1484 fixed: true,
1485 color: color.map(str::to_string),
1486 }
1487 };
1488 let mut part = block([0.0, 0.0, 0.0]);
1489 part.face_colors = BTreeMap::from([("z+".to_string(), "#00ff00".to_string())]);
1490 part.instances = vec![
1491 placed("own", 3.0, None),
1492 placed("over", 6.0, Some("#ff0000")),
1493 ];
1494 let mut scene = scene(part);
1495 scene.instances = scene.part.instances.clone();
1496 let (look, up) = (v3([0.0, 0.0, -1.0]), v3([0.0, 1.0, 0.0]));
1497 let camera = Camera::fitting(&scene, look, up, 400, 300).unwrap();
1498 let pixels = decoded(&picture(&scene, None, &[], &camera).unwrap());
1499 let seen = |x: f64| at(&pixels, camera.pixel(&v3([x + 0.8, 0.25, 1.0])));
1500 for x in [0.0, 3.0] {
1501 let [r, g, b] = seen(x);
1502 assert!(g > r + 60 && g > b + 60, "{:?}", [r, g, b]);
1503 }
1504 let [_, g, _] = seen(6.0);
1507 assert!(g < 20, "{g}");
1508 }
1509
1510 #[test]
1511 fn a_look_along_up_takes_another_up() {
1512 let scene = scene(block([0.0, 0.0, 0.0]));
1513 let (look, up) = (v3([0.0, 0.0, -1.0]), v3([0.0, 0.0, 1.0]));
1514 let camera = Camera::fitting(&scene, look, up, 80, 60).unwrap();
1515 assert_eq!(camera.up.to_f64_array(), [1.0, 0.0, 0.0]);
1516 assert!(Camera::fitting(&scene, Vector3::zero(), up, 80, 60).is_err());
1517 }
1518}