1use crate::color::solid_color_srgb;
18use crate::scene::RenderScene;
19
20pub const RENDERER: &str = "brep-thumb/1";
24
25pub const SIZE: u32 = 256;
27
28const SUPERSAMPLE: u32 = 2;
30
31const FILL: f64 = 0.9;
33
34const EDGE: [f32; 3] = [0.10, 0.11, 0.13];
36
37#[derive(Debug, Clone, Default)]
39pub struct Body {
40 pub positions: Vec<[f32; 3]>,
41 pub normals: Vec<[f32; 3]>,
42 pub indices: Vec<u32>,
43 pub colors: Vec<[f32; 3]>,
45 pub edges: Vec<Vec<[f32; 3]>>,
47}
48
49#[derive(Debug, Clone, Default)]
51pub struct Capture {
52 pub bodies: Vec<Body>,
53}
54
55impl Capture {
56 pub fn triangles(&self) -> usize {
57 self.bodies.iter().map(|b| b.indices.len() / 3).sum()
58 }
59
60 pub fn is_empty(&self) -> bool {
61 self.triangles() == 0
62 }
63}
64
65pub fn capture(scene: &RenderScene) -> Capture {
68 let mut bodies = Vec::new();
69 for solid in scene.solids().iter().filter(|s| s.visible) {
70 let mesh = &solid.mesh;
71 let base = solid.color_override.unwrap_or_else(|| solid_color_srgb(&solid.name).map(|c| c as f32));
72 let mut body = Body { positions: mesh.positions.clone(), normals: mesh.normals.clone(), ..Body::default() };
73 for (tri, idx) in mesh.indices.chunks_exact(3).enumerate() {
74 let face_index = mesh.face_ids.get(tri).map(|&f| f as usize);
75 if face_index.is_some_and(|f| !solid.visibility.is_face_visible(f)) {
76 continue;
77 }
78 let face = face_index.and_then(|f| solid.faces.get(f));
79 body.indices.extend_from_slice(idx);
80 body.colors.push(face.and_then(|f| f.color_override).unwrap_or(base));
81 }
82 body.edges = solid
83 .edges
84 .iter()
85 .enumerate()
86 .filter(|(i, e)| !e.centerline && solid.visibility.is_edge_visible(*i))
87 .map(|(_, e)| e.polyline.clone())
88 .collect();
89 if !body.indices.is_empty() {
90 bodies.push(body);
91 }
92 }
93 Capture { bodies }
94}
95
96fn iso_basis() -> ([f64; 3], [f64; 3], [f64; 3]) {
99 let k = 1.0 / 3f64.sqrt();
100 let back = [k, k, k];
101 let up_world = [0.0, 1.0, 0.0];
102 let right = normalize(cross(up_world, back));
103 let up = cross(back, right);
104 (right, up, back)
105}
106
107fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
108 [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
109}
110
111fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
112 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
113}
114
115fn normalize(v: [f64; 3]) -> [f64; 3] {
116 let n = dot(v, v).sqrt();
117 if n > 0.0 { [v[0] / n, v[1] / n, v[2] / n] } else { v }
118}
119
120fn f64s(p: [f32; 3]) -> [f64; 3] {
121 [p[0] as f64, p[1] as f64, p[2] as f64]
122}
123
124struct Projection {
127 right: [f64; 3],
128 up: [f64; 3],
129 back: [f64; 3],
130 center: [f64; 3],
131 scale: f64,
132 half: f64,
133}
134
135impl Projection {
136 fn fit(capture: &Capture, pixels: u32) -> Self {
137 let (right, up, back) = iso_basis();
138 let (mut lo, mut hi) = ([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]);
141 for body in &capture.bodies {
142 for &i in &body.indices {
143 let p = f64s(body.positions[i as usize]);
144 let (x, y) = (dot(p, right), dot(p, up));
145 lo = [lo[0].min(x), lo[1].min(y)];
146 hi = [hi[0].max(x), hi[1].max(y)];
147 }
148 }
149 let extent = (hi[0] - lo[0]).max(hi[1] - lo[1]).max(1e-12);
150 let mid = [(lo[0] + hi[0]) * 0.5, (lo[1] + hi[1]) * 0.5];
151 let center = [
152 right[0] * mid[0] + up[0] * mid[1],
153 right[1] * mid[0] + up[1] * mid[1],
154 right[2] * mid[0] + up[2] * mid[1],
155 ];
156 Self { right, up, back, center, scale: pixels as f64 * FILL / extent, half: pixels as f64 * 0.5 }
157 }
158
159 fn project(&self, p: [f64; 3]) -> [f64; 3] {
160 let d = [p[0] - self.center[0], p[1] - self.center[1], p[2] - self.center[2]];
161 [self.half + dot(d, self.right) * self.scale, self.half - dot(d, self.up) * self.scale, -dot(d, self.back) * self.scale]
162 }
163}
164
165pub fn render_png(capture: &Capture, size: u32) -> Option<Vec<u8>> {
168 let rgba = render_rgba(capture, size)?;
169 encode_png(&rgba, size, size).ok()
170}
171
172pub fn render_rgba(capture: &Capture, size: u32) -> Option<Vec<u8>> {
174 if capture.is_empty() || size == 0 {
175 return None;
176 }
177 let n = size * SUPERSAMPLE;
178 let w = n as usize;
179 let projection = Projection::fit(capture, n);
180 let mut depth = vec![f32::INFINITY; w * w];
181 let mut color = vec![[0f32; 4]; w * w];
182 let (right, up, back) = (projection.right, projection.up, projection.back);
185 let key = normalize([
186 -0.45 * right[0] + 0.6 * up[0] + 0.66 * back[0],
187 -0.45 * right[1] + 0.6 * up[1] + 0.66 * back[1],
188 -0.45 * right[2] + 0.6 * up[2] + 0.66 * back[2],
189 ]);
190 for body in &capture.bodies {
191 let screen: Vec<[f64; 3]> = body.positions.iter().map(|&p| projection.project(f64s(p))).collect();
192 for (tri, idx) in body.indices.chunks_exact(3).enumerate() {
193 let [a, b, c] = [idx[0] as usize, idx[1] as usize, idx[2] as usize];
194 let (pa, pb, pc) = (screen[a], screen[b], screen[c]);
195 let normal_at = |i: usize| -> [f64; 3] {
196 match body.normals.get(i) {
197 Some(&n) if n != [0.0; 3] => normalize(f64s(n)),
198 _ => {
199 let (wa, wb, wc) = (f64s(body.positions[a]), f64s(body.positions[b]), f64s(body.positions[c]));
200 normalize(cross([wb[0] - wa[0], wb[1] - wa[1], wb[2] - wa[2]], [wc[0] - wa[0], wc[1] - wa[1], wc[2] - wa[2]]))
201 }
202 }
203 };
204 let shade = |n: [f64; 3]| -> f32 {
205 let n = if dot(n, back) < 0.0 { [-n[0], -n[1], -n[2]] } else { n };
207 (0.34 + 0.56 * dot(n, key).max(0.0) + 0.10 * dot(n, back).max(0.0)) as f32
208 };
209 let (sa, sb, sc) = (shade(normal_at(a)), shade(normal_at(b)), shade(normal_at(c)));
210 let rgb = body.colors[tri];
211 fill_triangle(&mut depth, &mut color, w, [pa, pb, pc], [sa, sb, sc], rgb);
212 }
213 }
214 let bias = 2.5f32;
217 for body in &capture.bodies {
218 for line in &body.edges {
219 for seg in line.windows(2) {
220 let (p, q) = (projection.project(f64s(seg[0])), projection.project(f64s(seg[1])));
221 draw_segment(&mut depth, &mut color, w, p, q, bias);
222 }
223 }
224 }
225 let s = SUPERSAMPLE as usize;
227 let out_w = size as usize;
228 let mut out = vec![0u8; out_w * out_w * 4];
229 let inv = 1.0 / (s * s) as f32;
230 for y in 0..out_w {
231 for x in 0..out_w {
232 let mut acc = [0f32; 4];
233 for dy in 0..s {
234 for dx in 0..s {
235 let c = color[(y * s + dy) * w + x * s + dx];
236 for k in 0..3 {
238 acc[k] += c[k] * c[3];
239 }
240 acc[3] += c[3];
241 }
242 }
243 let alpha = acc[3] * inv;
244 let o = (y * out_w + x) * 4;
245 if acc[3] > 0.0 {
246 for k in 0..3 {
247 out[o + k] = ((acc[k] / acc[3]).clamp(0.0, 1.0) * 255.0).round() as u8;
248 }
249 }
250 out[o + 3] = (alpha.clamp(0.0, 1.0) * 255.0).round() as u8;
251 }
252 }
253 Some(out)
254}
255
256fn fill_triangle(depth: &mut [f32], color: &mut [[f32; 4]], w: usize, p: [[f64; 3]; 3], shade: [f32; 3], rgb: [f32; 3]) {
258 let area = (p[1][0] - p[0][0]) * (p[2][1] - p[0][1]) - (p[2][0] - p[0][0]) * (p[1][1] - p[0][1]);
259 if area.abs() < 1e-12 {
260 return;
261 }
262 let min_x = p.iter().map(|q| q[0]).fold(f64::INFINITY, f64::min).floor().max(0.0) as usize;
263 let max_x = (p.iter().map(|q| q[0]).fold(f64::NEG_INFINITY, f64::max).ceil() as i64).min(w as i64 - 1);
264 let min_y = p.iter().map(|q| q[1]).fold(f64::INFINITY, f64::min).floor().max(0.0) as usize;
265 let max_y = (p.iter().map(|q| q[1]).fold(f64::NEG_INFINITY, f64::max).ceil() as i64).min(w as i64 - 1);
266 if max_x < 0 || max_y < 0 {
267 return;
268 }
269 let inv_area = 1.0 / area;
270 for y in min_y..=max_y as usize {
271 let cy = y as f64 + 0.5;
272 for x in min_x..=max_x as usize {
273 let cx = x as f64 + 0.5;
274 let w0 = ((p[1][0] - cx) * (p[2][1] - cy) - (p[2][0] - cx) * (p[1][1] - cy)) * inv_area;
275 let w1 = ((p[2][0] - cx) * (p[0][1] - cy) - (p[0][0] - cx) * (p[2][1] - cy)) * inv_area;
276 let w2 = 1.0 - w0 - w1;
277 if w0 < 0.0 || w1 < 0.0 || w2 < 0.0 {
278 continue;
279 }
280 let z = (w0 * p[0][2] + w1 * p[1][2] + w2 * p[2][2]) as f32;
281 let i = y * w + x;
282 if z >= depth[i] {
283 continue;
284 }
285 depth[i] = z;
286 let s = w0 as f32 * shade[0] + w1 as f32 * shade[1] + w2 as f32 * shade[2];
287 color[i] = [rgb[0] * s, rgb[1] * s, rgb[2] * s, 1.0];
288 }
289 }
290}
291
292fn draw_segment(depth: &mut [f32], color: &mut [[f32; 4]], w: usize, p: [f64; 3], q: [f64; 3], bias: f32) {
294 let steps = ((q[0] - p[0]).abs().max((q[1] - p[1]).abs()).ceil() as usize).max(1);
295 for i in 0..=steps {
296 let t = i as f64 / steps as f64;
297 let (x, y, z) = (p[0] + (q[0] - p[0]) * t, p[1] + (q[1] - p[1]) * t, (p[2] + (q[2] - p[2]) * t) as f32);
298 for (dx, dy) in [(0i64, 0i64), (1, 0), (0, 1), (1, 1)] {
299 let (px, py) = (x.floor() as i64 + dx, y.floor() as i64 + dy);
300 if px < 0 || py < 0 || px >= w as i64 || py >= w as i64 {
301 continue;
302 }
303 let k = py as usize * w + px as usize;
304 if z - bias <= depth[k] {
305 color[k] = [EDGE[0], EDGE[1], EDGE[2], 1.0];
306 }
307 }
308 }
309}
310
311fn encode_png(rgba: &[u8], width: u32, height: u32) -> Result<Vec<u8>, String> {
313 let mut out = Vec::new();
314 {
315 let mut encoder = png::Encoder::new(&mut out, width, height);
316 encoder.set_color(png::ColorType::Rgba);
317 encoder.set_depth(png::BitDepth::Eight);
318 let mut writer = encoder.write_header().map_err(|error| format!("png header: {error}"))?;
319 writer.write_image_data(rgba).map_err(|error| format!("png data: {error}"))?;
320 }
321 Ok(out)
322}
323