1use axiolid_contracts::{
4 Backend, BackendDescriptor, BackendId, CancellationGranularity, ExecutionOptions,
5 ExecutionTarget, GeomError, ScratchRequirement,
6};
7use axiolid_mesh_hidden_lines_contract::{
8 HiddenLineError, HiddenLineEvidence, HiddenLineOutcome, HiddenLineRequest, HiddenLineSegment,
9 MeshHiddenLines, SourceEdge, ViewProjection, Visibility,
10};
11
12use crate::broad::{Broad, BroadPhase};
13use crate::edges::{collect, DrawEdge, MeshEdges};
14use crate::groups::resolve_groups;
15use crate::occlusion::{resolve, Resolved, Scene, Work};
16use crate::output::endpoint;
17use crate::prepare::{prepare, Prepared, ResourceNames, SharedLimits};
18
19#[derive(Debug, Default, Clone, Copy)]
35pub struct ScalarHiddenLines;
36
37impl ScalarHiddenLines {
38 pub const ID: BackendId = BackendId::new("scalar-hidden-lines");
40
41 #[must_use]
43 pub const fn new() -> Self {
44 Self
45 }
46}
47
48impl Backend for ScalarHiddenLines {
49 fn descriptor(&self) -> BackendDescriptor {
50 BackendDescriptor::new(Self::ID, ExecutionTarget::PortableCpu)
51 }
52}
53
54impl MeshHiddenLines for ScalarHiddenLines {
55 fn scratch_requirement(&self) -> ScratchRequirement {
56 ScratchRequirement::PerElement {
59 bytes_per_element: 1024,
60 }
61 }
62
63 fn cancellation_granularity(&self) -> CancellationGranularity {
64 CancellationGranularity::Incremental
65 }
66
67 fn supports_projection(&self, projection: ViewProjection) -> bool {
68 matches!(
69 projection,
70 ViewProjection::Orthographic | ViewProjection::Perspective
71 )
72 }
73
74 fn hidden_lines(
75 &self,
76 request: &HiddenLineRequest<'_>,
77 options: &ExecutionOptions,
78 ) -> Result<HiddenLineOutcome, HiddenLineError> {
79 draw(BroadPhase::Tree, request, options)
80 }
81}
82
83fn draw(
84 phase: BroadPhase,
85 request: &HiddenLineRequest<'_>,
86 options: &ExecutionOptions,
87) -> Result<HiddenLineOutcome, HiddenLineError> {
88 options.check_cancelled()?;
89 if request
90 .edges
91 .crease_cosine
92 .is_some_and(|cosine| !(-1.0..=1.0).contains(&cosine))
93 {
94 return Err(GeomError::InvalidInput(
95 "hidden-line crease cosine must lie in [-1, 1]".into(),
96 )
97 .into());
98 }
99 let limits = request.limits;
100 let Prepared {
101 verts,
102 tris,
103 caps,
104 ranges,
105 mut escalations,
106 total_triangles,
107 ..
108 } = prepare(
109 request.meshes,
110 &request.view,
111 request.region,
112 SharedLimits {
113 max_meshes: limits.max_meshes,
114 max_source_vertices: limits.max_source_vertices,
115 max_source_triangles: limits.max_source_triangles,
116 },
117 &ResourceNames::HIDDEN_LINES,
118 options,
119 )?;
120 let memory = || HiddenLineError::from(GeomError::BudgetExceeded { resource: "memory" });
121 let mut edges: Vec<DrawEdge> = Vec::new();
122 for (index, (entry, range)) in request.meshes.iter().zip(&ranges).enumerate() {
123 let groups = if request.edges.group_boundary {
125 Some(resolve_groups(entry, options, &mut escalations)?)
126 } else {
127 None
128 };
129 let input = MeshEdges {
130 index,
131 id: entry.id,
132 role: entry.role,
133 mesh: entry.mesh,
134 base: range.base,
135 first_tri: range.triangles.start,
136 groups: groups.as_deref(),
137 };
138 collect(
139 &input,
140 &verts,
141 &tris,
142 &request.edges,
143 options,
144 &mut escalations,
145 &mut edges,
146 )?;
147 }
148
149 let broad = Broad::build_view(
150 &tris,
151 phase,
152 request.view.projection == ViewProjection::Perspective,
153 );
154 let scene = Scene {
155 verts: &verts,
156 tris: &tris,
157 near: request.view.near,
158 far: request.view.far,
159 region: request.region,
160 caps: &caps,
161 broad: &broad,
162 edge_on_occludes: true,
163 perspective: request.view.projection == ViewProjection::Perspective,
164 };
165 let mut evidence = HiddenLineEvidence::input_mesh(request.meshes.len(), total_triangles);
166 evidence.selected_edges = edges.len();
167 let mut work = Work::default();
168 let mut segments: Vec<HiddenLineSegment> = Vec::new();
169 for edge in &edges {
170 options.check_cancelled()?;
171 let id = request.meshes[edge.mesh].id;
172 let resolved = resolve(&scene, edge, request.limits.max_candidate_pairs, &mut work)
173 .map_err(|error| match error {
174 GeomError::Degenerate(_) => HiddenLineError::mesh(id, error),
175 other => HiddenLineError::from(other),
176 })?;
177 let pieces = match resolved {
178 Resolved::Point => {
179 evidence.point_edges += 1;
180 continue;
181 }
182 Resolved::Clipped => {
183 evidence.clipped_edges += 1;
184 continue;
185 }
186 Resolved::Pieces(pieces) => pieces,
187 };
188 let [start, end] = edge.ends;
189 let source = SourceEdge {
190 mesh: id,
191 vertices: edge.local,
192 };
193 let first = segments.len();
194 let mut collapsed = false;
197 for piece in pieces {
198 if piece.hidden && !request.edges.hidden {
199 collapsed = false;
200 continue;
201 }
202 let round = |param| {
203 endpoint(&verts, start, end, param)
204 .map_err(|error| HiddenLineError::mesh(id, error))
205 };
206 let (from, to) = (round(piece.lo)?, round(piece.hi)?);
207 if from == to {
208 evidence.collapsed_segments += 1;
209 collapsed = true;
210 continue;
211 }
212 let visibility = if piece.hidden {
213 Visibility::Hidden
214 } else {
215 Visibility::Visible
216 };
217 if collapsed && segments.len() > first {
218 let count = segments.len();
219 let last = &mut segments[count - 1];
220 if last.visibility == visibility {
221 last.end = to;
222 collapsed = false;
223 continue;
224 }
225 }
226 collapsed = false;
227 if segments.len() >= request.limits.max_output_segments {
228 return Err(GeomError::BudgetExceeded {
229 resource: "hidden-line output segments",
230 }
231 .into());
232 }
233 segments.try_reserve(1).map_err(|_| memory())?;
234 segments.push(HiddenLineSegment::new(
235 source, edge.kind, visibility, from, to,
236 ));
237 }
238 }
239 for segment in &segments {
240 match segment.visibility {
241 Visibility::Hidden => evidence.hidden_segments += 1,
242 _ => evidence.visible_segments += 1,
243 }
244 }
245 evidence.candidate_pairs = work.candidate_pairs;
246 evidence.exact_escalations = work.escalations + escalations;
247 Ok(HiddenLineOutcome::new(request.view, segments, evidence))
248}
249
250#[doc(hidden)]
255#[derive(Debug, Default, Clone, Copy)]
256pub struct ScanHiddenLines;
257
258impl Backend for ScanHiddenLines {
259 fn descriptor(&self) -> BackendDescriptor {
260 BackendDescriptor::new(
261 BackendId::new("scalar-hidden-lines-scan"),
262 ExecutionTarget::PortableCpu,
263 )
264 }
265}
266
267impl MeshHiddenLines for ScanHiddenLines {
268 fn scratch_requirement(&self) -> ScratchRequirement {
269 ScalarHiddenLines.scratch_requirement()
270 }
271
272 fn cancellation_granularity(&self) -> CancellationGranularity {
273 CancellationGranularity::Incremental
274 }
275
276 fn supports_projection(&self, projection: ViewProjection) -> bool {
277 matches!(
278 projection,
279 ViewProjection::Orthographic | ViewProjection::Perspective
280 )
281 }
282
283 fn hidden_lines(
284 &self,
285 request: &HiddenLineRequest<'_>,
286 options: &ExecutionOptions,
287 ) -> Result<HiddenLineOutcome, HiddenLineError> {
288 draw(BroadPhase::Scan, request, options)
289 }
290}