forked from 1b-agent/opencadstudio-upstream
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:
1 parent
568422679d
commit
0ef99ff2fc
4 files changed
+65
-64
No files matched your search
+7
-1
@@ -1951,12 +1951,18 @@ impl OpenCADStudio {
|
||||
drop(sel);
|
||||
self.tabs[i].scene.orbit_active_viewport(dx, dy);
|
||||
return Task::none();
|
||||
} else {
|
||||
} else if self.tabs[i].scene.current_layout == "Model" {
|
||||
sel.right_last_pos = Some(p);
|
||||
drop(sel);
|
||||
self.tabs[i].scene.camera.borrow_mut().orbit(dx, dy);
|
||||
self.tabs[i].scene.camera_generation += 1;
|
||||
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 {
|
||||
sel.right_last_pos = Some(p);
|
||||
|
||||
+38
-4
@@ -436,6 +436,10 @@ pub struct ViewportInstance {
|
||||
pub render_mode: acadrust::entities::ViewportRenderMode,
|
||||
/// `true` when this is the viewport receiving cursor input.
|
||||
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
|
||||
@@ -2691,16 +2695,18 @@ impl Scene {
|
||||
};
|
||||
cam.orbit(delta_x, delta_y);
|
||||
// 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
|
||||
// (cos(p)*sin(y), +cos(p)*cos(y), sin(p)) ← Y has opposite sign.
|
||||
// Negate Y when writing back so camera_for_viewport round-trips correctly.
|
||||
// `camera_for_viewport` reconstructs the rotation so that
|
||||
// `rotation * Z == view_direction` exactly (its `yaw = atan2(x, -y)`
|
||||
// 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;
|
||||
if let Some(acadrust::EntityType::Viewport(vp)) = self.document.get_entity_mut(vp_handle) {
|
||||
if vp.status.locked {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -5699,12 +5705,37 @@ impl Scene {
|
||||
camera,
|
||||
render_mode: model_mode,
|
||||
active: i == active,
|
||||
paper_sheet: false,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
let layout_block = self.current_layout_block_handle();
|
||||
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() {
|
||||
let EntityType::Viewport(vp) = e else {
|
||||
continue;
|
||||
@@ -5728,6 +5759,7 @@ impl Scene {
|
||||
camera,
|
||||
render_mode: vp.render_mode,
|
||||
active: self.active_viewport == Some(h),
|
||||
paper_sheet: false,
|
||||
});
|
||||
}
|
||||
out
|
||||
@@ -5796,6 +5828,7 @@ impl Scene {
|
||||
/// All wires needed to render the paper-space canvas (2D widget path).
|
||||
/// Includes paper entities, paper boundary, inactive viewport projections
|
||||
/// (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>> {
|
||||
{
|
||||
let cache = self.paper_canvas_cache.borrow();
|
||||
@@ -5919,6 +5952,7 @@ impl Scene {
|
||||
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> {
|
||||
(*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);
|
||||
}
|
||||
|
||||
// px-per-world-unit scale for linetype dash lengths.
|
||||
let world_to_px_scale = if half_w > 0.0 {
|
||||
bounds.width / (2.0 * half_w)
|
||||
} 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,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Entity lines, viewport borders and interim/preview wires are now
|
||||
// rendered by the GPU shader's full-canvas paper "sheet" viewport, so
|
||||
// the 2-D canvas only paints the desk + sheet + fills beneath it.
|
||||
vec![frame.into_geometry()]
|
||||
}
|
||||
}
|
||||
|
||||
+17
-3
@@ -597,6 +597,11 @@ impl Scene {
|
||||
1.0
|
||||
};
|
||||
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 {
|
||||
self.entity_wires_arc()
|
||||
} else {
|
||||
@@ -648,13 +653,22 @@ impl Scene {
|
||||
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 {
|
||||
wires: all_wires,
|
||||
face3d_wires: Arc::new(face3d_wires),
|
||||
draw_depths: self.draw_depth_map(),
|
||||
hatches: self.hatch_models_arc(),
|
||||
wipeout_hatches: self.wipeout_models_arc(),
|
||||
images: self.images_arc(),
|
||||
hatches,
|
||||
wipeout_hatches,
|
||||
images,
|
||||
meshes: self.meshes_arc(),
|
||||
uniforms,
|
||||
cam_rotation: inst.camera.view_rotation_mat(),
|
||||
|
||||
Reference in new issue
Block a user