Another project
1use bone_types::{
2 BrepEdgeId, BrepFaceId, BrepVertexId, SketchDimensionId, SketchEntityId, SketchRelationId,
3};
4use slotmap::Key;
5use std::collections::HashMap;
6use std::collections::hash_map::Entry;
7
8use crate::camera::{ViewportExtent, ViewportPx};
9use crate::gpu::{Gpu, PICK_BYTES_PER_PIXEL};
10use crate::{RenderError, Result};
11
12#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
13#[repr(u8)]
14pub enum EntityKindTag {
15 Point = 0,
16 Line = 1,
17 Arc = 2,
18 Circle = 3,
19 Relation = 4,
20 Dimension = 5,
21 BrepFace = 6,
22 BrepEdge = 7,
23 BrepVertex = 8,
24}
25
26impl EntityKindTag {
27 #[must_use]
28 pub const fn bits(self) -> u8 {
29 self as u8
30 }
31
32 #[must_use]
33 pub const fn from_bits(bits: u8) -> Option<Self> {
34 match bits {
35 0 => Some(Self::Point),
36 1 => Some(Self::Line),
37 2 => Some(Self::Arc),
38 3 => Some(Self::Circle),
39 4 => Some(Self::Relation),
40 5 => Some(Self::Dimension),
41 6 => Some(Self::BrepFace),
42 7 => Some(Self::BrepEdge),
43 8 => Some(Self::BrepVertex),
44 _ => None,
45 }
46 }
47
48 const fn pick_priority(self) -> PickPriority {
49 PickPriority(match self {
50 Self::Point => 0,
51 Self::Line => 1,
52 Self::Arc => 2,
53 Self::Circle => 3,
54 Self::Relation => 4,
55 Self::Dimension => 5,
56 Self::BrepVertex => 6,
57 Self::BrepEdge => 7,
58 Self::BrepFace => 8,
59 })
60 }
61}
62
63impl core::fmt::Display for EntityKindTag {
64 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
65 let name = match self {
66 Self::Point => "point",
67 Self::Line => "line",
68 Self::Arc => "arc",
69 Self::Circle => "circle",
70 Self::Relation => "relation",
71 Self::Dimension => "dimension",
72 Self::BrepFace => "brep_face",
73 Self::BrepEdge => "brep_edge",
74 Self::BrepVertex => "brep_vertex",
75 };
76 f.write_str(name)
77 }
78}
79
80#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
81struct PickPriority(u8);
82
83#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
84pub enum PickedItem {
85 Point(SketchEntityId),
86 Line(SketchEntityId),
87 Arc(SketchEntityId),
88 Circle(SketchEntityId),
89 Relation(SketchRelationId),
90 Dimension(SketchDimensionId),
91 BrepFace(BrepFaceId),
92 BrepEdge(BrepEdgeId),
93 BrepVertex(BrepVertexId),
94}
95
96impl PickedItem {
97 #[must_use]
98 pub const fn tag(self) -> EntityKindTag {
99 match self {
100 Self::Point(_) => EntityKindTag::Point,
101 Self::Line(_) => EntityKindTag::Line,
102 Self::Arc(_) => EntityKindTag::Arc,
103 Self::Circle(_) => EntityKindTag::Circle,
104 Self::Relation(_) => EntityKindTag::Relation,
105 Self::Dimension(_) => EntityKindTag::Dimension,
106 Self::BrepFace(_) => EntityKindTag::BrepFace,
107 Self::BrepEdge(_) => EntityKindTag::BrepEdge,
108 Self::BrepVertex(_) => EntityKindTag::BrepVertex,
109 }
110 }
111}
112
113#[derive(Debug, thiserror::Error)]
114pub enum PickIdError {
115 #[error("slotmap slot {slot:#x} exceeds {limit:#x}, the 28-bit PickId index field")]
116 SlotIndexOverflow { slot: u32, limit: u32 },
117 #[error("two entries collapse to packed slot index {0:#x} in the PickIndex")]
118 SlotIndexCollision(u32),
119}
120
121#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
122pub struct PickId(u32);
123
124impl PickId {
125 pub const TAG_SHIFT: u32 = 28;
126 pub const INDEX_MASK: u32 = 0x0FFF_FFFF;
127 pub const TAG_MASK: u32 = 0xF000_0000;
128 pub const NONE: Self = Self(u32::MAX);
129
130 #[must_use]
131 pub const fn from_raw(bits: u32) -> Self {
132 Self(bits)
133 }
134
135 #[must_use]
136 pub const fn raw(self) -> u32 {
137 self.0
138 }
139
140 #[must_use]
141 #[allow(
142 clippy::cast_possible_truncation,
143 reason = "right-shift by TAG_SHIFT (28) leaves the high nibble in the low 4 bits; value is in 0..16 so u8 fits"
144 )]
145 pub const fn tag(self) -> Option<EntityKindTag> {
146 EntityKindTag::from_bits((self.0 >> Self::TAG_SHIFT) as u8)
147 }
148
149 #[must_use]
150 pub const fn index(self) -> u32 {
151 self.0 & Self::INDEX_MASK
152 }
153
154 pub fn point(id: SketchEntityId) -> Result<Self, PickIdError> {
155 Self::from_keyed(EntityKindTag::Point, id)
156 }
157
158 pub fn line(id: SketchEntityId) -> Result<Self, PickIdError> {
159 Self::from_keyed(EntityKindTag::Line, id)
160 }
161
162 pub fn arc(id: SketchEntityId) -> Result<Self, PickIdError> {
163 Self::from_keyed(EntityKindTag::Arc, id)
164 }
165
166 pub fn circle(id: SketchEntityId) -> Result<Self, PickIdError> {
167 Self::from_keyed(EntityKindTag::Circle, id)
168 }
169
170 pub fn relation(id: SketchRelationId) -> Result<Self, PickIdError> {
171 Self::from_keyed(EntityKindTag::Relation, id)
172 }
173
174 pub fn dimension(id: SketchDimensionId) -> Result<Self, PickIdError> {
175 Self::from_keyed(EntityKindTag::Dimension, id)
176 }
177
178 pub fn brep_face(id: BrepFaceId) -> Result<Self, PickIdError> {
179 Self::from_keyed(EntityKindTag::BrepFace, id)
180 }
181
182 pub fn brep_edge(id: BrepEdgeId) -> Result<Self, PickIdError> {
183 Self::from_keyed(EntityKindTag::BrepEdge, id)
184 }
185
186 pub fn brep_vertex(id: BrepVertexId) -> Result<Self, PickIdError> {
187 Self::from_keyed(EntityKindTag::BrepVertex, id)
188 }
189
190 #[must_use]
191 pub fn unpack(self, index: &PickIndex) -> Option<PickedItem> {
192 let tag = self.tag()?;
193 let slot = self.index();
194 match tag {
195 EntityKindTag::Point => index.lookup_entity(slot, tag).map(PickedItem::Point),
196 EntityKindTag::Line => index.lookup_entity(slot, tag).map(PickedItem::Line),
197 EntityKindTag::Arc => index.lookup_entity(slot, tag).map(PickedItem::Arc),
198 EntityKindTag::Circle => index.lookup_entity(slot, tag).map(PickedItem::Circle),
199 EntityKindTag::Relation => index
200 .relations
201 .get(&slot)
202 .copied()
203 .map(PickedItem::Relation),
204 EntityKindTag::Dimension => index
205 .dimensions
206 .get(&slot)
207 .copied()
208 .map(PickedItem::Dimension),
209 EntityKindTag::BrepFace => index.faces.get(&slot).copied().map(PickedItem::BrepFace),
210 EntityKindTag::BrepEdge => index.edges.get(&slot).copied().map(PickedItem::BrepEdge),
211 EntityKindTag::BrepVertex => index
212 .vertices
213 .get(&slot)
214 .copied()
215 .map(PickedItem::BrepVertex),
216 }
217 }
218
219 fn from_keyed<K: Key>(tag: EntityKindTag, id: K) -> Result<Self, PickIdError> {
220 slot_index(id).map(|idx| Self::pack(tag, idx))
221 }
222
223 const fn pack(tag: EntityKindTag, idx: u32) -> Self {
224 debug_assert!(idx <= Self::INDEX_MASK);
225 Self(((tag.bits() as u32) << Self::TAG_SHIFT) | idx)
226 }
227}
228
229const _: () = assert!(PickId::TAG_MASK | PickId::INDEX_MASK == u32::MAX);
230const _: () = assert!(PickId::TAG_MASK & PickId::INDEX_MASK == 0);
231const _: () = assert!(PickId::TAG_MASK == 0xFu32 << PickId::TAG_SHIFT);
232const _: () = assert!(PickId::NONE.tag().is_none());
233
234impl core::fmt::Display for PickId {
235 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
236 match self.tag() {
237 Some(tag) => write!(f, "{tag}#{}", self.index()),
238 None => write!(f, "invalid#{:08x}", self.0),
239 }
240 }
241}
242
243#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
244pub struct PickAperture(u32);
245
246impl PickAperture {
247 pub const EXACT: Self = Self(0);
248 pub const DEFAULT: Self = Self(5);
249
250 #[must_use]
251 pub const fn new(radius_px: u32) -> Self {
252 Self(radius_px)
253 }
254
255 #[must_use]
256 pub const fn radius_px(self) -> u32 {
257 self.0
258 }
259}
260
261impl Default for PickAperture {
262 fn default() -> Self {
263 Self::DEFAULT
264 }
265}
266
267#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
268pub struct PickQuery {
269 x: ViewportPx,
270 y: ViewportPx,
271 aperture: PickAperture,
272}
273
274impl PickQuery {
275 #[must_use]
276 pub const fn new(x: ViewportPx, y: ViewportPx) -> Self {
277 Self {
278 x,
279 y,
280 aperture: PickAperture::DEFAULT,
281 }
282 }
283
284 #[must_use]
285 pub const fn with_aperture(self, aperture: PickAperture) -> Self {
286 Self { aperture, ..self }
287 }
288
289 #[must_use]
290 pub const fn x(self) -> ViewportPx {
291 self.x
292 }
293
294 #[must_use]
295 pub const fn y(self) -> ViewportPx {
296 self.y
297 }
298
299 #[must_use]
300 pub const fn aperture(self) -> PickAperture {
301 self.aperture
302 }
303}
304
305impl core::fmt::Display for PickQuery {
306 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
307 write!(f, "({}, {})", self.x, self.y)
308 }
309}
310
311pub struct Picker<'a> {
312 gpu: &'a Gpu,
313 pick: &'a wgpu::Texture,
314 staging: &'a wgpu::Buffer,
315 extent: ViewportExtent,
316 index: PickIndex,
317}
318
319impl<'a> Picker<'a> {
320 pub(crate) fn new(
321 gpu: &'a Gpu,
322 pick: &'a wgpu::Texture,
323 staging: &'a wgpu::Buffer,
324 extent: ViewportExtent,
325 index: PickIndex,
326 ) -> Self {
327 Self {
328 gpu,
329 pick,
330 staging,
331 extent,
332 index,
333 }
334 }
335
336 pub fn raw_at(&self, query: PickQuery) -> Result<PickId> {
337 let width = self.extent.width().value();
338 let height = self.extent.height().value();
339 if query.x.value() >= width || query.y.value() >= height {
340 return Err(RenderError::PickOutOfBounds {
341 query,
342 extent: self.extent,
343 });
344 }
345 let device = self.gpu.device();
346 let queue = self.gpu.queue();
347 let padded_bpr = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
348 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
349 label: Some("bone-render:pick-encoder"),
350 });
351 encoder.copy_texture_to_buffer(
352 wgpu::TexelCopyTextureInfo {
353 texture: self.pick,
354 mip_level: 0,
355 origin: wgpu::Origin3d {
356 x: query.x.value(),
357 y: query.y.value(),
358 z: 0,
359 },
360 aspect: wgpu::TextureAspect::All,
361 },
362 wgpu::TexelCopyBufferInfo {
363 buffer: self.staging,
364 layout: wgpu::TexelCopyBufferLayout {
365 offset: 0,
366 bytes_per_row: Some(padded_bpr),
367 rows_per_image: Some(1),
368 },
369 },
370 wgpu::Extent3d {
371 width: 1,
372 height: 1,
373 depth_or_array_layers: 1,
374 },
375 );
376 queue.submit(Some(encoder.finish()));
377
378 let slice = self.staging.slice(..);
379 let (tx, rx) =
380 std::sync::mpsc::sync_channel::<core::result::Result<(), wgpu::BufferAsyncError>>(1);
381 slice.map_async(wgpu::MapMode::Read, move |res| {
382 let _ = tx.send(res);
383 });
384 device
385 .poll(wgpu::PollType::wait_indefinitely())
386 .map_err(RenderError::Poll)?;
387 match rx.try_recv() {
388 Ok(Ok(())) => {}
389 Ok(Err(e)) => return Err(RenderError::Map(e)),
390 Err(_) => return Err(RenderError::MapMissing),
391 }
392 let raw = {
393 let view = slice.get_mapped_range();
394 debug_assert!(view.len() >= PICK_BYTES_PER_PIXEL as usize);
395 u32::from_le_bytes([view[0], view[1], view[2], view[3]])
396 };
397 self.staging.unmap();
398 Ok(PickId::from_raw(raw))
399 }
400
401 pub fn at(&self, query: PickQuery) -> Result<Option<PickedItem>> {
402 let radius = query.aperture.radius_px();
403 if radius == 0 {
404 let pid = self.raw_at(query)?;
405 return Ok(pid.unpack(&self.index));
406 }
407 self.bounds_check(query)?;
408 let candidates = self.read_region(query, radius)?;
409 Ok(nearest_pick(query, &candidates).and_then(|pid| pid.unpack(&self.index)))
410 }
411
412 fn bounds_check(&self, query: PickQuery) -> Result<()> {
413 let width = self.extent.width().value();
414 let height = self.extent.height().value();
415 if query.x.value() >= width || query.y.value() >= height {
416 return Err(RenderError::PickOutOfBounds {
417 query,
418 extent: self.extent,
419 });
420 }
421 Ok(())
422 }
423
424 #[allow(
425 clippy::cast_possible_wrap,
426 clippy::cast_sign_loss,
427 reason = "viewport extents and aperture radius fit i32; bounds are clamped non-negative"
428 )]
429 fn read_region(&self, query: PickQuery, radius: u32) -> Result<Vec<(i32, i32, PickId)>> {
430 let r = radius as i32;
431 let qx = query.x.value() as i32;
432 let qy = query.y.value() as i32;
433 let viewport_w = self.extent.width().value() as i32;
434 let viewport_h = self.extent.height().value() as i32;
435 let x0 = (qx - r).max(0);
436 let y0 = (qy - r).max(0);
437 let x1 = (qx + r + 1).min(viewport_w);
438 let y1 = (qy + r + 1).min(viewport_h);
439 let region_w = (x1 - x0) as u32;
440 let region_h = (y1 - y0) as u32;
441 let raw = self.copy_region_to_host(x0 as u32, y0 as u32, region_w, region_h)?;
442 let bpr = padded_row_bytes(region_w);
443 Ok(decode_region(x0, y0, region_w, region_h, bpr, &raw))
444 }
445
446 fn copy_region_to_host(
447 &self,
448 origin_x: u32,
449 origin_y: u32,
450 width: u32,
451 height: u32,
452 ) -> Result<Vec<u8>> {
453 let bpr = padded_row_bytes(width);
454 let device = self.gpu.device();
455 let queue = self.gpu.queue();
456 let staging = device.create_buffer(&wgpu::BufferDescriptor {
457 label: Some("bone-render:pick-aperture-staging"),
458 size: u64::from(bpr) * u64::from(height),
459 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
460 mapped_at_creation: false,
461 });
462 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
463 label: Some("bone-render:pick-aperture-encoder"),
464 });
465 encoder.copy_texture_to_buffer(
466 wgpu::TexelCopyTextureInfo {
467 texture: self.pick,
468 mip_level: 0,
469 origin: wgpu::Origin3d {
470 x: origin_x,
471 y: origin_y,
472 z: 0,
473 },
474 aspect: wgpu::TextureAspect::All,
475 },
476 wgpu::TexelCopyBufferInfo {
477 buffer: &staging,
478 layout: wgpu::TexelCopyBufferLayout {
479 offset: 0,
480 bytes_per_row: Some(bpr),
481 rows_per_image: Some(height),
482 },
483 },
484 wgpu::Extent3d {
485 width,
486 height,
487 depth_or_array_layers: 1,
488 },
489 );
490 queue.submit(Some(encoder.finish()));
491
492 let slice = staging.slice(..);
493 let (tx, rx) =
494 std::sync::mpsc::sync_channel::<core::result::Result<(), wgpu::BufferAsyncError>>(1);
495 slice.map_async(wgpu::MapMode::Read, move |res| {
496 let _ = tx.send(res);
497 });
498 device
499 .poll(wgpu::PollType::wait_indefinitely())
500 .map_err(RenderError::Poll)?;
501 match rx.try_recv() {
502 Ok(Ok(())) => {}
503 Ok(Err(e)) => return Err(RenderError::Map(e)),
504 Err(_) => return Err(RenderError::MapMissing),
505 }
506 let bytes = slice.get_mapped_range().to_vec();
507 staging.unmap();
508 Ok(bytes)
509 }
510}
511
512const fn padded_row_bytes(pixels: u32) -> u32 {
513 let raw = pixels * PICK_BYTES_PER_PIXEL;
514 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
515 raw.div_ceil(align) * align
516}
517
518#[allow(
519 clippy::cast_possible_wrap,
520 reason = "decoded coords are bounded by viewport extents which fit i32"
521)]
522fn decode_region(
523 origin_x: i32,
524 origin_y: i32,
525 width: u32,
526 height: u32,
527 bytes_per_row: u32,
528 raw: &[u8],
529) -> Vec<(i32, i32, PickId)> {
530 let bpr = bytes_per_row as usize;
531 let bpp = PICK_BYTES_PER_PIXEL as usize;
532 assert_eq!(
533 raw.len(),
534 bpr * height as usize,
535 "pick readback length mismatch: got {}, want {} ({} rows of {} bytes)",
536 raw.len(),
537 bpr * height as usize,
538 height,
539 bpr,
540 );
541 (0..height)
542 .flat_map(|row| (0..width).map(move |col| (row, col)))
543 .filter_map(|(row, col)| {
544 let offset = (row as usize) * bpr + (col as usize) * bpp;
545 let bytes = raw.get(offset..offset + bpp)?;
546 let pid =
547 PickId::from_raw(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]));
548 Some((origin_x + col as i32, origin_y + row as i32, pid))
549 })
550 .collect()
551}
552
553#[allow(
554 clippy::cast_possible_wrap,
555 reason = "query coords fit i32 (display pixels)"
556)]
557#[must_use]
558pub(crate) fn nearest_pick(query: PickQuery, candidates: &[(i32, i32, PickId)]) -> Option<PickId> {
559 let qx = query.x.value() as i32;
560 let qy = query.y.value() as i32;
561 candidates
562 .iter()
563 .copied()
564 .filter_map(|(px, py, pid)| pid.tag().map(|tag| (px, py, pid, tag)))
565 .min_by_key(|(px, py, _, tag)| {
566 let dx = px - qx;
567 let dy = py - qy;
568 (dx * dx + dy * dy, tag.pick_priority())
569 })
570 .map(|(_, _, pid, _)| pid)
571}
572
573impl core::fmt::Debug for Picker<'_> {
574 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
575 f.debug_struct("Picker")
576 .field("extent", &self.extent)
577 .finish_non_exhaustive()
578 }
579}
580
581#[derive(Debug, Default, Clone)]
582pub struct PickIndex {
583 entities: HashMap<u32, (SketchEntityId, EntityKindTag)>,
584 relations: HashMap<u32, SketchRelationId>,
585 dimensions: HashMap<u32, SketchDimensionId>,
586 faces: HashMap<u32, BrepFaceId>,
587 edges: HashMap<u32, BrepEdgeId>,
588 vertices: HashMap<u32, BrepVertexId>,
589}
590
591impl PickIndex {
592 pub fn build<EI, RI, DI>(
593 entities: EI,
594 relations: RI,
595 dimensions: DI,
596 ) -> Result<Self, PickIdError>
597 where
598 EI: IntoIterator<Item = (SketchEntityId, EntityKindTag)>,
599 RI: IntoIterator<Item = SketchRelationId>,
600 DI: IntoIterator<Item = SketchDimensionId>,
601 {
602 Ok(Self {
603 entities: try_collect_keyed(entities, |(id, _)| slot_index(*id))?,
604 relations: try_collect_keyed(relations, |id| slot_index(*id))?,
605 dimensions: try_collect_keyed(dimensions, |id| slot_index(*id))?,
606 ..Self::default()
607 })
608 }
609
610 pub fn build_solid<FI, EI, VI>(faces: FI, edges: EI, vertices: VI) -> Result<Self, PickIdError>
611 where
612 FI: IntoIterator<Item = BrepFaceId>,
613 EI: IntoIterator<Item = BrepEdgeId>,
614 VI: IntoIterator<Item = BrepVertexId>,
615 {
616 Ok(Self {
617 faces: try_collect_keyed(faces, |id| slot_index(*id))?,
618 edges: try_collect_keyed(edges, |id| slot_index(*id))?,
619 vertices: try_collect_keyed(vertices, |id| slot_index(*id))?,
620 ..Self::default()
621 })
622 }
623
624 fn lookup_entity(&self, slot: u32, expected: EntityKindTag) -> Option<SketchEntityId> {
625 self.entities
626 .get(&slot)
627 .filter(|(_, kind)| *kind == expected)
628 .map(|(id, _)| *id)
629 }
630}
631
632#[allow(
633 clippy::cast_possible_truncation,
634 reason = "high 32 bits are masked off before the u64-to-u32 cast, so the low 32 bits fit u32 exactly"
635)]
636fn slot_index<K: Key>(key: K) -> Result<u32, PickIdError> {
637 let slot = (key.data().as_ffi() & 0xFFFF_FFFF_u64) as u32;
638 if slot > PickId::INDEX_MASK {
639 return Err(PickIdError::SlotIndexOverflow {
640 slot,
641 limit: PickId::INDEX_MASK,
642 });
643 }
644 Ok(slot)
645}
646
647fn try_collect_keyed<I, V, F>(iter: I, slot_of: F) -> Result<HashMap<u32, V>, PickIdError>
648where
649 I: IntoIterator<Item = V>,
650 F: Fn(&V) -> Result<u32, PickIdError>,
651{
652 iter.into_iter().try_fold(HashMap::new(), |mut acc, item| {
653 let slot = slot_of(&item)?;
654 match acc.entry(slot) {
655 Entry::Vacant(v) => {
656 v.insert(item);
657 Ok(acc)
658 }
659 Entry::Occupied(_) => Err(PickIdError::SlotIndexCollision(slot)),
660 }
661 })
662}
663
664#[cfg(test)]
665mod tests {
666 use super::*;
667 use proptest::prelude::*;
668 use slotmap::{KeyData, SlotMap};
669
670 #[derive(Copy, Clone, Debug)]
671 enum TagKind {
672 Point,
673 Line,
674 Arc,
675 Circle,
676 Relation,
677 Dimension,
678 }
679
680 impl TagKind {
681 fn entity_kind(self) -> EntityKindTag {
682 match self {
683 Self::Point | Self::Relation | Self::Dimension => EntityKindTag::Point,
684 Self::Line => EntityKindTag::Line,
685 Self::Arc => EntityKindTag::Arc,
686 Self::Circle => EntityKindTag::Circle,
687 }
688 }
689 }
690
691 fn arb_tag() -> impl Strategy<Value = TagKind> {
692 prop_oneof![
693 Just(TagKind::Point),
694 Just(TagKind::Line),
695 Just(TagKind::Arc),
696 Just(TagKind::Circle),
697 Just(TagKind::Relation),
698 Just(TagKind::Dimension),
699 ]
700 }
701
702 type TestSlots = (
703 SlotMap<SketchEntityId, EntityKindTag>,
704 SlotMap<SketchRelationId, ()>,
705 SlotMap<SketchDimensionId, ()>,
706 );
707
708 fn populate_uniform(
709 n: usize,
710 entity_kind: EntityKindTag,
711 ) -> (
712 Vec<SketchEntityId>,
713 Vec<SketchRelationId>,
714 Vec<SketchDimensionId>,
715 TestSlots,
716 ) {
717 let mut entities = SlotMap::with_key();
718 let mut relations = SlotMap::with_key();
719 let mut dimensions = SlotMap::with_key();
720 let e: Vec<_> = (0..n).map(|_| entities.insert(entity_kind)).collect();
721 let r: Vec<_> = (0..n).map(|_| relations.insert(())).collect();
722 let d: Vec<_> = (0..n).map(|_| dimensions.insert(())).collect();
723 (e, r, d, (entities, relations, dimensions))
724 }
725
726 fn ok<T>(result: Result<T, PickIdError>) -> T {
727 let Ok(value) = result else {
728 panic!("test fixture should fit in PickId field");
729 };
730 value
731 }
732
733 fn build_index(slots: &TestSlots) -> PickIndex {
734 ok(PickIndex::build(
735 slots.0.iter().map(|(k, kind)| (k, *kind)),
736 slots.1.keys(),
737 slots.2.keys(),
738 ))
739 }
740
741 fn build_case(
742 tag: TagKind,
743 e: &[SketchEntityId],
744 r: &[SketchRelationId],
745 d: &[SketchDimensionId],
746 target: usize,
747 ) -> (PickId, PickedItem) {
748 match tag {
749 TagKind::Point => (ok(PickId::point(e[target])), PickedItem::Point(e[target])),
750 TagKind::Line => (ok(PickId::line(e[target])), PickedItem::Line(e[target])),
751 TagKind::Arc => (ok(PickId::arc(e[target])), PickedItem::Arc(e[target])),
752 TagKind::Circle => (ok(PickId::circle(e[target])), PickedItem::Circle(e[target])),
753 TagKind::Relation => (
754 ok(PickId::relation(r[target])),
755 PickedItem::Relation(r[target]),
756 ),
757 TagKind::Dimension => (
758 ok(PickId::dimension(d[target])),
759 PickedItem::Dimension(d[target]),
760 ),
761 }
762 }
763
764 fn forget(slots: &mut TestSlots, item: PickedItem) {
765 match item {
766 PickedItem::Point(k)
767 | PickedItem::Line(k)
768 | PickedItem::Arc(k)
769 | PickedItem::Circle(k) => {
770 slots.0.remove(k);
771 }
772 PickedItem::Relation(k) => {
773 slots.1.remove(k);
774 }
775 PickedItem::Dimension(k) => {
776 slots.2.remove(k);
777 }
778 PickedItem::BrepFace(_) | PickedItem::BrepEdge(_) | PickedItem::BrepVertex(_) => {
779 unreachable!("forget receives only sketch items")
780 }
781 }
782 }
783
784 #[test]
785 fn tag_bits_roundtrip() {
786 (0u8..9).for_each(|bits| {
787 let Some(tag) = EntityKindTag::from_bits(bits) else {
788 panic!("tag {bits} should decode");
789 };
790 assert_eq!(tag.bits(), bits);
791 });
792 }
793
794 #[test]
795 fn invalid_tag_bits_return_none() {
796 (9u8..16).for_each(|bits| {
797 assert!(EntityKindTag::from_bits(bits).is_none());
798 });
799 }
800
801 #[test]
802 fn raw_roundtrip() {
803 let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key();
804 let k = entities.insert(());
805 let pid = ok(PickId::point(k));
806 assert_eq!(PickId::from_raw(pid.raw()), pid);
807 }
808
809 #[test]
810 fn tag_and_index_split_cleanly() {
811 let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key();
812 let k = entities.insert(());
813 let pid = ok(PickId::line(k));
814 assert_eq!(pid.tag(), Some(EntityKindTag::Line));
815 assert_eq!(pid.raw() & PickId::TAG_MASK, 1u32 << PickId::TAG_SHIFT);
816 assert_eq!(pid.index(), pid.raw() & PickId::INDEX_MASK);
817 }
818
819 #[test]
820 fn display_renders_tag_and_index() {
821 let mut entities: SlotMap<SketchEntityId, ()> = SlotMap::with_key();
822 let k = entities.insert(());
823 let pid = ok(PickId::arc(k));
824 assert!(format!("{pid}").starts_with("arc#"));
825 }
826
827 #[test]
828 fn display_renders_invalid_tag() {
829 let raw = 0xF000_0000u32;
830 let pid = PickId::from_raw(raw);
831 assert_eq!(pid.tag(), None);
832 assert_eq!(format!("{pid}"), "invalid#f0000000");
833 }
834
835 #[test]
836 fn max_index_roundtrip() {
837 let raw =
838 (u32::from(EntityKindTag::Point.bits()) << PickId::TAG_SHIFT) | PickId::INDEX_MASK;
839 let pid = PickId::from_raw(raw);
840 assert_eq!(pid.tag(), Some(EntityKindTag::Point));
841 assert_eq!(pid.index(), PickId::INDEX_MASK);
842 assert_eq!(pid.raw(), raw);
843 }
844
845 #[test]
846 fn slot_overflow_is_rejected_by_constructor() {
847 let bad_slot: u32 = PickId::INDEX_MASK + 1;
848 let key = SketchEntityId::from(KeyData::from_ffi((1u64 << 32) | u64::from(bad_slot)));
849 let result = PickId::point(key);
850 assert!(matches!(
851 result,
852 Err(PickIdError::SlotIndexOverflow { slot, limit })
853 if slot == bad_slot && limit == PickId::INDEX_MASK
854 ));
855 }
856
857 #[test]
858 fn slot_overflow_is_rejected_by_pick_index_build() {
859 let bad_slot: u32 = PickId::INDEX_MASK + 1;
860 let key = SketchEntityId::from(KeyData::from_ffi((1u64 << 32) | u64::from(bad_slot)));
861 let result = PickIndex::build(
862 [(key, EntityKindTag::Point)],
863 std::iter::empty::<SketchRelationId>(),
864 std::iter::empty::<SketchDimensionId>(),
865 );
866 assert!(matches!(result, Err(PickIdError::SlotIndexOverflow { .. })));
867 }
868
869 #[test]
870 fn none_sentinel_is_invalid() {
871 assert_eq!(PickId::NONE.raw(), u32::MAX);
872 assert_eq!(PickId::NONE.tag(), None);
873 assert_eq!(PickId::NONE.unpack(&PickIndex::default()), None);
874 }
875
876 #[test]
877 fn collision_in_pick_index_build_is_rejected() {
878 let mut entities: SlotMap<SketchEntityId, EntityKindTag> = SlotMap::with_key();
879 let k = entities.insert(EntityKindTag::Point);
880 let result = PickIndex::build(
881 [(k, EntityKindTag::Point), (k, EntityKindTag::Line)],
882 std::iter::empty::<SketchRelationId>(),
883 std::iter::empty::<SketchDimensionId>(),
884 );
885 assert!(matches!(result, Err(PickIdError::SlotIndexCollision(_))));
886 }
887
888 fn build_entity_index(entities: &SlotMap<SketchEntityId, EntityKindTag>) -> PickIndex {
889 ok(PickIndex::build(
890 entities.iter().map(|(k, kind)| (k, *kind)),
891 std::iter::empty::<SketchRelationId>(),
892 std::iter::empty::<SketchDimensionId>(),
893 ))
894 }
895
896 #[test]
897 fn slot_reuse_with_same_kind_returns_new_key() {
898 let mut entities: SlotMap<SketchEntityId, EntityKindTag> = SlotMap::with_key();
899 let k1 = entities.insert(EntityKindTag::Point);
900 let pid = ok(PickId::point(k1));
901 entities.remove(k1);
902 let k2 = entities.insert(EntityKindTag::Point);
903 assert_ne!(k1, k2);
904 let index = build_entity_index(&entities);
905 assert_eq!(pid.unpack(&index), Some(PickedItem::Point(k2)));
906 }
907
908 #[test]
909 fn slot_reuse_with_different_kind_returns_none() {
910 let mut entities: SlotMap<SketchEntityId, EntityKindTag> = SlotMap::with_key();
911 let k1 = entities.insert(EntityKindTag::Point);
912 let pid = ok(PickId::point(k1));
913 entities.remove(k1);
914 let _k2 = entities.insert(EntityKindTag::Line);
915 let index = build_entity_index(&entities);
916 assert_eq!(pid.unpack(&index), None);
917 }
918
919 proptest! {
920 #[test]
921 fn unpack_roundtrip_across_all_populated(
922 tag in arb_tag(),
923 n in 1usize..32,
924 target in 0usize..32,
925 ) {
926 let target = target % n;
927 let (e, r, d, slots) = populate_uniform(n, tag.entity_kind());
928 let (pid, expected) = build_case(tag, &e, &r, &d, target);
929 let index = build_index(&slots);
930 prop_assert_eq!(pid.unpack(&index), Some(expected));
931 prop_assert_eq!(pid.tag(), Some(expected.tag()));
932 }
933
934 #[test]
935 fn removed_slot_unpacks_to_none(
936 tag in arb_tag(),
937 n in 1usize..32,
938 target in 0usize..32,
939 ) {
940 let target = target % n;
941 let (e, r, d, mut slots) = populate_uniform(n, tag.entity_kind());
942 let (pid, item) = build_case(tag, &e, &r, &d, target);
943 forget(&mut slots, item);
944 let index = build_index(&slots);
945 prop_assert!(pid.unpack(&index).is_none());
946 }
947
948 #[test]
949 fn invalid_tag_bits_unpack_to_none(bits in 9u8..16, idx in 0u32..=PickId::INDEX_MASK) {
950 let raw = (u32::from(bits) << PickId::TAG_SHIFT) | (idx & PickId::INDEX_MASK);
951 let pid = PickId::from_raw(raw);
952 prop_assert!(pid.unpack(&PickIndex::default()).is_none());
953 }
954 }
955
956 #[derive(Copy, Clone, Debug)]
957 enum BrepKind {
958 Face,
959 Edge,
960 Vertex,
961 }
962
963 fn arb_brep_kind() -> impl Strategy<Value = BrepKind> {
964 prop_oneof![
965 Just(BrepKind::Face),
966 Just(BrepKind::Edge),
967 Just(BrepKind::Vertex),
968 ]
969 }
970
971 type BrepSlots = (
972 SlotMap<BrepFaceId, ()>,
973 SlotMap<BrepEdgeId, ()>,
974 SlotMap<BrepVertexId, ()>,
975 );
976
977 fn populate_brep(
978 n: usize,
979 ) -> (
980 Vec<BrepFaceId>,
981 Vec<BrepEdgeId>,
982 Vec<BrepVertexId>,
983 BrepSlots,
984 ) {
985 let mut faces = SlotMap::with_key();
986 let mut edges = SlotMap::with_key();
987 let mut vertices = SlotMap::with_key();
988 let f: Vec<_> = (0..n).map(|_| faces.insert(())).collect();
989 let e: Vec<_> = (0..n).map(|_| edges.insert(())).collect();
990 let v: Vec<_> = (0..n).map(|_| vertices.insert(())).collect();
991 (f, e, v, (faces, edges, vertices))
992 }
993
994 fn build_brep_index(slots: &BrepSlots) -> PickIndex {
995 ok(PickIndex::build_solid(
996 slots.0.keys(),
997 slots.1.keys(),
998 slots.2.keys(),
999 ))
1000 }
1001
1002 fn build_brep_case(
1003 kind: BrepKind,
1004 f: &[BrepFaceId],
1005 e: &[BrepEdgeId],
1006 v: &[BrepVertexId],
1007 target: usize,
1008 ) -> (PickId, PickedItem) {
1009 match kind {
1010 BrepKind::Face => (
1011 ok(PickId::brep_face(f[target])),
1012 PickedItem::BrepFace(f[target]),
1013 ),
1014 BrepKind::Edge => (
1015 ok(PickId::brep_edge(e[target])),
1016 PickedItem::BrepEdge(e[target]),
1017 ),
1018 BrepKind::Vertex => (
1019 ok(PickId::brep_vertex(v[target])),
1020 PickedItem::BrepVertex(v[target]),
1021 ),
1022 }
1023 }
1024
1025 #[test]
1026 fn brep_tag_and_index_split_cleanly() {
1027 let mut faces: SlotMap<BrepFaceId, ()> = SlotMap::with_key();
1028 let k = faces.insert(());
1029 let pid = ok(PickId::brep_face(k));
1030 assert_eq!(pid.tag(), Some(EntityKindTag::BrepFace));
1031 assert_eq!(pid.raw() & PickId::TAG_MASK, 6u32 << PickId::TAG_SHIFT);
1032 assert_eq!(pid.index(), pid.raw() & PickId::INDEX_MASK);
1033 assert!(format!("{pid}").starts_with("brep_face#"));
1034 }
1035
1036 #[test]
1037 fn brep_families_resolve_by_tag_at_shared_slot() {
1038 let (f, e, v, slots) = populate_brep(1);
1039 let index = build_brep_index(&slots);
1040 assert_eq!(
1041 ok(PickId::brep_face(f[0])).unpack(&index),
1042 Some(PickedItem::BrepFace(f[0]))
1043 );
1044 assert_eq!(
1045 ok(PickId::brep_edge(e[0])).unpack(&index),
1046 Some(PickedItem::BrepEdge(e[0]))
1047 );
1048 assert_eq!(
1049 ok(PickId::brep_vertex(v[0])).unpack(&index),
1050 Some(PickedItem::BrepVertex(v[0]))
1051 );
1052 }
1053
1054 #[test]
1055 fn brep_slot_overflow_is_rejected_by_build_solid() {
1056 let bad_slot: u32 = PickId::INDEX_MASK + 1;
1057 let key = BrepFaceId::from(KeyData::from_ffi((1u64 << 32) | u64::from(bad_slot)));
1058 let result = PickIndex::build_solid(
1059 [key],
1060 std::iter::empty::<BrepEdgeId>(),
1061 std::iter::empty::<BrepVertexId>(),
1062 );
1063 assert!(matches!(result, Err(PickIdError::SlotIndexOverflow { .. })));
1064 }
1065
1066 proptest! {
1067 #[test]
1068 fn brep_unpack_roundtrip(
1069 kind in arb_brep_kind(),
1070 n in 1usize..32,
1071 target in 0usize..32,
1072 ) {
1073 let target = target % n;
1074 let (f, e, v, slots) = populate_brep(n);
1075 let (pid, expected) = build_brep_case(kind, &f, &e, &v, target);
1076 let index = build_brep_index(&slots);
1077 prop_assert_eq!(pid.unpack(&index), Some(expected));
1078 prop_assert_eq!(pid.tag(), Some(expected.tag()));
1079 }
1080
1081 #[test]
1082 fn brep_removed_slot_unpacks_to_none(
1083 kind in arb_brep_kind(),
1084 n in 1usize..32,
1085 target in 0usize..32,
1086 ) {
1087 let target = target % n;
1088 let (f, e, v, mut slots) = populate_brep(n);
1089 let (pid, _) = build_brep_case(kind, &f, &e, &v, target);
1090 match kind {
1091 BrepKind::Face => slots.0.remove(f[target]),
1092 BrepKind::Edge => slots.1.remove(e[target]),
1093 BrepKind::Vertex => slots.2.remove(v[target]),
1094 };
1095 let index = build_brep_index(&slots);
1096 prop_assert!(pid.unpack(&index).is_none());
1097 }
1098 }
1099
1100 #[test]
1101 fn pick_priority_prefers_smaller_brep_entities() {
1102 assert!(
1103 EntityKindTag::BrepVertex.pick_priority() < EntityKindTag::BrepEdge.pick_priority()
1104 );
1105 assert!(EntityKindTag::BrepEdge.pick_priority() < EntityKindTag::BrepFace.pick_priority());
1106 }
1107
1108 #[test]
1109 fn nearest_pick_prefers_vertex_over_face_at_equal_distance() {
1110 let mut faces: SlotMap<BrepFaceId, ()> = SlotMap::with_key();
1111 let mut verts: SlotMap<BrepVertexId, ()> = SlotMap::with_key();
1112 let face_pid = ok(PickId::brep_face(faces.insert(())));
1113 let vert_pid = ok(PickId::brep_vertex(verts.insert(())));
1114 let q = query_at(10, 10);
1115 assert_eq!(
1116 nearest_pick(q, &[(9, 10, face_pid), (11, 10, vert_pid)]),
1117 Some(vert_pid)
1118 );
1119 assert_eq!(
1120 nearest_pick(q, &[(9, 10, vert_pid), (11, 10, face_pid)]),
1121 Some(vert_pid)
1122 );
1123 }
1124
1125 #[test]
1126 fn nearest_pick_prefers_edge_over_face_at_equal_distance() {
1127 let mut faces: SlotMap<BrepFaceId, ()> = SlotMap::with_key();
1128 let mut edges: SlotMap<BrepEdgeId, ()> = SlotMap::with_key();
1129 let face_pid = ok(PickId::brep_face(faces.insert(())));
1130 let edge_pid = ok(PickId::brep_edge(edges.insert(())));
1131 let q = query_at(10, 10);
1132 assert_eq!(
1133 nearest_pick(q, &[(9, 10, face_pid), (11, 10, edge_pid)]),
1134 Some(edge_pid)
1135 );
1136 }
1137
1138 fn pid_for(slot: u32, tag: EntityKindTag) -> PickId {
1139 debug_assert!(slot <= PickId::INDEX_MASK);
1140 PickId::from_raw((u32::from(tag.bits()) << PickId::TAG_SHIFT) | slot)
1141 }
1142
1143 fn query_at(x: u32, y: u32) -> PickQuery {
1144 PickQuery::new(ViewportPx::new(x), ViewportPx::new(y))
1145 }
1146
1147 #[test]
1148 fn aperture_default_is_five_pixels() {
1149 assert_eq!(PickAperture::default().radius_px(), 5);
1150 assert_eq!(PickAperture::EXACT.radius_px(), 0);
1151 }
1152
1153 #[test]
1154 fn pick_query_carries_default_aperture() {
1155 let q = query_at(10, 20);
1156 assert_eq!(q.aperture(), PickAperture::DEFAULT);
1157 let exact = q.with_aperture(PickAperture::EXACT);
1158 assert_eq!(exact.aperture(), PickAperture::EXACT);
1159 assert_eq!((exact.x(), exact.y()), (q.x(), q.y()));
1160 }
1161
1162 #[test]
1163 fn nearest_pick_returns_none_when_all_empty() {
1164 let q = query_at(5, 5);
1165 let region = vec![
1166 (4, 5, PickId::NONE),
1167 (5, 5, PickId::NONE),
1168 (6, 5, PickId::NONE),
1169 ];
1170 assert_eq!(nearest_pick(q, ®ion), None);
1171 }
1172
1173 #[test]
1174 fn nearest_pick_picks_center_when_center_hits() {
1175 let q = query_at(5, 5);
1176 let center_pid = pid_for(7, EntityKindTag::Line);
1177 let neighbour_pid = pid_for(11, EntityKindTag::Line);
1178 let region = vec![
1179 (3, 5, neighbour_pid),
1180 (5, 5, center_pid),
1181 (8, 5, neighbour_pid),
1182 ];
1183 assert_eq!(nearest_pick(q, ®ion), Some(center_pid));
1184 }
1185
1186 #[test]
1187 fn nearest_pick_resolves_by_squared_distance() {
1188 let q = query_at(10, 10);
1189 let near_pid = pid_for(1, EntityKindTag::Line);
1190 let far_pid = pid_for(2, EntityKindTag::Line);
1191 let region = vec![(13, 14, far_pid), (12, 11, near_pid)];
1192 assert_eq!(nearest_pick(q, ®ion), Some(near_pid));
1193 }
1194
1195 #[test]
1196 fn nearest_pick_breaks_ties_by_iteration_order() {
1197 let q = query_at(0, 0);
1198 let first_pid = pid_for(1, EntityKindTag::Line);
1199 let second_pid = pid_for(2, EntityKindTag::Line);
1200 let region = vec![(3, 4, first_pid), (4, 3, second_pid)];
1201 assert_eq!(nearest_pick(q, ®ion), Some(first_pid));
1202 }
1203
1204 #[test]
1205 fn nearest_pick_skips_none_and_picks_only_real() {
1206 let q = query_at(20, 20);
1207 let real = pid_for(7, EntityKindTag::Arc);
1208 let region = vec![
1209 (20, 20, PickId::NONE),
1210 (22, 22, real),
1211 (25, 25, PickId::NONE),
1212 ];
1213 assert_eq!(nearest_pick(q, ®ion), Some(real));
1214 }
1215
1216 #[test]
1217 fn nearest_pick_ignores_invalid_tag() {
1218 let q = query_at(10, 10);
1219 let invalid = PickId::from_raw((10u32 << PickId::TAG_SHIFT) | 3);
1220 assert_eq!(invalid.tag(), None);
1221 assert_ne!(invalid, PickId::NONE);
1222 let real = pid_for(7, EntityKindTag::Line);
1223 let region = vec![(10, 10, invalid), (12, 11, real)];
1224 assert_eq!(nearest_pick(q, ®ion), Some(real));
1225 }
1226
1227 #[test]
1228 fn padded_row_bytes_aligns_to_wgpu_alignment() {
1229 assert_eq!(
1230 padded_row_bytes(1),
1231 wgpu::COPY_BYTES_PER_ROW_ALIGNMENT,
1232 "1 px row pads to alignment"
1233 );
1234 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
1235 let pixels = align / PICK_BYTES_PER_PIXEL;
1236 assert_eq!(
1237 padded_row_bytes(pixels),
1238 align,
1239 "exactly one alignment-worth of pixels stays at one alignment unit"
1240 );
1241 assert_eq!(
1242 padded_row_bytes(pixels + 1),
1243 align * 2,
1244 "one extra pixel triggers a second alignment unit"
1245 );
1246 }
1247
1248 #[test]
1249 fn decode_region_shifts_indices_by_origin() {
1250 let bpr_u32 = padded_row_bytes(2);
1251 let bpr = bpr_u32 as usize;
1252 let mut raw = vec![0u8; bpr * 2];
1253 let pid_a = pid_for(1, EntityKindTag::Line);
1254 let pid_b = pid_for(2, EntityKindTag::Arc);
1255 raw[0..4].copy_from_slice(&pid_a.raw().to_le_bytes());
1256 raw[bpr + 4..bpr + 8].copy_from_slice(&pid_b.raw().to_le_bytes());
1257 let decoded = decode_region(10, 20, 2, 2, bpr_u32, &raw);
1258 assert_eq!(decoded.len(), 4);
1259 assert_eq!(decoded[0], (10, 20, pid_a));
1260 assert_eq!(decoded[3], (11, 21, pid_b));
1261 }
1262}