Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

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, &region), 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, &region), 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, &region), 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, &region), 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, &region), 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, &region), 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}