git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

commit0a3a1378bb3180a71edffe2656faf52f26f6ed33
parentc2be76533d
authorLucas Galante <[email protected]>
date2026-07-12 08:27
refactor: own Canvas + Viewport3D (verbatim from deleted cce-ui types, Phase 6ay part 2)

The designer was the only consumer of both roster widgets. Viewport3D
verbatim in src/viewport_3d.rs; the 25-line Canvas inlined into app.rs
beside the other app-local widgets. A/B: AE=0.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018u7qTwzX95dd5ysAkaSCLk

 src/app.rs         |  31 +++++-
 src/main.rs        |   1 +
 src/viewport_3d.rs | 313 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 344 insertions(+), 1 deletion(-)

diff --git a/src/app.rs b/src/app.rs
index 6ee3b64..038a5d0 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -35,7 +35,9 @@ use wayland_client::{
 };
 
 use wgpu::util::DeviceExt;
-use cce_ui::widget::{Breadcrumb, Canvas, MenuBar, MenuController, ParametersBg, Splitter, Spreadsheet, StatusBar, TextLabel, Viewport3D, Element, GraphNode, Graph, Button, Checkbox, Label, Dropdown};
+use cce_ui::widget::{Breadcrumb, MenuBar, MenuController, ParametersBg, Splitter, Spreadsheet, StatusBar, TextLabel, Element, GraphNode, Graph, Button, Checkbox, Label, Dropdown};
+use cce_ui::widget::UiContext;
+use crate::viewport_3d::Viewport3D;
 use cce_ui::colors;
 use glyphon::{Attrs, Buffer, Cache, FontSystem, Metrics, Resolution, TextAtlas, TextRenderer, Viewport};
 use glam::{Mat4, Vec3};
@@ -610,6 +612,33 @@ impl cce_ui::widget::Element for PassivePlate {
     }
 }
 
+
+/// App-owned copy of the dissolved cce-ui `Canvas` (Phase 6ay part 2): the transparent
+/// hit-through pane behind the network area. Verbatim; dies with the machinery retype.
+pub struct Canvas {
+    x: f32, y: f32, w: f32, h: f32,
+    hovered: bool,
+}
+
+impl Canvas {
+    pub fn new() -> Self { Self { x: 0.0, y: 0.0, w: 0.0, h: 0.0, hovered: false } }
+}
+
+impl Element for Canvas {
+    fn rect(&self) -> (f32, f32, f32, f32) { (self.x, self.y, self.w, self.h) }
+    fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) { self.x = x; self.y = y; self.w = w; self.h = h; }
+    fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
+    fn as_ptr(&self) -> *mut (dyn Element + 'static) {
+        self as *const Self as *mut Self as *mut (dyn Element + 'static)
+    }
+    fn as_ptr_mut(&mut self) -> *mut (dyn Element + 'static) {
+        self as *mut Self as *mut (dyn Element + 'static)
+    }
+    fn set_hovered(&mut self, v: bool) { self.hovered = v; }
+    fn hovered(&self) -> bool { self.hovered }
+    fn hit_test(&self, _px: f32, _py: f32, _ctx: &UiContext) -> bool { false }
+}
+
 impl Element for NodePalette {
     // Leaf legacy widget: own labels via paint_self (cce-ui's default no longer drains
     // the text getters; the render loop's walk text reads it).
diff --git a/src/main.rs b/src/main.rs
index 37819b2..afc17b5 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -41,6 +41,7 @@ use calloop_wayland_source::WaylandSource;
 use glam::{Mat4, Vec3};
 
 pub mod app;
+pub mod viewport_3d;
 pub mod graphics;
 pub mod api;
 pub mod window;
diff --git a/src/viewport_3d.rs b/src/viewport_3d.rs
new file mode 100644
index 0000000..657a8ce
--- /dev/null
+++ b/src/viewport_3d.rs
@@ -0,0 +1,313 @@
+//! App-owned copy of the dissolved cce-ui `Viewport3D` (Phase 6ay part 2): the designer
+//! is the only consumer — the 3D preview pane of the roster. Verbatim from cce-ui; the
+//! `Element` impl dies with the machinery retype.
+
+use cce_ui::colors;
+use cce_ui::widget::*;
+use glam::{Mat4, Vec3};
+
+#[derive(Debug, Clone)]
+pub struct Viewport3D {
+    base: Widget,
+    pub rotation_x: f32,
+    pub rotation_y: f32,
+    pub zoom: f32,
+    pub active_camera: String,
+    pub bg_color: [f32; 3],
+    pub grid_color: [f32; 3],
+    pub show_grid: bool,
+    pub show_cube: bool,
+    pub show_origin: bool,
+    pub show_camera_pivot: bool,
+
+    // Pending rotation to be consumed by the application when not using the default camera
+    pub pending_yaw: f32,
+    pub pending_pitch: f32,
+
+    // Modifiers state
+    ctrl_pressed: bool,
+    shift_pressed: bool,
+    alt_pressed: bool,
+
+    // Drag & scroll state tracking
+    is_rotating: bool,
+    is_zooming: bool,
+    scroll_lock: u8,
+    rotate_accum_yaw: f32,
+    rotate_accum_pitch: f32,
+    zoom_accum: f32,
+    rotate_velocity_yaw: f32,
+    rotate_velocity_pitch: f32,
+    zoom_velocity: f32,
+    last_rotate_time: std::time::Instant,
+    last_zoom_time: std::time::Instant,
+    pub scroll_speed: f32,
+    pub inertial_scroll: bool,
+    pub scroll_friction: f32,
+}
+
+impl Viewport3D {
+    pub fn new() -> Self {
+        let base = Widget::new();
+        Self {
+            base,
+            rotation_x: 0.0,
+            rotation_y: 0.0,
+            zoom: 1.0,
+            active_camera: "Default Camera".to_string(),
+            bg_color: [0.10, 0.10, 0.13],
+            grid_color: [0.18, 0.18, 0.22],
+            show_grid: true,
+            show_cube: true,
+            show_origin: true,
+            show_camera_pivot: true,
+            pending_yaw: 0.0,
+            pending_pitch: 0.0,
+            ctrl_pressed: false,
+            shift_pressed: false,
+            alt_pressed: false,
+            is_rotating: false,
+            is_zooming: false,
+            scroll_lock: 0,
+            rotate_accum_yaw: 0.0,
+            rotate_accum_pitch: 0.0,
+            zoom_accum: 0.0,
+            rotate_velocity_yaw: 0.0,
+            rotate_velocity_pitch: 0.0,
+            zoom_velocity: 0.0,
+            last_rotate_time: std::time::Instant::now(),
+            last_zoom_time: std::time::Instant::now(),
+            scroll_speed: 1.0,
+            inertial_scroll: true,
+            scroll_friction: 0.90,
+        }
+    }
+
+    pub fn with_scroll_speed(mut self, speed: f32) -> Self {
+        self.scroll_speed = speed;
+        self
+    }
+
+    pub fn with_inertial_scroll(mut self, enabled: bool) -> Self {
+        self.inertial_scroll = enabled;
+        self
+    }
+
+    pub fn with_scroll_friction(mut self, friction: f32) -> Self {
+        self.scroll_friction = friction;
+        self
+    }
+
+    pub fn reset_velocity(&mut self) {
+        self.rotate_velocity_yaw = 0.0;
+        self.rotate_velocity_pitch = 0.0;
+        self.zoom_velocity = 0.0;
+        self.is_rotating = false;
+        self.is_zooming = false;
+        self.scroll_lock = 0;
+    }
+
+    pub fn get_matrices(&self, aspect: f32, custom_camera_pos: Option<Vec3>, custom_camera_rot: Option<Vec3>, custom_pivot: Option<Vec3>) -> (Mat4, Mat4, Mat4) {
+        let camera_pos = custom_camera_pos.unwrap_or(Vec3::new(2.5, 1.8, 2.5));
+        let pivot = custom_pivot.unwrap_or(Vec3::ZERO);
+        let rot = custom_camera_rot.unwrap_or(Vec3::ZERO);
+        let rx = rot.x;
+        let ry = rot.y;
+        let rz = rot.z;
+
+        let base_offset = camera_pos - pivot;
+        let distance = base_offset.length();
+        let yaw0 = base_offset.x.atan2(base_offset.z);
+        let pitch0 = (base_offset.y / distance.max(1e-5)).asin();
+
+        let total_ry = ry.to_radians() + yaw0;
+        let total_rx = rx.to_radians() + pitch0;
+
+        let view_rot_pos = Mat4::from_rotation_y(total_ry) * Mat4::from_rotation_x(-total_rx);
+        let camera_up = view_rot_pos.transform_vector3(Vec3::Y);
+        let camera_world_pos = pivot + view_rot_pos.transform_vector3(Vec3::new(0.0, 0.0, distance) * self.zoom);
+        let view_mat = Mat4::from_rotation_z(rz.to_radians()) * Mat4::look_at_rh(camera_world_pos, pivot, camera_up);
+
+        let model = Mat4::from_rotation_y(self.rotation_y) * Mat4::from_rotation_x(self.rotation_x);
+        let proj = Mat4::perspective_rh(0.9, aspect, 0.1, 100.0);
+
+        (proj, view_mat, model)
+    }
+}
+
+impl Element for Viewport3D {
+    cce_ui::impl_widget_base!(Viewport3D);
+
+    fn color(&self) -> [f32; 4] {
+        colors::VIEWPORT_BG
+    }
+
+    fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
+        self.ctrl_pressed = ctrl;
+        self.shift_pressed = shift;
+        self.alt_pressed = alt;
+    }
+
+    fn hit_test(&self, px: f32, py: f32, _ctx: &UiContext) -> bool {
+        let (x, y, w, h) = self.rect();
+        px >= x && px <= x + w && py >= y && py <= y + h
+    }
+
+    fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+        if !self.hit_test(px, py, ctx) {
+            return false;
+        }
+        let scale = cce_ui::scale::scale_factor();
+        if self.ctrl_pressed {
+            match delta {
+                MouseScrollDelta::LineDelta(_x, y) => {
+                    let dy = *y * 0.15 * self.scroll_speed;
+                    self.zoom *= (-dy).exp();
+                    self.zoom = self.zoom.clamp(0.05, 20.0);
+                    self.is_zooming = false;
+                    
+                    let dt = 0.016;
+                    self.zoom_velocity = self.zoom_velocity * 0.4 + (dy / dt) * 0.6;
+                    true
+                }
+                MouseScrollDelta::PixelDelta(pos) => {
+                    let dy = (pos.y as f32 / scale) * 0.005 * self.scroll_speed;
+                    self.zoom *= (-dy).exp();
+                    self.zoom = self.zoom.clamp(0.05, 20.0);
+                    self.is_zooming = true;
+                    self.last_zoom_time = std::time::Instant::now();
+                    self.zoom_accum += dy;
+                    true
+                }
+            }
+        } else {
+            match delta {
+                MouseScrollDelta::LineDelta(x, y) => {
+                    self.scroll_lock = 0;
+                    let dx = *x * 0.05 * self.scroll_speed;
+                    let dy = *y * 0.05 * self.scroll_speed;
+                    
+                    if self.active_camera != "Default Camera" {
+                        self.pending_yaw += dx;
+                        self.pending_pitch += -dy;
+                    } else {
+                        self.rotation_y += dx;
+                        self.rotation_x -= dy;
+                    }
+                    
+                    self.is_rotating = false;
+                    let dt = 0.016;
+                    self.rotate_velocity_yaw = self.rotate_velocity_yaw * 0.4 + (dx / dt) * 0.6;
+                    self.rotate_velocity_pitch = self.rotate_velocity_pitch * 0.4 + (-dy / dt) * 0.6;
+                    true
+                }
+                MouseScrollDelta::PixelDelta(pos) => {
+                    let mut dx = (pos.x as f32 / scale) * 0.005 * self.scroll_speed;
+                    let mut dy = (pos.y as f32 / scale) * 0.005 * self.scroll_speed;
+
+                    self.rotate_accum_yaw += dx;
+                    self.rotate_accum_pitch -= dy;
+                    if self.scroll_lock == 0 {
+                        if self.rotate_accum_yaw.abs() > 0.002 || self.rotate_accum_pitch.abs() > 0.002 {
+                            if self.rotate_accum_pitch.abs() > 1.2 * self.rotate_accum_yaw.abs() {
+                                self.scroll_lock = 2;
+                            } else if self.rotate_accum_yaw.abs() > 1.2 * self.rotate_accum_pitch.abs() {
+                                self.scroll_lock = 1;
+                            }
+                        }
+                    } else {
+                        if self.scroll_lock == 1 {
+                            dy = 0.0;
+                        } else {
+                            dx = 0.0;
+                        }
+                    }
+
+                    if self.active_camera != "Default Camera" {
+                        self.pending_yaw += dx;
+                        self.pending_pitch += -dy;
+                    } else {
+                        self.rotation_y += dx;
+                        self.rotation_x -= dy;
+                    }
+
+                    self.is_rotating = true;
+                    self.last_rotate_time = std::time::Instant::now();
+                    true
+                }
+            }
+        }
+    }
+
+    fn tick(&mut self, dt: f32, _ctx: &mut UiContext) -> bool {
+        let now = std::time::Instant::now();
+        let mut changed = false;
+
+        if self.is_rotating {
+            if now.duration_since(self.last_rotate_time).as_secs_f32() > 0.05 {
+                self.is_rotating = false;
+                self.scroll_lock = 0;
+            } else if dt > 1e-5 {
+                let vel_yaw = self.rotate_accum_yaw / dt;
+                let vel_pitch = self.rotate_accum_pitch / dt;
+                self.rotate_velocity_yaw = self.rotate_velocity_yaw * 0.4 + vel_yaw * 0.6;
+                self.rotate_velocity_pitch = self.rotate_velocity_pitch * 0.4 + vel_pitch * 0.6;
+            }
+            self.rotate_accum_yaw = 0.0;
+            self.rotate_accum_pitch = 0.0;
+        }
+
+        if self.is_zooming {
+            if now.duration_since(self.last_zoom_time).as_secs_f32() > 0.05 {
+                self.is_zooming = false;
+            } else if dt > 1e-5 {
+                let vel_zoom = self.zoom_accum / dt;
+                self.zoom_velocity = self.zoom_velocity * 0.4 + vel_zoom * 0.6;
+            }
+            self.zoom_accum = 0.0;
+        }
+
+        if !self.is_rotating && (self.rotate_velocity_yaw.abs() > 0.001 || self.rotate_velocity_pitch.abs() > 0.001) {
+            if !self.inertial_scroll {
+                self.rotate_velocity_yaw = 0.0;
+                self.rotate_velocity_pitch = 0.0;
+            } else {
+                let dx = self.rotate_velocity_yaw * dt;
+                let dy = self.rotate_velocity_pitch * dt;
+                
+                if self.active_camera != "Default Camera" {
+                    self.pending_yaw += dx;
+                    self.pending_pitch += dy;
+                } else {
+                    self.rotation_y += dx;
+                    self.rotation_x += dy;
+                }
+
+                let decay = self.scroll_friction.powf(dt * 60.0);
+                self.rotate_velocity_yaw *= decay;
+                self.rotate_velocity_pitch *= decay;
+
+                if self.rotate_velocity_yaw.abs() < 0.01 { self.rotate_velocity_yaw = 0.0; }
+                if self.rotate_velocity_pitch.abs() < 0.01 { self.rotate_velocity_pitch = 0.0; }
+                changed = true;
+            }
+        }
+
+        if !self.is_zooming && self.zoom_velocity.abs() > 0.001 {
+            if !self.inertial_scroll {
+                self.zoom_velocity = 0.0;
+            } else {
+                let d_zoom = self.zoom_velocity * dt;
+                self.zoom *= (-d_zoom).exp();
+                self.zoom = self.zoom.clamp(0.05, 20.0);
+
+                let decay = self.scroll_friction.powf(dt * 60.0);
+                self.zoom_velocity *= decay;
+                if self.zoom_velocity.abs() < 0.01 { self.zoom_velocity = 0.0; }
+                changed = true;
+            }
+        }
+
+        changed
+    }
+}