1use geop_core_geometry::{nurb_curve::NurbCurve3D, nurb_surface::NurbSurface3D};
2use geop_core_math::{
3 geop_error::{GeopError, GeopResult},
4 primitives::TriangleFace,
5 scalars::Scalar,
6 vector::Vector3,
7};
8
9use super::Color10;
10
11fn sample_surface_grid(
12 surface: &NurbSurface3D,
13 u_min: Scalar,
14 u_max: Scalar,
15 v_min: Scalar,
16 v_max: Scalar,
17 n: usize,
18) -> GeopResult<Vec<Vector3>> {
19 let mut grid = Vec::with_capacity(n * n);
20 for j in 0..n {
21 for i in 0..n {
22 let u = if i == 0 {
23 u_min
24 } else if i == n - 1 {
25 u_max
26 } else {
27 let f = Scalar::from_ratio(i as i64, (n - 1) as i64)?;
28 u_min.add(u_max.sub(u_min).mul(f))
29 };
30 let v = if j == 0 {
31 v_min
32 } else if j == n - 1 {
33 v_max
34 } else {
35 let f = Scalar::from_ratio(j as i64, (n - 1) as i64)?;
36 v_min.add(v_max.sub(v_min).mul(f))
37 };
38 grid.push(surface.evaluate(u, v)?);
39 }
40 }
41 Ok(grid)
42}
43
44pub struct PrimitiveScene {
47 pub points: Vec<(Vector3, Color10)>,
48 pub lines: Vec<([Vector3; 2], Color10)>,
50 pub triangles: Vec<(TriangleFace, Color10)>,
51 pub triangles_transparent: Vec<(TriangleFace, Color10, f64)>,
55 pub labels: Vec<(Vector3, String, Color10)>,
58 pub cylinders: Vec<(Vector3, Vector3, f64, Color10)>,
62}
63
64impl core::fmt::Debug for PrimitiveScene {
65 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
66 write!(
67 f,
68 "PrimitiveScene({} pts, {} lines, {} tris)",
69 self.points.len(),
70 self.lines.len(),
71 self.triangles.len()
72 )
73 }
74}
75
76impl PrimitiveScene {
77 pub fn new() -> Self {
78 Self {
79 points: Vec::new(),
80 lines: Vec::new(),
81 triangles: Vec::new(),
82 triangles_transparent: Vec::new(),
83 labels: Vec::new(),
84 cylinders: Vec::new(),
85 }
86 }
87
88 pub fn add_point(&mut self, p: Vector3, c: Color10) {
89 self.points.push((p, c));
90 }
91
92 pub fn add_polyline(&mut self, points: &[Vector3], color: Color10) {
95 for w in points.windows(2) {
96 if !w[1].sub(&w[0]).norm_sq().could_be_equal(Scalar::ZERO) {
97 self.lines.push(([w[0], w[1]], color));
98 }
99 }
100 }
101
102 pub fn add_triangle(&mut self, t: TriangleFace, c: Color10) {
103 self.triangles.push((t, c));
104 }
105
106 pub fn add_triangle_transparent(&mut self, t: TriangleFace, c: Color10, opacity: f64) {
109 self.triangles_transparent.push((t, c, opacity));
110 }
111
112 pub fn add_label(&mut self, pos: Vector3, text: impl Into<String>, c: Color10) {
115 self.labels.push((pos, text.into(), c));
116 }
117
118 pub fn add_cylinder(&mut self, start: Vector3, end: Vector3, radius: f64, c: Color10) {
122 self.cylinders.push((start, end, radius, c));
123 }
124
125 pub fn add_curve(
127 &mut self,
128 curve: &NurbCurve3D,
129 t_min: Scalar,
130 t_max: Scalar,
131 color: Color10,
132 n: usize,
133 ) -> GeopResult<()> {
134 if n < 2 {
135 return Err(GeopError::new("add_curve: n must be >= 2"));
136 }
137 let mut pts = Vec::with_capacity(n);
138 for i in 0..n {
139 let t = if i == 0 {
140 t_min
141 } else if i == n - 1 {
142 t_max
143 } else {
144 let frac = Scalar::from_ratio(i as i64, (n - 1) as i64)?;
145 t_min.add(t_max.sub(t_min).mul(frac))
146 };
147 pts.push(curve.evaluate(t)?);
148 }
149 self.add_polyline(&pts, color);
150 Ok(())
151 }
152
153 #[allow(clippy::too_many_arguments)]
155 pub fn add_surface_wireframe(
156 &mut self,
157 surface: &NurbSurface3D,
158 color: Color10,
159 u_min: Scalar,
160 u_max: Scalar,
161 v_min: Scalar,
162 v_max: Scalar,
163 n: usize,
164 ) -> GeopResult<()> {
165 if n < 2 {
166 return Err(GeopError::new("add_surface_wireframe: n must be >= 2"));
167 }
168 let grid = sample_surface_grid(surface, u_min, u_max, v_min, v_max, n)?;
169 for row in grid.chunks(n) {
170 self.add_polyline(row, color);
171 }
172 for i in 0..n {
173 let column: Vec<_> = (0..n).map(|j| grid[j * n + i]).collect();
174 self.add_polyline(&column, color);
175 }
176 Ok(())
177 }
178
179 pub fn save_to_file(&self, filename: &str) -> GeopResult<()> {
181 let html = self.render_html();
182 std::fs::write(filename, html)
183 .map_err(|e| GeopError::new(format!("PrimitiveScene::save_to_file: {e}")))?;
184 Ok(())
185 }
186
187 fn render_html(&self) -> String {
188 let points_js = self
189 .points
190 .iter()
191 .map(|(p, c)| {
192 format!(
193 "[{},{},{},{}]",
194 p[0].to_f64(),
195 p[1].to_f64(),
196 p[2].to_f64(),
197 c.to_hex()
198 )
199 })
200 .collect::<Vec<_>>()
201 .join(",");
202
203 let lines_js = self
204 .lines
205 .iter()
206 .map(|([s, e], c)| {
207 format!(
208 "[{},{},{},{},{},{},{}]",
209 s[0].to_f64(),
210 s[1].to_f64(),
211 s[2].to_f64(),
212 e[0].to_f64(),
213 e[1].to_f64(),
214 e[2].to_f64(),
215 c.to_hex()
216 )
217 })
218 .collect::<Vec<_>>()
219 .join(",");
220
221 let tris_js = self
222 .triangles
223 .iter()
224 .map(|(t, c)| {
225 format!(
226 "[{},{},{},{},{},{},{},{},{},{}]",
227 t.a[0].to_f64(),
228 t.a[1].to_f64(),
229 t.a[2].to_f64(),
230 t.b[0].to_f64(),
231 t.b[1].to_f64(),
232 t.b[2].to_f64(),
233 t.c[0].to_f64(),
234 t.c[1].to_f64(),
235 t.c[2].to_f64(),
236 c.to_hex()
237 )
238 })
239 .collect::<Vec<_>>()
240 .join(",");
241
242 let tris_transparent_js = self
243 .triangles_transparent
244 .iter()
245 .map(|(t, c, opacity)| {
246 format!(
247 "[{},{},{},{},{},{},{},{},{},{},{}]",
248 t.a[0].to_f64(),
249 t.a[1].to_f64(),
250 t.a[2].to_f64(),
251 t.b[0].to_f64(),
252 t.b[1].to_f64(),
253 t.b[2].to_f64(),
254 t.c[0].to_f64(),
255 t.c[1].to_f64(),
256 t.c[2].to_f64(),
257 c.to_hex(),
258 opacity
259 )
260 })
261 .collect::<Vec<_>>()
262 .join(",");
263
264 let labels_js = self
265 .labels
266 .iter()
267 .map(|(p, text, c)| {
268 let escaped = text.replace('\\', "\\\\").replace('`', "'");
269 format!(
270 "[{},{},{},`{}`,{}]",
271 p[0].to_f64(),
272 p[1].to_f64(),
273 p[2].to_f64(),
274 escaped,
275 c.to_hex()
276 )
277 })
278 .collect::<Vec<_>>()
279 .join(",");
280
281 let cylinders_js = self
282 .cylinders
283 .iter()
284 .map(|(s, e, r, c)| {
285 format!(
286 "[{},{},{},{},{},{},{},{}]",
287 s[0].to_f64(),
288 s[1].to_f64(),
289 s[2].to_f64(),
290 e[0].to_f64(),
291 e[1].to_f64(),
292 e[2].to_f64(),
293 r,
294 c.to_hex()
295 )
296 })
297 .collect::<Vec<_>>()
298 .join(",");
299
300 format!(
301 r#"<!DOCTYPE html>
302<html><head><meta charset="utf-8">
303<title>Geop Debug Scene</title>
304<style>body{{margin:0;overflow:hidden;background:#1a1a2e}}.geop-label{{font:11px monospace;padding:0 2px;background:rgba(0,0,0,0.55);border-radius:2px;white-space:nowrap;pointer-events:none}}</style>
305<script type="importmap">{{"imports":{{"three":"https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"}}}}</script>
306</head><body>
307<script type="module">
308import * as THREE from 'three';
309import {{OrbitControls}} from 'three/addons/controls/OrbitControls.js';
310import {{CSS2DRenderer, CSS2DObject}} from 'three/addons/renderers/CSS2DRenderer.js';
311
312const renderer=new THREE.WebGLRenderer({{antialias:true}});
313renderer.setSize(window.innerWidth,window.innerHeight);
314renderer.setPixelRatio(devicePixelRatio);
315document.body.appendChild(renderer.domElement);
316
317const labelRenderer=new CSS2DRenderer();
318labelRenderer.setSize(window.innerWidth,window.innerHeight);
319labelRenderer.domElement.style.position='absolute';
320labelRenderer.domElement.style.top='0';
321labelRenderer.domElement.style.left='0';
322labelRenderer.domElement.style.pointerEvents='none';
323document.body.appendChild(labelRenderer.domElement);
324
325const scene=new THREE.Scene();
326scene.background=new THREE.Color(0x1a1a2e);
327scene.add(new THREE.AmbientLight(0xffffff,0.6));
328const dLight=new THREE.DirectionalLight(0xffffff,0.8);
329dLight.position.set(5,10,7);
330scene.add(dLight);
331
332const camera=new THREE.PerspectiveCamera(60,innerWidth/innerHeight,0.001,10000);
333const controls=new OrbitControls(camera,renderer.domElement);
334controls.enableDamping=true;
335
336const POINTS=[{points_js}];
337const LINES=[{lines_js}];
338const TRIS=[{tris_js}];
339const TRIS_TRANSPARENT=[{tris_transparent_js}];
340const LABELS=[{labels_js}];
341const CYLINDERS=[{cylinders_js}];
342
343// Points
344const ptGeo=new THREE.BufferGeometry();
345if(POINTS.length){{
346 const pos=new Float32Array(POINTS.length*3);
347 const col=new Float32Array(POINTS.length*3);
348 POINTS.forEach(([x,y,z,hex],i)=>{{
349 pos[i*3]=x;pos[i*3+1]=y;pos[i*3+2]=z;
350 const c=new THREE.Color(hex);col[i*3]=c.r;col[i*3+1]=c.g;col[i*3+2]=c.b;
351 }});
352 ptGeo.setAttribute('position',new THREE.BufferAttribute(pos,3));
353 ptGeo.setAttribute('color',new THREE.BufferAttribute(col,3));
354 scene.add(new THREE.Points(ptGeo,new THREE.PointsMaterial({{size:0.05,vertexColors:true}})));
355}}
356
357// Lines
358if(LINES.length){{
359 const pos=new Float32Array(LINES.length*6);
360 const col=new Float32Array(LINES.length*6);
361 LINES.forEach(([x1,y1,z1,x2,y2,z2,hex],i)=>{{
362 pos[i*6]=x1;pos[i*6+1]=y1;pos[i*6+2]=z1;
363 pos[i*6+3]=x2;pos[i*6+4]=y2;pos[i*6+5]=z2;
364 const c=new THREE.Color(hex);
365 col[i*6]=c.r;col[i*6+1]=c.g;col[i*6+2]=c.b;
366 col[i*6+3]=c.r;col[i*6+4]=c.g;col[i*6+5]=c.b;
367 }});
368 const geo=new THREE.BufferGeometry();
369 geo.setAttribute('position',new THREE.BufferAttribute(pos,3));
370 geo.setAttribute('color',new THREE.BufferAttribute(col,3));
371 scene.add(new THREE.LineSegments(geo,new THREE.LineBasicMaterial({{vertexColors:true}})));
372}}
373
374// Triangles
375if(TRIS.length){{
376 const pos=new Float32Array(TRIS.length*9);
377 const col=new Float32Array(TRIS.length*9);
378 TRIS.forEach(([ax,ay,az,bx,by,bz,cx,cy,cz,hex],i)=>{{
379 pos[i*9+0]=ax;pos[i*9+1]=ay;pos[i*9+2]=az;
380 pos[i*9+3]=bx;pos[i*9+4]=by;pos[i*9+5]=bz;
381 pos[i*9+6]=cx;pos[i*9+7]=cy;pos[i*9+8]=cz;
382 const c=new THREE.Color(hex);
383 for(let k=0;k<3;k++){{col[i*9+k*3]=c.r;col[i*9+k*3+1]=c.g;col[i*9+k*3+2]=c.b;}}
384 }});
385 const geo=new THREE.BufferGeometry();
386 geo.setAttribute('position',new THREE.BufferAttribute(pos,3));
387 geo.setAttribute('color',new THREE.BufferAttribute(col,3));
388 geo.computeVertexNormals();
389 scene.add(new THREE.Mesh(geo,new THREE.MeshLambertMaterial({{vertexColors:true,side:THREE.DoubleSide}})));
390}}
391
392// Transparent triangles: one mesh per distinct (color, opacity) pair — not
393// one per triangle (opacity is a per-material, not per-vertex, property in
394// three.js, so triangles can't just be vertex-colored into a single mesh
395// like TRIS above) — but a curved face rasterized at any real
396// resolution has thousands of same-colored triangles, and a separate
397// Mesh/BufferGeometry/Material per one of those tanks frame rate; grouping
398// keeps draw calls down to the number of distinct (color, opacity) pairs
399// actually used, typically a handful.
400{{
401 const groups=new Map();
402 TRIS_TRANSPARENT.forEach(([ax,ay,az,bx,by,bz,cx,cy,cz,hex,opacity])=>{{
403 const key=hex+'|'+opacity;
404 if(!groups.has(key))groups.set(key,{{hex,opacity,verts:[]}});
405 groups.get(key).verts.push(ax,ay,az,bx,by,bz,cx,cy,cz);
406 }});
407 groups.forEach(({{hex,opacity,verts}})=>{{
408 const geo=new THREE.BufferGeometry();
409 geo.setAttribute('position',new THREE.BufferAttribute(new Float32Array(verts),3));
410 geo.computeVertexNormals();
411 const mat=new THREE.MeshLambertMaterial({{color:hex,transparent:true,opacity,side:THREE.DoubleSide,depthWrite:false}});
412 scene.add(new THREE.Mesh(geo,mat));
413 }});
414}}
415
416// Cylinders (solid tubes, e.g. thick axes)
417CYLINDERS.forEach(([x1,y1,z1,x2,y2,z2,radius,hex])=>{{
418 const start=new THREE.Vector3(x1,y1,z1),end=new THREE.Vector3(x2,y2,z2);
419 const dir=new THREE.Vector3().subVectors(end,start);
420 const height=dir.length();
421 if(height<=0)return;
422 const geo=new THREE.CylinderGeometry(radius,radius,height,12);
423 const mat=new THREE.MeshLambertMaterial({{color:hex}});
424 const mesh=new THREE.Mesh(geo,mat);
425 mesh.position.copy(start).addScaledVector(dir,0.5);
426 mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0,1,0),dir.clone().normalize());
427 scene.add(mesh);
428}});
429
430// Labels (CSS2D overlays, always facing the camera). Labels anchored at the
431// same (or nearly the same) world position — e.g. a vertex and the midpoint
432// label of a very short edge touching it — would otherwise land on the same
433// screen pixels and hide each other, which reads as "nothing is there"
434// instead of "there are two things here that need a closer look". Bucketing
435// by a rounded position key and stacking each bucket's labels vertically
436// (via a per-label CSS offset on an inner span, so it stays correct every
437// frame without needing to redo the stacking on every camera move) keeps
438// every label visible.
439const labelBuckets=new Map();
440LABELS.forEach(([x,y,z,text,hex])=>{{
441 const key=x.toFixed(3)+','+y.toFixed(3)+','+z.toFixed(3);
442 const bucket=labelBuckets.get(key)||[];
443 bucket.push([x,y,z,text,hex]);
444 labelBuckets.set(key,bucket);
445}});
446labelBuckets.forEach(bucket=>{{
447 bucket.forEach(([x,y,z,text,hex],i)=>{{
448 const outer=document.createElement('div');
449 const inner=document.createElement('div');
450 inner.className='geop-label';
451 inner.textContent=text;
452 inner.style.color='#'+hex.toString(16).padStart(6,'0');
453 if(bucket.length>1){{
454 inner.style.transform=`translateY(${{i*14}}px)`;
455 inner.style.outline='1px solid #'+hex.toString(16).padStart(6,'0');
456 }}
457 outer.appendChild(inner);
458 const obj=new CSS2DObject(outer);
459 obj.position.set(x,y,z);
460 scene.add(obj);
461 }});
462}});
463
464// Fit camera to content
465const box=new THREE.Box3().setFromObject(scene);
466if(!box.isEmpty()){{
467 const center=box.getCenter(new THREE.Vector3());
468 const size=box.getSize(new THREE.Vector3()).length();
469 camera.position.copy(center).addScaledVector(new THREE.Vector3(0.5,0.5,1).normalize(),size*1.5);
470 controls.target.copy(center);
471 camera.near=size/1000;camera.far=size*100;camera.updateProjectionMatrix();
472}}
473
474window.addEventListener('resize',()=>{{
475 camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();
476 renderer.setSize(innerWidth,innerHeight);
477 labelRenderer.setSize(innerWidth,innerHeight);
478}});
479
480(function animate(){{requestAnimationFrame(animate);controls.update();renderer.render(scene,camera);labelRenderer.render(scene,camera);}})();
481</script></body></html>"#,
482 points_js = points_js,
483 lines_js = lines_js,
484 tris_js = tris_js,
485 tris_transparent_js = tris_transparent_js,
486 labels_js = labels_js,
487 )
488 }
489}
490
491impl Default for PrimitiveScene {
492 fn default() -> Self {
493 Self::new()
494 }
495}