forked from 1b-agent/opencadstudio-upstream
Point clouds: measured lighting, highlighted extents box, gizmo shaft grab
- Lighting follows the view: the default source is a key light from the upper left and a fill light from the lower right over an ambient term, the other source one light from above; POINTCLOUDLIGHTING 1 lights the front faces, 2 both. - A selected or hovered point cloud shows its extents box. - A move-gizmo arrow can be grabbed along its shaft; the grab offset is taken on the press, so click-and-place and typed distances start from the insertion point. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4b00504453
commit
15c0da8b79
6 files changed
+87
-39
No files matched your search
@@ -1666,8 +1666,8 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
if grip.gizmo {
|
||||
// The first move measures where the part was grabbed; the
|
||||
// cloud then follows the cursor by that offset.
|
||||
// The cloud follows the cursor by the offset it was grabbed
|
||||
// at (measured on the press; the first move as a fallback).
|
||||
let grab = match grip.grab {
|
||||
Some(grab) => grab,
|
||||
None => {
|
||||
@@ -3639,8 +3639,25 @@ impl OpenCADStudio {
|
||||
self.grip_popup = None;
|
||||
return Task::none();
|
||||
}
|
||||
self.tabs[i].active_grip =
|
||||
Some(self.grip_edit_for_hit(i, handle, grip_id, is_translate, world));
|
||||
let mut edit = self.grip_edit_for_hit(i, handle, grip_id, is_translate, world);
|
||||
if edit.gizmo {
|
||||
// Where the gizmo part was grabbed: the drag keeps
|
||||
// that offset from the cloud's insertion point.
|
||||
let (view_rot, eye) = match &edit_cam {
|
||||
Some(cam) => (cam.view_proj_rte(bounds), cam.eye()),
|
||||
None => {
|
||||
let cam = self.tabs[i].scene.camera.borrow();
|
||||
(cam.view_proj_rte(bounds), cam.eye())
|
||||
}
|
||||
};
|
||||
let grabbed = match (edit.axis, edit.plane) {
|
||||
(Some(axis), _) => cursor_on_projected_axis(p, bounds, view_rot, eye, world, axis),
|
||||
(_, Some(normal)) => cursor_on_plane(p, bounds, view_rot, eye, world, normal),
|
||||
_ => None,
|
||||
};
|
||||
edit.grab = grabbed.map(|point| point - world);
|
||||
}
|
||||
self.tabs[i].active_grip = Some(edit);
|
||||
self.grip_hover = None;
|
||||
self.grip_popup = None;
|
||||
self.tabs[i]
|
||||
|
||||
+19
-8
@@ -79,6 +79,21 @@ fn placed_marker(
|
||||
}
|
||||
}
|
||||
|
||||
/// Pixel distance from `cursor` to a marker; a gizmo arrow counts along its
|
||||
/// whole shaft, not only at its tip.
|
||||
fn marker_distance(shape: GripShape, screen: Vec2, dir: Option<[f32; 2]>, cursor: Point) -> f32 {
|
||||
let cursor = Vec2::new(cursor.x, cursor.y);
|
||||
match (shape, dir) {
|
||||
(GripShape::GizmoAxis(_), Some([dx, dy])) => {
|
||||
let d = Vec2::new(dx, -dy);
|
||||
let start = screen - d * (GIZMO_AXIS_PX - 14.0);
|
||||
let t = (cursor - start).dot(d).clamp(0.0, GIZMO_AXIS_PX - 14.0);
|
||||
cursor.distance(start + d * t)
|
||||
}
|
||||
_ => cursor.distance(screen),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Active drag state ─────────────────────────────────────────────────────
|
||||
|
||||
/// Stored on `OpenCADStudio` while a grip is being dragged.
|
||||
@@ -327,12 +342,10 @@ pub fn find_hit_grip(
|
||||
|
||||
for (index, g) in grips.iter().enumerate() {
|
||||
let project = |world: DVec3| camera.project(world, bounds);
|
||||
let Some((screen, _)) = placed_marker(g, project(g.world), project) else {
|
||||
let Some((screen, dir)) = placed_marker(g, project(g.world), project) else {
|
||||
continue;
|
||||
};
|
||||
let dx = screen.x - cursor.x;
|
||||
let dy = screen.y - cursor.y;
|
||||
let d = (dx * dx + dy * dy).sqrt();
|
||||
let d = marker_distance(g.shape, screen, dir, cursor);
|
||||
if d < best_dist {
|
||||
best_dist = d;
|
||||
best = Some((index, g.id, g.is_midpoint, g.world));
|
||||
@@ -396,12 +409,10 @@ pub fn find_hit_grip_rte(
|
||||
|
||||
for (index, g) in grips.iter().enumerate() {
|
||||
let project = |world: DVec3| project_rte(world, view_rot, eye, bounds);
|
||||
let Some((screen, _)) = placed_marker(g, project(g.world), project) else {
|
||||
let Some((screen, dir)) = placed_marker(g, project(g.world), project) else {
|
||||
continue;
|
||||
};
|
||||
let dx = screen.x - cursor.x;
|
||||
let dy = screen.y - cursor.y;
|
||||
let d = (dx * dx + dy * dy).sqrt();
|
||||
let d = marker_distance(g.shape, screen, dir, cursor);
|
||||
if d < best_dist {
|
||||
best_dist = d;
|
||||
best = Some((index, g.id, g.is_midpoint, g.world));
|
||||
|
||||
@@ -2813,7 +2813,9 @@ impl Pipeline {
|
||||
);
|
||||
for &i in idxs {
|
||||
if let Some(w) = wires.get(i as usize) {
|
||||
if !w.display_visible {
|
||||
// An unseen pick hull (a point cloud's extents box) shows while
|
||||
// highlighted, as the reference does.
|
||||
if !w.display_visible && (w.pick_tris.is_empty() || w.snap_only) {
|
||||
continue;
|
||||
}
|
||||
selected_wires.push(w);
|
||||
@@ -2825,7 +2827,9 @@ impl Pipeline {
|
||||
if let Some(idxs) = self.wire_handle_index.get(&h.value()) {
|
||||
for &i in idxs {
|
||||
if let Some(w) = wires.get(i as usize) {
|
||||
if !w.display_visible {
|
||||
// An unseen pick hull (a point cloud's extents box) shows while
|
||||
// highlighted, as the reference does.
|
||||
if !w.display_visible && (w.pick_tris.is_empty() || w.snap_only) {
|
||||
continue;
|
||||
}
|
||||
hover_wires.push(w);
|
||||
@@ -3848,7 +3852,7 @@ analytic={:.1} regular={:.1} blocks={:.1}",
|
||||
self.gpu_point_clouds.upload(device, queue, set);
|
||||
}
|
||||
|
||||
pub fn set_point_cloud_view(&mut self, queue: &wgpu::Queue, light: [f32; 8]) {
|
||||
pub fn set_point_cloud_view(&mut self, queue: &wgpu::Queue, light: [f32; 12]) {
|
||||
self.gpu_point_clouds.set_view(queue, light);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// screen-space square sprites (6 vertices per instance, no vertex buffer).
|
||||
//
|
||||
// Group 1 binding 0 — PointParams (sprite size in pixels, lighting mode and
|
||||
// light source, the light direction; 32 bytes).
|
||||
// light source, the two light directions; 48 bytes).
|
||||
//
|
||||
// Buffers are keyed by `PlacedCloud::key`: a new render set re-uploads only
|
||||
// the clouds whose points changed; the rest keep their buffers.
|
||||
@@ -68,7 +68,7 @@ pub struct PointCloudGpu {
|
||||
params: wgpu::Buffer,
|
||||
bind_group: wgpu::BindGroup,
|
||||
point_size: f32,
|
||||
params_written: [f32; 8],
|
||||
params_written: [f32; 12],
|
||||
/// The share of each cloud's points drawn (real-time density).
|
||||
fraction: f32,
|
||||
}
|
||||
@@ -77,7 +77,7 @@ impl PointCloudGpu {
|
||||
pub fn new(device: &wgpu::Device, layout: &wgpu::BindGroupLayout) -> Self {
|
||||
let params = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("point_cloud.params"),
|
||||
size: 32,
|
||||
size: 48,
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
@@ -91,7 +91,7 @@ impl PointCloudGpu {
|
||||
params,
|
||||
bind_group,
|
||||
point_size: f32::NAN,
|
||||
params_written: [f32::NAN; 8],
|
||||
params_written: [f32::NAN; 12],
|
||||
fraction: 1.0,
|
||||
}
|
||||
}
|
||||
@@ -125,9 +125,13 @@ impl PointCloudGpu {
|
||||
|
||||
/// The view's lighting (`[mode, source, fraction, _, light x, y, z, _]`)
|
||||
/// with the sprite size, written when they change.
|
||||
pub fn set_view(&mut self, queue: &wgpu::Queue, light: [f32; 8]) {
|
||||
pub fn set_view(&mut self, queue: &wgpu::Queue, light: [f32; 12]) {
|
||||
self.fraction = light[2].clamp(0.0, 1.0);
|
||||
let params = [self.point_size, light[0], light[1], 0.0, light[4], light[5], light[6], 0.0];
|
||||
let mut params = light;
|
||||
params[0] = self.point_size;
|
||||
params[1] = light[0];
|
||||
params[2] = light[1];
|
||||
params[3] = 0.0;
|
||||
if params != self.params_written {
|
||||
self.params_written = params;
|
||||
queue.write_buffer(&self.params, 0, bytemuck::cast_slice(¶ms));
|
||||
|
||||
@@ -169,7 +169,7 @@ pub struct ViewportData {
|
||||
pub(in crate::scene) point_clouds: Arc<crate::scene::model::point_cloud::PointCloudSet>,
|
||||
/// Point cloud lighting and density for this view: `[mode, source,
|
||||
/// drawn fraction, _, light direction x, y, z, _]`.
|
||||
pub(in crate::scene) point_cloud_light: [f32; 8],
|
||||
pub(in crate::scene) point_cloud_light: [f32; 12],
|
||||
pub(in crate::scene) meshes: Arc<Vec<MeshLodSet>>,
|
||||
pub(in crate::scene) background_image:
|
||||
Option<crate::scene::model::image_model::DecodedImage>,
|
||||
@@ -4841,18 +4841,21 @@ impl Scene {
|
||||
// Lit only in the shaded styles, as the reference.
|
||||
let lighting = if flags.mesh_fill && !flags.hidden_line { value("POINTCLOUDLIGHTING") } else { 0.0 };
|
||||
let source = value("POINTCLOUDLIGHTSOURCE");
|
||||
let forward = (inst.camera.rotation * glam::Vec3::NEG_Z).normalize_or(glam::Vec3::NEG_Z);
|
||||
// ponytail: the default lights as one light from the upper left
|
||||
// of the view; the reference's exact light set is not known.
|
||||
let light = if source > 0.5 {
|
||||
let (up, right) = (inst.camera.rotation * glam::Vec3::Y, inst.camera.rotation * glam::Vec3::X);
|
||||
(-forward * 0.6 + up * 0.6 - right * 0.5).normalize_or(-forward)
|
||||
// The lights follow the view (measured from the reference in
|
||||
// view coordinates: right, up, toward the viewer): the default
|
||||
// lighting is a key light from the upper left and a fill light
|
||||
// from the lower right, the other source one light from above.
|
||||
let view = |x: f32, y: f32, z: f32| {
|
||||
(inst.camera.rotation * glam::Vec3::new(x, y, z)).normalize_or(glam::Vec3::Z)
|
||||
};
|
||||
let (first, second) = if source > 0.5 {
|
||||
(view(-0.034, 0.987, 0.158), glam::Vec3::ZERO)
|
||||
} else {
|
||||
-forward
|
||||
(view(-0.512, 0.500, 0.698), view(0.834, -0.451, 0.318))
|
||||
};
|
||||
// While the view moves, POINTCLOUDRTDENSITY per cent of the points.
|
||||
let fraction = if navigating { value("POINTCLOUDRTDENSITY") / 100.0 } else { 1.0 };
|
||||
[lighting, source, fraction, 0.0, light.x, light.y, light.z, 0.0]
|
||||
[lighting, source, fraction, 0.0, first.x, first.y, first.z, 0.0, second.x, second.y, second.z, 0.0]
|
||||
};
|
||||
Some(ViewportData {
|
||||
instance_id,
|
||||
|
||||
@@ -22,13 +22,15 @@ struct Uniforms {
|
||||
|
||||
struct PointParams {
|
||||
size_px: f32,
|
||||
// POINTCLOUDLIGHTING (0 off; 1 lit from both sides; 2 back faces dark)
|
||||
// and POINTCLOUDLIGHTSOURCE (0 headlight; 1 the default lights).
|
||||
// POINTCLOUDLIGHTING (0 off; 1 front faces lit; 2 both faces) and
|
||||
// POINTCLOUDLIGHTSOURCE (0 key + fill light; 1 one light from above).
|
||||
lighting: f32,
|
||||
source: f32,
|
||||
_pad0: f32,
|
||||
light_dir: vec3<f32>,
|
||||
_pad1: f32,
|
||||
fill_dir: vec3<f32>,
|
||||
_pad2: f32,
|
||||
};
|
||||
@group(1) @binding(0) var<uniform> params: PointParams;
|
||||
|
||||
@@ -61,18 +63,25 @@ fn vs_main(@builtin(vertex_index) corner: u32, in: PointIn) -> VertOut {
|
||||
out.color = in.color;
|
||||
let n = in.normal.xyz;
|
||||
if params.lighting > 0.5 && dot(n, n) > 0.01 {
|
||||
// Measured from the reference: headlight ambient 0.2 + 0.576 diffuse,
|
||||
// the default lights 0.3 + 0.9 (clamped).
|
||||
let d = dot(normalize(n), params.light_dir);
|
||||
// Fitted to the reference's shading of the normals.
|
||||
let nn = normalize(n);
|
||||
var d = dot(nn, params.light_dir);
|
||||
var f = dot(nn, params.fill_dir);
|
||||
if params.lighting > 1.5 {
|
||||
d = abs(d);
|
||||
f = abs(f);
|
||||
}
|
||||
var shade: f32;
|
||||
if params.source > 0.5 {
|
||||
shade = min(0.3 + 0.9 * max(d, 0.0), 1.0);
|
||||
shade = 0.3 + 0.7 * max(d, 0.0);
|
||||
} else if params.lighting > 1.5 {
|
||||
shade = 0.2 + 0.576 * max(d, 0.0);
|
||||
// ponytail: best fit only; the reference also darkens some
|
||||
// regions that no normal-based term explains.
|
||||
shade = 0.133 + 0.61 * d + 0.086 * f;
|
||||
} else {
|
||||
shade = 0.2 + 0.576 * abs(d);
|
||||
shade = 0.204 + 0.569 * max(d, 0.0) + 0.329 * max(f, 0.0);
|
||||
}
|
||||
out.color = vec4<f32>(in.color.rgb * shade, in.color.a);
|
||||
out.color = vec4<f32>(in.color.rgb * min(shade, 1.0), in.color.a);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user