feat(paper): render the sheet as a top-locked GPU viewport
Paper space now renders its sheet through the unified shader, the same path as model space: a full-canvas "sheet" ViewportInstance draws the layout's own entities + viewport borders + interim/preview wires, with a camera locked to the top/plan orientation (paper is 2-D and never orbits). Floating content viewports overlay it. - PaperCanvas no longer draws entity wires (still paints the desk, sheet and hatch/wipeout/image fills; full removal is the next step). - Right-drag on the paper sheet no longer orbits the camera (it would corrupt the frame and skew subsequent pans). - Fix floating-viewport orbit oscillation: orbit_active_viewport wrote view_direction.y negated while camera_for_viewport reads it un-negated, so each drag step read back a Y-mirrored camera and flipped the model between a rotation and its opposite every frame. Store eye directly, matching snap_active_viewport_to_direction and the reader. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
568422679d
commit
0ef99ff2fc
4 changed files with 65 additions and 64 deletions
|
|
@ -1951,12 +1951,18 @@ impl OpenCADStudio {
|
||||||
drop(sel);
|
drop(sel);
|
||||||
self.tabs[i].scene.orbit_active_viewport(dx, dy);
|
self.tabs[i].scene.orbit_active_viewport(dx, dy);
|
||||||
return Task::none();
|
return Task::none();
|
||||||
} else {
|
} else if self.tabs[i].scene.current_layout == "Model" {
|
||||||
sel.right_last_pos = Some(p);
|
sel.right_last_pos = Some(p);
|
||||||
drop(sel);
|
drop(sel);
|
||||||
self.tabs[i].scene.camera.borrow_mut().orbit(dx, dy);
|
self.tabs[i].scene.camera.borrow_mut().orbit(dx, dy);
|
||||||
self.tabs[i].scene.camera_generation += 1;
|
self.tabs[i].scene.camera_generation += 1;
|
||||||
return Task::none();
|
return Task::none();
|
||||||
|
} else {
|
||||||
|
// Paper sheet is top-locked: right-drag never
|
||||||
|
// orbits it (orbiting would corrupt the camera
|
||||||
|
// frame and skew subsequent pans).
|
||||||
|
sel.right_last_pos = Some(p);
|
||||||
|
return Task::none();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sel.right_last_pos = Some(p);
|
sel.right_last_pos = Some(p);
|
||||||
|
|
|
||||||
|
|
@ -436,6 +436,10 @@ pub struct ViewportInstance {
|
||||||
pub render_mode: acadrust::entities::ViewportRenderMode,
|
pub render_mode: acadrust::entities::ViewportRenderMode,
|
||||||
/// `true` when this is the viewport receiving cursor input.
|
/// `true` when this is the viewport receiving cursor input.
|
||||||
pub active: bool,
|
pub active: bool,
|
||||||
|
/// `true` for the full-canvas paper "sheet" viewport — the layout's own
|
||||||
|
/// view (paper-space entities, top-locked), the paper equivalent of the
|
||||||
|
/// Model view. Floating content viewports overlay it.
|
||||||
|
pub paper_sheet: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One pane of the Model-space tiled viewport layout: the normalized screen
|
/// One pane of the Model-space tiled viewport layout: the normalized screen
|
||||||
|
|
@ -2691,16 +2695,18 @@ impl Scene {
|
||||||
};
|
};
|
||||||
cam.orbit(delta_x, delta_y);
|
cam.orbit(delta_x, delta_y);
|
||||||
// yaw_pitch_to_quat(y,p)*Z = (cos(p)*sin(y), -cos(p)*cos(y), sin(p))
|
// yaw_pitch_to_quat(y,p)*Z = (cos(p)*sin(y), -cos(p)*cos(y), sin(p))
|
||||||
// but view_direction convention (matching snap_active_viewport_to_angles) is
|
// `camera_for_viewport` reconstructs the rotation so that
|
||||||
// (cos(p)*sin(y), +cos(p)*cos(y), sin(p)) ← Y has opposite sign.
|
// `rotation * Z == view_direction` exactly (its `yaw = atan2(x, -y)`
|
||||||
// Negate Y when writing back so camera_for_viewport round-trips correctly.
|
// cancels the sign). Store `eye` directly so the orbit round-trips —
|
||||||
|
// negating Y here made each drag step read back a Y-mirrored camera,
|
||||||
|
// flipping the model between a rotation and its opposite every frame.
|
||||||
let eye = cam.rotation * glam::Vec3::Z;
|
let eye = cam.rotation * glam::Vec3::Z;
|
||||||
if let Some(acadrust::EntityType::Viewport(vp)) = self.document.get_entity_mut(vp_handle) {
|
if let Some(acadrust::EntityType::Viewport(vp)) = self.document.get_entity_mut(vp_handle) {
|
||||||
if vp.status.locked {
|
if vp.status.locked {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
vp.view_direction.x = eye.x as f64;
|
vp.view_direction.x = eye.x as f64;
|
||||||
vp.view_direction.y = -eye.y as f64;
|
vp.view_direction.y = eye.y as f64;
|
||||||
vp.view_direction.z = eye.z as f64;
|
vp.view_direction.z = eye.z as f64;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5699,12 +5705,37 @@ impl Scene {
|
||||||
camera,
|
camera,
|
||||||
render_mode: model_mode,
|
render_mode: model_mode,
|
||||||
active: i == active,
|
active: i == active,
|
||||||
|
paper_sheet: false,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
let layout_block = self.current_layout_block_handle();
|
let layout_block = self.current_layout_block_handle();
|
||||||
let mut out: Vec<ViewportInstance> = Vec::new();
|
let mut out: Vec<ViewportInstance> = Vec::new();
|
||||||
|
// The full-canvas sheet viewport renders the paper-space entities
|
||||||
|
// themselves — the layout's own view, drawn first so the floating
|
||||||
|
// content viewports overlay it. Its camera keeps the paper pan/zoom
|
||||||
|
// (target + ortho size) but is LOCKED to the top/plan orientation:
|
||||||
|
// paper is 2-D, so the sheet never orbits.
|
||||||
|
let mut sheet_cam = self.camera.borrow().clone();
|
||||||
|
sheet_cam.yaw = 0.0;
|
||||||
|
sheet_cam.pitch = std::f32::consts::FRAC_PI_2;
|
||||||
|
sheet_cam.rotation = camera::yaw_pitch_to_quat(0.0, std::f32::consts::FRAC_PI_2, 0.0);
|
||||||
|
sheet_cam.projection = camera::Projection::Orthographic;
|
||||||
|
out.push(ViewportInstance {
|
||||||
|
handle: Handle::NULL,
|
||||||
|
tile_idx: None,
|
||||||
|
screen_rect: iced::Rectangle {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
width: canvas_w,
|
||||||
|
height: canvas_h,
|
||||||
|
},
|
||||||
|
camera: sheet_cam,
|
||||||
|
render_mode: acadrust::entities::ViewportRenderMode::Wireframe2D,
|
||||||
|
active: false,
|
||||||
|
paper_sheet: true,
|
||||||
|
});
|
||||||
for e in self.document.entities() {
|
for e in self.document.entities() {
|
||||||
let EntityType::Viewport(vp) = e else {
|
let EntityType::Viewport(vp) = e else {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -5728,6 +5759,7 @@ impl Scene {
|
||||||
camera,
|
camera,
|
||||||
render_mode: vp.render_mode,
|
render_mode: vp.render_mode,
|
||||||
active: self.active_viewport == Some(h),
|
active: self.active_viewport == Some(h),
|
||||||
|
paper_sheet: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
|
|
@ -5796,6 +5828,7 @@ impl Scene {
|
||||||
/// All wires needed to render the paper-space canvas (2D widget path).
|
/// All wires needed to render the paper-space canvas (2D widget path).
|
||||||
/// Includes paper entities, paper boundary, inactive viewport projections
|
/// Includes paper entities, paper boundary, inactive viewport projections
|
||||||
/// (excluding the active MSPACE viewport), plus interim/preview wires.
|
/// (excluding the active MSPACE viewport), plus interim/preview wires.
|
||||||
|
#[allow(dead_code)] // superseded by the GPU sheet viewport; removed in Stage 2.
|
||||||
pub fn paper_canvas_wires(&self) -> Arc<Vec<WireModel>> {
|
pub fn paper_canvas_wires(&self) -> Arc<Vec<WireModel>> {
|
||||||
{
|
{
|
||||||
let cache = self.paper_canvas_cache.borrow();
|
let cache = self.paper_canvas_cache.borrow();
|
||||||
|
|
@ -5919,6 +5952,7 @@ impl Scene {
|
||||||
Arc::new(models)
|
Arc::new(models)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)] // superseded by the GPU sheet viewport; removed in Stage 2.
|
||||||
pub(super) fn paper_sheet_wires(&self) -> Vec<WireModel> {
|
pub(super) fn paper_sheet_wires(&self) -> Vec<WireModel> {
|
||||||
(*self.paper_sheet_wires_arc()).clone()
|
(*self.paper_sheet_wires_arc()).clone()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,62 +100,9 @@ impl<'a> canvas::Program<Message> for PaperCanvas<'a> {
|
||||||
draw_hatch(&mut frame, hatch, &to_px);
|
draw_hatch(&mut frame, hatch, &to_px);
|
||||||
}
|
}
|
||||||
|
|
||||||
// px-per-world-unit scale for linetype dash lengths.
|
// Entity lines, viewport borders and interim/preview wires are now
|
||||||
let world_to_px_scale = if half_w > 0.0 {
|
// rendered by the GPU shader's full-canvas paper "sheet" viewport, so
|
||||||
bounds.width / (2.0 * half_w)
|
// the 2-D canvas only paints the desk + sheet + fills beneath it.
|
||||||
} else {
|
|
||||||
1.0
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Wires (entity lines + inactive viewport projections) ──────────────
|
|
||||||
for wire in self.scene.paper_canvas_wires().iter() {
|
|
||||||
let [r, g, b, a] = wire.color;
|
|
||||||
let color = Color { r, g, b, a };
|
|
||||||
|
|
||||||
let path = canvas::Path::new(|b| {
|
|
||||||
let mut started = false;
|
|
||||||
for &[wx, wy, _] in &wire.points {
|
|
||||||
if wx.is_nan() || wy.is_nan() {
|
|
||||||
started = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let p = to_px(wx, wy);
|
|
||||||
if started {
|
|
||||||
b.line_to(p);
|
|
||||||
} else {
|
|
||||||
b.move_to(p);
|
|
||||||
started = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Convert WireModel linetype pattern (world units) to pixel lengths.
|
|
||||||
// Keep the Vec alive for the duration of frame.stroke().
|
|
||||||
let dash_segments: Vec<f32> = if wire.pattern_length > 0.0 {
|
|
||||||
wire.pattern
|
|
||||||
.iter()
|
|
||||||
.take_while(|&&v| v != 0.0)
|
|
||||||
.map(|&v| v.abs() * world_to_px_scale)
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
};
|
|
||||||
|
|
||||||
frame.stroke(
|
|
||||||
&path,
|
|
||||||
canvas::Stroke {
|
|
||||||
style: canvas::Style::Solid(color),
|
|
||||||
width: wire.line_weight_px.max(1.0),
|
|
||||||
line_cap: canvas::LineCap::Square,
|
|
||||||
line_join: canvas::LineJoin::Miter,
|
|
||||||
line_dash: canvas::LineDash {
|
|
||||||
segments: &dash_segments,
|
|
||||||
offset: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
vec![frame.into_geometry()]
|
vec![frame.into_geometry()]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -597,6 +597,11 @@ impl Scene {
|
||||||
1.0
|
1.0
|
||||||
};
|
};
|
||||||
self.model_tile_wires_arc(tile_idx, &inst.camera, aspect, full.height)
|
self.model_tile_wires_arc(tile_idx, &inst.camera, aspect, full.height)
|
||||||
|
} else if inst.paper_sheet {
|
||||||
|
// The sheet renders the paper block's own entities + viewport
|
||||||
|
// borders — NOT the projected viewport content (the GPU content
|
||||||
|
// viewports draw that themselves).
|
||||||
|
self.paper_sheet_wires_arc()
|
||||||
} else if inst.handle == acadrust::Handle::NULL {
|
} else if inst.handle == acadrust::Handle::NULL {
|
||||||
self.entity_wires_arc()
|
self.entity_wires_arc()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -648,13 +653,22 @@ impl Scene {
|
||||||
height: visible_h / canvas.1,
|
height: visible_h / canvas.1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Stage 1: the paper sheet instance contributes only entity wires; its
|
||||||
|
// hatch / wipeout / image fills are still drawn by PaperCanvas beneath
|
||||||
|
// the shader. (Stage 2 moves those into the shader and drops PaperCanvas.)
|
||||||
|
let (hatches, wipeout_hatches, images) = if inst.paper_sheet {
|
||||||
|
(Arc::new(Vec::new()), Arc::new(Vec::new()), Arc::new(Vec::new()))
|
||||||
|
} else {
|
||||||
|
(self.hatch_models_arc(), self.wipeout_models_arc(), self.images_arc())
|
||||||
|
};
|
||||||
|
|
||||||
Some(ViewportData {
|
Some(ViewportData {
|
||||||
wires: all_wires,
|
wires: all_wires,
|
||||||
face3d_wires: Arc::new(face3d_wires),
|
face3d_wires: Arc::new(face3d_wires),
|
||||||
draw_depths: self.draw_depth_map(),
|
draw_depths: self.draw_depth_map(),
|
||||||
hatches: self.hatch_models_arc(),
|
hatches,
|
||||||
wipeout_hatches: self.wipeout_models_arc(),
|
wipeout_hatches,
|
||||||
images: self.images_arc(),
|
images,
|
||||||
meshes: self.meshes_arc(),
|
meshes: self.meshes_arc(),
|
||||||
uniforms,
|
uniforms,
|
||||||
cam_rotation: inst.camera.view_rotation_mat(),
|
cam_rotation: inst.camera.view_rotation_mat(),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue