Another project
0

Configure Feed

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

feat(render): sdf text, sketch preview, surface picker

Lewis: May this revision serve well! <lu5a@proton.me>

author
Lewis
date (May 8, 2026, 4:35 PM +0300) commit 2e50ed7c parent 2b4af203 change-id rttlymot
+574 -66
+8 -4
crates/bone-render/src/lib.rs
··· 3 3 pub mod gpu; 4 4 pub mod pick; 5 5 pub mod pipelines; 6 + pub mod preview; 6 7 pub mod scene; 7 8 pub mod snapshot; 8 9 pub mod surface; ··· 12 13 pub use gpu::{BackendTag, Capabilities, Gpu, OffscreenContext}; 13 14 pub use pick::{EntityKindTag, PickId, PickIdError, PickIndex, PickQuery, PickedItem, Picker}; 14 15 pub use pipelines::{ 15 - ArcPipeline, ChromeInstance, ChromePipeline, GlyphPipeline, GridPipeline, LinesPipeline, 16 - TextPipeline, 16 + ArcPipeline, ChromeInstance, ChromePipeline, ChromeTextPipeline, GlyphPipeline, GridPipeline, 17 + LinesPipeline, SdfGlyphInstance, TextPipeline, 17 18 }; 19 + pub use preview::SketchPreview; 18 20 pub use scene::{ 19 21 RelationGlyphKind, SceneArc, SceneCircle, SceneDimension, SceneLine, ScenePoint, 20 22 SceneRelationGlyph, SketchScene, ··· 95 97 color_view: &wgpu::TextureView, 96 98 pick_view: &wgpu::TextureView, 97 99 scene: &SketchScene, 100 + preview: &SketchPreview, 98 101 camera: Camera2, 99 102 style: &Style, 100 103 ) { ··· 103 106 self.arcs 104 107 .draw(encoder, color_view, pick_view, camera, style, scene); 105 108 self.lines 106 - .draw(encoder, color_view, pick_view, camera, style, scene); 109 + .draw(encoder, color_view, pick_view, camera, style, scene, preview); 107 110 self.glyphs 108 111 .draw(encoder, color_view, pick_view, camera, style, scene); 109 112 self.text ··· 123 126 "camera extent must match offscreen context extent", 124 127 ); 125 128 self.prepare(scene, style); 129 + let preview = SketchPreview::empty(); 126 130 ctx.render(|encoder, color_view, pick_view| { 127 - self.encode_passes(encoder, color_view, pick_view, scene, camera, style); 131 + self.encode_passes(encoder, color_view, pick_view, scene, &preview, camera, style); 128 132 }) 129 133 } 130 134 }
+305
crates/bone-render/src/pipelines/chrome_text.rs
··· 1 + use crate::gpu::Gpu; 2 + 3 + const ATLAS_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R8Unorm; 4 + const INITIAL_INSTANCE_CAP: u64 = 256; 5 + 6 + #[repr(C, align(16))] 7 + #[derive(Copy, Clone, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] 8 + pub struct SdfGlyphInstance { 9 + pub rect_xywh_px: [f32; 4], 10 + pub uv_min: [f32; 2], 11 + pub uv_max: [f32; 2], 12 + pub color_premul_rgba: [f32; 4], 13 + } 14 + 15 + const INSTANCE_STRIDE: u64 = core::mem::size_of::<SdfGlyphInstance>() as u64; 16 + 17 + const INSTANCE_ATTRS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![ 18 + 0 => Float32x4, 19 + 1 => Float32x2, 20 + 2 => Float32x2, 21 + 3 => Float32x4, 22 + ]; 23 + 24 + #[repr(C, align(16))] 25 + #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] 26 + struct Frame { 27 + viewport_px: [f32; 2], 28 + _pad: [f32; 2], 29 + } 30 + 31 + const FRAME_SIZE: u64 = core::mem::size_of::<Frame>() as u64; 32 + 33 + pub struct ChromeTextPipeline { 34 + device: wgpu::Device, 35 + queue: wgpu::Queue, 36 + pipeline: wgpu::RenderPipeline, 37 + uniform_buffer: wgpu::Buffer, 38 + bind_group: wgpu::BindGroup, 39 + atlas: wgpu::Texture, 40 + atlas_extent: u32, 41 + atlas_version: Option<u64>, 42 + instance_buffer: wgpu::Buffer, 43 + instance_capacity: u64, 44 + } 45 + 46 + impl ChromeTextPipeline { 47 + #[must_use] 48 + pub fn new(gpu: &Gpu, color_format: wgpu::TextureFormat, atlas_extent: u32) -> Self { 49 + let device = gpu.device().clone(); 50 + let queue = gpu.queue().clone(); 51 + let atlas = create_atlas_texture(&device, atlas_extent); 52 + let atlas_view = atlas.create_view(&wgpu::TextureViewDescriptor::default()); 53 + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { 54 + label: Some("bone-render:chrome-text-sampler"), 55 + address_mode_u: wgpu::AddressMode::ClampToEdge, 56 + address_mode_v: wgpu::AddressMode::ClampToEdge, 57 + address_mode_w: wgpu::AddressMode::ClampToEdge, 58 + mag_filter: wgpu::FilterMode::Linear, 59 + min_filter: wgpu::FilterMode::Linear, 60 + mipmap_filter: wgpu::MipmapFilterMode::Nearest, 61 + ..Default::default() 62 + }); 63 + let bind_group_layout = create_bind_group_layout(&device); 64 + let pipeline = create_pipeline(&device, &bind_group_layout, color_format); 65 + let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { 66 + label: Some("bone-render:chrome-text-uniform"), 67 + size: FRAME_SIZE, 68 + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, 69 + mapped_at_creation: false, 70 + }); 71 + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { 72 + label: Some("bone-render:chrome-text-bg"), 73 + layout: &bind_group_layout, 74 + entries: &[ 75 + wgpu::BindGroupEntry { 76 + binding: 0, 77 + resource: uniform_buffer.as_entire_binding(), 78 + }, 79 + wgpu::BindGroupEntry { 80 + binding: 1, 81 + resource: wgpu::BindingResource::TextureView(&atlas_view), 82 + }, 83 + wgpu::BindGroupEntry { 84 + binding: 2, 85 + resource: wgpu::BindingResource::Sampler(&sampler), 86 + }, 87 + ], 88 + }); 89 + let instance_buffer = create_instance_buffer(&device, INITIAL_INSTANCE_CAP); 90 + Self { 91 + device, 92 + queue, 93 + pipeline, 94 + uniform_buffer, 95 + bind_group, 96 + atlas, 97 + atlas_extent, 98 + atlas_version: None, 99 + instance_buffer, 100 + instance_capacity: INITIAL_INSTANCE_CAP, 101 + } 102 + } 103 + 104 + pub fn draw( 105 + &mut self, 106 + encoder: &mut wgpu::CommandEncoder, 107 + color_view: &wgpu::TextureView, 108 + viewport_px: [f32; 2], 109 + atlas_pixels: &[u8], 110 + atlas_version: u64, 111 + instances: &[SdfGlyphInstance], 112 + ) { 113 + if instances.is_empty() { 114 + return; 115 + } 116 + if self.atlas_version != Some(atlas_version) { 117 + self.upload_atlas(atlas_pixels); 118 + self.atlas_version = Some(atlas_version); 119 + } 120 + let frame = Frame { 121 + viewport_px, 122 + _pad: [0.0, 0.0], 123 + }; 124 + self.queue 125 + .write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&frame)); 126 + let needed = instances.len() as u64; 127 + if needed > self.instance_capacity { 128 + let new_cap = needed.next_power_of_two().max(self.instance_capacity * 2); 129 + self.instance_buffer = create_instance_buffer(&self.device, new_cap); 130 + self.instance_capacity = new_cap; 131 + } 132 + self.queue 133 + .write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(instances)); 134 + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 135 + label: Some("bone-render:chrome-text-pass"), 136 + color_attachments: &[Some(wgpu::RenderPassColorAttachment { 137 + view: color_view, 138 + resolve_target: None, 139 + depth_slice: None, 140 + ops: wgpu::Operations { 141 + load: wgpu::LoadOp::Load, 142 + store: wgpu::StoreOp::Store, 143 + }, 144 + })], 145 + depth_stencil_attachment: None, 146 + timestamp_writes: None, 147 + occlusion_query_set: None, 148 + multiview_mask: None, 149 + }); 150 + pass.set_pipeline(&self.pipeline); 151 + pass.set_bind_group(0, &self.bind_group, &[]); 152 + let used_bytes = needed * INSTANCE_STRIDE; 153 + pass.set_vertex_buffer(0, self.instance_buffer.slice(0..used_bytes)); 154 + let Ok(count) = u32::try_from(needed) else { 155 + panic!("chrome text instance count {needed} exceeds u32::MAX"); 156 + }; 157 + pass.draw(0..6, 0..count); 158 + } 159 + 160 + fn upload_atlas(&self, pixels: &[u8]) { 161 + let bytes_per_row = self.atlas_extent; 162 + self.queue.write_texture( 163 + wgpu::TexelCopyTextureInfo { 164 + texture: &self.atlas, 165 + mip_level: 0, 166 + origin: wgpu::Origin3d::ZERO, 167 + aspect: wgpu::TextureAspect::All, 168 + }, 169 + pixels, 170 + wgpu::TexelCopyBufferLayout { 171 + offset: 0, 172 + bytes_per_row: Some(bytes_per_row), 173 + rows_per_image: Some(self.atlas_extent), 174 + }, 175 + wgpu::Extent3d { 176 + width: self.atlas_extent, 177 + height: self.atlas_extent, 178 + depth_or_array_layers: 1, 179 + }, 180 + ); 181 + } 182 + } 183 + 184 + impl core::fmt::Debug for ChromeTextPipeline { 185 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 186 + f.debug_struct("ChromeTextPipeline") 187 + .field("atlas_extent", &self.atlas_extent) 188 + .field("atlas_version", &self.atlas_version) 189 + .field("instance_capacity", &self.instance_capacity) 190 + .finish_non_exhaustive() 191 + } 192 + } 193 + 194 + fn create_atlas_texture(device: &wgpu::Device, extent: u32) -> wgpu::Texture { 195 + device.create_texture(&wgpu::TextureDescriptor { 196 + label: Some("bone-render:chrome-text-atlas"), 197 + size: wgpu::Extent3d { 198 + width: extent, 199 + height: extent, 200 + depth_or_array_layers: 1, 201 + }, 202 + mip_level_count: 1, 203 + sample_count: 1, 204 + dimension: wgpu::TextureDimension::D2, 205 + format: ATLAS_FORMAT, 206 + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, 207 + view_formats: &[], 208 + }) 209 + } 210 + 211 + fn create_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { 212 + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 213 + label: Some("bone-render:chrome-text-bgl"), 214 + entries: &[ 215 + wgpu::BindGroupLayoutEntry { 216 + binding: 0, 217 + visibility: wgpu::ShaderStages::VERTEX_FRAGMENT, 218 + ty: wgpu::BindingType::Buffer { 219 + ty: wgpu::BufferBindingType::Uniform, 220 + has_dynamic_offset: false, 221 + min_binding_size: wgpu::BufferSize::new(FRAME_SIZE), 222 + }, 223 + count: None, 224 + }, 225 + wgpu::BindGroupLayoutEntry { 226 + binding: 1, 227 + visibility: wgpu::ShaderStages::FRAGMENT, 228 + ty: wgpu::BindingType::Texture { 229 + sample_type: wgpu::TextureSampleType::Float { filterable: true }, 230 + view_dimension: wgpu::TextureViewDimension::D2, 231 + multisampled: false, 232 + }, 233 + count: None, 234 + }, 235 + wgpu::BindGroupLayoutEntry { 236 + binding: 2, 237 + visibility: wgpu::ShaderStages::FRAGMENT, 238 + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), 239 + count: None, 240 + }, 241 + ], 242 + }) 243 + } 244 + 245 + fn create_pipeline( 246 + device: &wgpu::Device, 247 + bind_group_layout: &wgpu::BindGroupLayout, 248 + color_format: wgpu::TextureFormat, 249 + ) -> wgpu::RenderPipeline { 250 + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { 251 + label: Some("bone-render:chrome-text-shader"), 252 + source: wgpu::ShaderSource::Wgsl(include_str!("chrome_text.wgsl").into()), 253 + }); 254 + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 255 + label: Some("bone-render:chrome-text-layout"), 256 + bind_group_layouts: &[Some(bind_group_layout)], 257 + immediate_size: 0, 258 + }); 259 + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 260 + label: Some("bone-render:chrome-text-pipeline"), 261 + layout: Some(&pipeline_layout), 262 + vertex: wgpu::VertexState { 263 + module: &shader, 264 + entry_point: Some("vs"), 265 + compilation_options: wgpu::PipelineCompilationOptions::default(), 266 + buffers: &[wgpu::VertexBufferLayout { 267 + array_stride: INSTANCE_STRIDE, 268 + step_mode: wgpu::VertexStepMode::Instance, 269 + attributes: &INSTANCE_ATTRS, 270 + }], 271 + }, 272 + fragment: Some(wgpu::FragmentState { 273 + module: &shader, 274 + entry_point: Some("fs"), 275 + compilation_options: wgpu::PipelineCompilationOptions::default(), 276 + targets: &[Some(wgpu::ColorTargetState { 277 + format: color_format, 278 + blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), 279 + write_mask: wgpu::ColorWrites::ALL, 280 + })], 281 + }), 282 + primitive: wgpu::PrimitiveState { 283 + topology: wgpu::PrimitiveTopology::TriangleList, 284 + strip_index_format: None, 285 + front_face: wgpu::FrontFace::Ccw, 286 + cull_mode: None, 287 + polygon_mode: wgpu::PolygonMode::Fill, 288 + conservative: false, 289 + unclipped_depth: false, 290 + }, 291 + depth_stencil: None, 292 + multisample: wgpu::MultisampleState::default(), 293 + multiview_mask: None, 294 + cache: None, 295 + }) 296 + } 297 + 298 + fn create_instance_buffer(device: &wgpu::Device, capacity: u64) -> wgpu::Buffer { 299 + device.create_buffer(&wgpu::BufferDescriptor { 300 + label: Some("bone-render:chrome-text-instances"), 301 + size: capacity * INSTANCE_STRIDE, 302 + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, 303 + mapped_at_creation: false, 304 + }) 305 + }
+55
crates/bone-render/src/pipelines/chrome_text.wgsl
··· 1 + struct Frame { 2 + viewport_px: vec2<f32>, 3 + _pad: vec2<f32>, 4 + }; 5 + 6 + @group(0) @binding(0) var<uniform> u: Frame; 7 + @group(0) @binding(1) var atlas: texture_2d<f32>; 8 + @group(0) @binding(2) var atlas_sampler: sampler; 9 + 10 + struct VsOut { 11 + @builtin(position) clip: vec4<f32>, 12 + @location(0) uv: vec2<f32>, 13 + @location(1) color: vec4<f32>, 14 + }; 15 + 16 + @vertex 17 + fn vs( 18 + @builtin(vertex_index) vid: u32, 19 + @location(0) rect_xywh_px: vec4<f32>, 20 + @location(1) uv_min: vec2<f32>, 21 + @location(2) uv_max: vec2<f32>, 22 + @location(3) color: vec4<f32>, 23 + ) -> VsOut { 24 + var corners = array<vec2<f32>, 6>( 25 + vec2<f32>(0.0, 0.0), 26 + vec2<f32>(1.0, 0.0), 27 + vec2<f32>(0.0, 1.0), 28 + vec2<f32>(1.0, 0.0), 29 + vec2<f32>(1.0, 1.0), 30 + vec2<f32>(0.0, 1.0), 31 + ); 32 + let c = corners[vid]; 33 + let pos_px = rect_xywh_px.xy + c * rect_xywh_px.zw; 34 + let uv = mix(uv_min, uv_max, c); 35 + let ndc = vec2<f32>( 36 + (pos_px.x / u.viewport_px.x) * 2.0 - 1.0, 37 + 1.0 - (pos_px.y / u.viewport_px.y) * 2.0, 38 + ); 39 + var out: VsOut; 40 + out.clip = vec4<f32>(ndc, 0.0, 1.0); 41 + out.uv = uv; 42 + out.color = color; 43 + return out; 44 + } 45 + 46 + @fragment 47 + fn fs(in: VsOut) -> @location(0) vec4<f32> { 48 + let s = textureSample(atlas, atlas_sampler, in.uv).r; 49 + let aa = max(fwidth(s), 1.0e-4); 50 + let alpha = 1.0 - smoothstep(0.5 - aa, 0.5 + aa, s); 51 + if alpha <= 0.0 { 52 + discard; 53 + } 54 + return in.color * alpha; 55 + }
+49 -3
crates/bone-render/src/pipelines/lines.rs
··· 2 2 3 3 use crate::camera::Camera2; 4 4 use crate::gpu::{Gpu, PICK_FORMAT}; 5 + use crate::pick::PickId; 5 6 use crate::pipelines::{CONSTRUCTION_BIT, FRAME_UNIFORM_SIZE, build_frame_uniform}; 7 + use crate::preview::SketchPreview; 6 8 use crate::scene::{SceneLine, ScenePoint, SketchScene}; 7 9 use crate::snapshot::Style; 10 + use bone_types::Point2; 8 11 9 12 #[repr(C)] 10 13 #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] ··· 128 131 camera: Camera2, 129 132 style: &Style, 130 133 scene: &SketchScene, 134 + preview: &SketchPreview, 131 135 ) { 132 - let instances = build_instances(scene, style); 136 + let instances = build_instances(scene, preview, style); 133 137 if instances.is_empty() { 134 138 return; 135 139 } ··· 195 199 4 => Uint32, 196 200 ]; 197 201 198 - fn build_instances(scene: &SketchScene, style: &Style) -> Vec<LineInstance> { 202 + fn build_instances( 203 + scene: &SketchScene, 204 + preview: &SketchPreview, 205 + style: &Style, 206 + ) -> Vec<LineInstance> { 199 207 let lines = scene.lines().iter().map(|l| line_instance(*l, style)); 200 208 let points = scene.points().iter().map(|p| point_instance(*p, style)); 201 - lines.chain(points).collect() 209 + let preview_line = preview 210 + .rubber_band 211 + .iter() 212 + .map(|(a, b)| preview_line_instance(*a, *b, style)); 213 + let preview_anchor = preview 214 + .anchor 215 + .iter() 216 + .map(|p| preview_point_instance(*p, style)); 217 + lines 218 + .chain(points) 219 + .chain(preview_line) 220 + .chain(preview_anchor) 221 + .collect() 202 222 } 203 223 204 224 #[allow(clippy::cast_possible_truncation)] ··· 231 251 style_bits: 0, 232 252 } 233 253 } 254 + 255 + #[allow(clippy::cast_possible_truncation)] 256 + fn preview_line_instance(a: Point2, b: Point2, style: &Style) -> LineInstance { 257 + let (ax, ay) = a.coords_mm(); 258 + let (bx, by) = b.coords_mm(); 259 + LineInstance { 260 + a: [ax as f32, ay as f32], 261 + b: [bx as f32, by as f32], 262 + half_width_px: style.strokes().stroke_width_px() * 0.5, 263 + pick_id: PickId::NONE.raw(), 264 + style_bits: 0, 265 + } 266 + } 267 + 268 + #[allow(clippy::cast_possible_truncation)] 269 + fn preview_point_instance(at: Point2, style: &Style) -> LineInstance { 270 + let (x, y) = at.coords_mm(); 271 + let xy = [x as f32, y as f32]; 272 + LineInstance { 273 + a: xy, 274 + b: xy, 275 + half_width_px: style.strokes().point_radius_px(), 276 + pick_id: PickId::NONE.raw(), 277 + style_bits: 0, 278 + } 279 + }
+3
crates/bone-render/src/pipelines/mod.rs
··· 1 1 pub mod arc; 2 2 pub mod chrome; 3 + pub mod chrome_text; 3 4 pub mod glyph; 4 5 pub mod grid; 5 6 pub mod lines; 6 7 pub mod text; 8 + mod text_common; 7 9 8 10 pub use arc::ArcPipeline; 9 11 pub use chrome::{ChromeInstance, ChromePipeline}; 12 + pub use chrome_text::{ChromeTextPipeline, SdfGlyphInstance}; 10 13 pub use glyph::GlyphPipeline; 11 14 pub use grid::GridPipeline; 12 15 pub use lines::LinesPipeline;
+7 -59
crates/bone-render/src/pipelines/text.rs
··· 1 1 use std::collections::HashMap; 2 2 3 - use bone_text::{ 4 - FontFace, FontWeight, ShapeRequest, ShapedText, Shaper, TessellatedOutline, append_outline, 5 - load_font, tessellate_path, 6 - }; 3 + use bone_text::{FontFace, ShapedLine, ShapedText, Shaper, TessellatedOutline, load_font}; 7 4 use bone_types::SketchDimensionId; 8 - use lyon_tessellation::{FillTessellator, path::Path as LyonPath}; 9 - use swash::{ 10 - FontRef, 11 - scale::{ScaleContext, outline::Outline}, 12 - zeno::Point as ZenoPoint, 13 - }; 5 + use lyon_tessellation::FillTessellator; 6 + use swash::{FontRef, scale::ScaleContext, zeno::Point as ZenoPoint}; 14 7 use wgpu::util::DeviceExt; 15 8 16 9 use crate::camera::Camera2; 17 10 use crate::gpu::{Gpu, PICK_FORMAT}; 11 + use crate::pipelines::text_common::{shape_line, tessellate_at}; 18 12 use crate::scene::SketchScene; 19 13 use crate::snapshot::{Style, TextStyle}; 20 14 ··· 344 338 scale_ctx: &mut ScaleContext, 345 339 fill: &mut FillTessellator, 346 340 ) -> TessellatedOutline { 347 - let layout = shaper.shape( 348 - text, 349 - ShapeRequest { 350 - face: FontFace::Mono, 351 - size_px, 352 - weight: FontWeight::Regular, 353 - line_height_px: 0.0, 354 - letter_spacing_px: 0.0, 355 - max_width: None, 356 - }, 357 - ); 341 + let layout = shape_line(text, size_px, FontFace::Mono, shaper); 358 342 let metrics = font.metrics(&[]).scale(size_px); 359 343 let center = label_center(&layout, metrics.cap_height); 360 - let path = build_centered_path(&layout, scale_ctx, font, center); 361 - tessellate_path(&path, fill).unwrap_or_else(|e| { 362 - tracing::warn!(error = %e, text, size_px, "text tessellation failed"); 363 - TessellatedOutline::default() 364 - }) 344 + tessellate_at(&layout, font, scale_ctx, fill, center) 365 345 } 366 346 367 347 fn label_center(layout: &ShapedText, cap_height: f32) -> ZenoPoint { 368 348 let visible_advance = layout 369 349 .lines 370 350 .first() 371 - .map_or(0.0, bone_text::ShapedLine::visible_advance_px); 351 + .map_or(0.0, ShapedLine::visible_advance_px); 372 352 ZenoPoint::new(-visible_advance * 0.5, -cap_height * 0.5) 373 353 } 374 - 375 - fn build_centered_path( 376 - layout: &ShapedText, 377 - scale_ctx: &mut ScaleContext, 378 - font: &FontRef<'_>, 379 - center: ZenoPoint, 380 - ) -> LyonPath { 381 - let mut scaler = scale_ctx.builder(*font).size(layout.font_size_px).build(); 382 - layout 383 - .lines 384 - .iter() 385 - .flat_map(|line| line.runs.iter()) 386 - .flat_map(|run| { 387 - let origin = run.origin_x_px; 388 - run.glyphs.iter().map(move |g| (origin + g.x_px, g.id)) 389 - }) 390 - .fold(LyonPath::svg_builder(), |mut builder, (x, id)| { 391 - let Ok(glyph_id_u16) = u16::try_from(id.raw()) else { 392 - return builder; 393 - }; 394 - let mut outline = Outline::new(); 395 - if scaler.scale_outline_into(glyph_id_u16, &mut outline) { 396 - append_outline( 397 - &mut builder, 398 - &outline, 399 - ZenoPoint::new(center.x + x, center.y), 400 - ); 401 - } 402 - builder 403 - }) 404 - .build() 405 - }
+75
crates/bone-render/src/pipelines/text_common.rs
··· 1 + use bone_text::{ 2 + FontFace, FontWeight, ShapeRequest, ShapedText, Shaper, TessellatedOutline, append_outline, 3 + tessellate_path, 4 + }; 5 + use lyon_tessellation::{FillTessellator, path::Path as LyonPath}; 6 + use swash::{ 7 + FontRef, 8 + scale::{ScaleContext, outline::Outline}, 9 + zeno::Point as ZenoPoint, 10 + }; 11 + 12 + pub(crate) fn shape_line( 13 + text: &str, 14 + size_px: f32, 15 + face: FontFace, 16 + shaper: &mut Shaper, 17 + ) -> ShapedText { 18 + shaper.shape( 19 + text, 20 + ShapeRequest { 21 + face, 22 + size_px, 23 + weight: FontWeight::Regular, 24 + line_height_px: 0.0, 25 + letter_spacing_px: 0.0, 26 + max_width: None, 27 + }, 28 + ) 29 + } 30 + 31 + pub(crate) fn tessellate_at( 32 + layout: &ShapedText, 33 + font: &FontRef<'_>, 34 + scale_ctx: &mut ScaleContext, 35 + fill: &mut FillTessellator, 36 + origin: ZenoPoint, 37 + ) -> TessellatedOutline { 38 + let path = build_path_at(layout, scale_ctx, font, origin); 39 + tessellate_path(&path, fill).unwrap_or_else(|e| { 40 + tracing::warn!(error = %e, "text tessellation failed"); 41 + TessellatedOutline::default() 42 + }) 43 + } 44 + 45 + fn build_path_at( 46 + layout: &ShapedText, 47 + scale_ctx: &mut ScaleContext, 48 + font: &FontRef<'_>, 49 + origin: ZenoPoint, 50 + ) -> LyonPath { 51 + let mut scaler = scale_ctx.builder(*font).size(layout.font_size_px).build(); 52 + layout 53 + .lines 54 + .iter() 55 + .flat_map(|line| line.runs.iter()) 56 + .flat_map(|run| { 57 + let run_origin = run.origin_x_px; 58 + run.glyphs.iter().map(move |g| (run_origin + g.x_px, g.id)) 59 + }) 60 + .fold(LyonPath::svg_builder(), |mut builder, (x, id)| { 61 + let Ok(glyph_id_u16) = u16::try_from(id.raw()) else { 62 + return builder; 63 + }; 64 + let mut outline = Outline::new(); 65 + if scaler.scale_outline_into(glyph_id_u16, &mut outline) { 66 + append_outline( 67 + &mut builder, 68 + &outline, 69 + ZenoPoint::new(origin.x + x, origin.y), 70 + ); 71 + } 72 + builder 73 + }) 74 + .build() 75 + }
+52
crates/bone-render/src/preview.rs
··· 1 + use bone_types::Point2; 2 + 3 + #[derive(Copy, Clone, Debug, Default, PartialEq)] 4 + pub struct SketchPreview { 5 + pub anchor: Option<Point2>, 6 + pub rubber_band: Option<(Point2, Point2)>, 7 + } 8 + 9 + impl SketchPreview { 10 + #[must_use] 11 + pub const fn empty() -> Self { 12 + Self { 13 + anchor: None, 14 + rubber_band: None, 15 + } 16 + } 17 + 18 + #[must_use] 19 + pub const fn is_empty(&self) -> bool { 20 + self.anchor.is_none() && self.rubber_band.is_none() 21 + } 22 + } 23 + 24 + #[cfg(test)] 25 + mod tests { 26 + use super::SketchPreview; 27 + use bone_types::Point2; 28 + 29 + #[test] 30 + fn empty_has_no_overlay() { 31 + let preview = SketchPreview::empty(); 32 + assert!(preview.is_empty()); 33 + } 34 + 35 + #[test] 36 + fn anchor_only_is_non_empty() { 37 + let preview = SketchPreview { 38 + anchor: Some(Point2::from_mm(1.0, 2.0)), 39 + rubber_band: None, 40 + }; 41 + assert!(!preview.is_empty()); 42 + } 43 + 44 + #[test] 45 + fn rubber_band_only_is_non_empty() { 46 + let preview = SketchPreview { 47 + anchor: None, 48 + rubber_band: Some((Point2::from_mm(0.0, 0.0), Point2::from_mm(1.0, 1.0))), 49 + }; 50 + assert!(!preview.is_empty()); 51 + } 52 + }
+20
crates/bone-render/src/surface.rs
··· 1 1 use crate::camera::ViewportExtent; 2 2 use crate::gpu::{Capabilities, Gpu}; 3 + use crate::pick::{PickIndex, Picker}; 3 4 4 5 const PICK_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R32Uint; 5 6 ··· 24 25 surface: wgpu::Surface<'static>, 25 26 config: wgpu::SurfaceConfiguration, 26 27 pick: wgpu::Texture, 28 + pick_staging: wgpu::Buffer, 27 29 extent: ViewportExtent, 28 30 reconfigure_pending: bool, 29 31 } ··· 93 95 }; 94 96 surface.configure(&device, &config); 95 97 let pick = create_pick_texture(&device, extent); 98 + let pick_staging = device.create_buffer(&wgpu::BufferDescriptor { 99 + label: Some("bone-render:surface-pick-readback"), 100 + size: u64::from(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT), 101 + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, 102 + mapped_at_creation: false, 103 + }); 96 104 Ok(Self { 97 105 gpu: Gpu::from_parts(device, queue, capabilities), 98 106 surface, 99 107 config, 100 108 pick, 109 + pick_staging, 101 110 extent, 102 111 reconfigure_pending: false, 103 112 }) ··· 116 125 #[must_use] 117 126 pub const fn color_format(&self) -> wgpu::TextureFormat { 118 127 self.config.format 128 + } 129 + 130 + #[must_use] 131 + pub fn picker(&self, index: PickIndex) -> Picker<'_> { 132 + Picker::new( 133 + &self.gpu, 134 + &self.pick, 135 + &self.pick_staging, 136 + self.extent, 137 + index, 138 + ) 119 139 } 120 140 121 141 pub fn resize(&mut self, extent: ViewportExtent) {