diff --git a/crates/compositor/src/compositor_linux.rs b/crates/compositor/src/compositor_linux.rs index 464b1debe..0b464ed83 100644 --- a/crates/compositor/src/compositor_linux.rs +++ b/crates/compositor/src/compositor_linux.rs @@ -3230,30 +3230,28 @@ impl Compositor { /// Le cristal de Prism Glow : il lit la copie de l'image composee (`CursorPlan::glass`). glass: bool, } - // La pointe et les clics récents, tels que les effets shader les lisent. - let mut fx_cursor = None; - let mut fx_clicks = Vec::new(); + // La pointe et les clics récents, tels que les effets shader les lisent : ceux de + // l'enregistrement, que le curseur soit dessiné ou non (`effect_pointer`). + let mut fx_pointer = crate::frame_geometry::EffectPointer::default(); let cursor_draw: Option = (|| { let track = cursor_ref.as_ref()?; - let plan = plan_cursor( - &g, - &CursorPlanInput { - render_px: [rw, rh], - u_max, - v_max, - cfg, - live: lp, - scene: scene_ref.as_ref(), - track, - t: self - .cursor_time - .borrow() - .unwrap_or(frame / crate::frame_geometry::FPS), - }, - )? - .for_backend(self.gpu.backend == crate::d3d::Backend::Cpu); - fx_cursor = Some(plan.pointer); - fx_clicks = plan.clicks.clone(); + let input = CursorPlanInput { + render_px: [rw, rh], + u_max, + v_max, + cfg, + live: lp, + scene: scene_ref.as_ref(), + track, + t: self + .cursor_time + .borrow() + .unwrap_or(frame / crate::frame_geometry::FPS), + }; + if scene_ref.as_ref().is_some_and(|s| !s.shader_effects.is_empty()) { + fx_pointer = crate::frame_geometry::effect_pointer(&g, &input); + } + let plan = plan_cursor(&g, &input)?.for_backend(self.gpu.backend == crate::d3d::Backend::Cpu); // `taps == 1` : un seul placement, celui de l'instant rendu -- le // chemin net d'avant, inchange. Au-dela, on echelonne les copies @@ -3751,8 +3749,8 @@ impl Compositor { // D.1) : `image` avant les annotations, `text` sur un calque des textes a part, `both` sur // l'image finale. Meme ordre que `compositor_windows.rs`. let fx_inputs = crate::shader_fx::FxFrameInputs { - cursor: fx_cursor, - clicks: fx_clicks, + cursor: fx_pointer.cursor, + clicks: fx_pointer.clicks, footage: g.footage_quad([rw, rh]), footage_aspect: g.footage_aspect([rw, rh]), camera: lp.has_webcam.then_some(g.w_dst), @@ -7052,6 +7050,24 @@ mod tests { assert_eq!(corner, [0, 255, 0]); } + /// La scène de ces tests masque le curseur (`cursor.show` faux) : rien n'est dessiné, mais un + /// effet voit toujours la pointe et le clic de l'enregistrement (`effect_pointer`). Il peint + /// `ctx.cursor.z` en rouge et `ctx.clicks[0].w` en vert : jaune, il a vu les deux. + #[test] + fn a_hidden_cursor_still_hands_the_effect_its_pointer_and_clicks() { + let Some(gpu) = gpu() else { return }; + const SEES: &str = "fn effect(uv: vec2) -> vec4 { return vec4(ctx.cursor.z, ctx.clicks[0].w, 0.0, 1.0); }"; + let comp = fx_compositor(&gpu, 320, 180, serde_json::json!([fx(SEES, "image", 0)])); + comp.set_cursor(crate::cursor::CursorTrack::new_with_visibility( + vec![(0.0, 0.5, 0.5), (4.0, 0.5, 0.5)], + vec![1.5], + vec![], + vec![(0.0, true)], + )); + comp.set_cursor_time(Some(2.0)); + assert_eq!(corner_at(&comp, &gpu, 2.0), [255, 255, 0]); + } + /// Les lanes s'appliquent dans l'ordre : bleu puis inversion donne jaune, l'inverse donne bleu. #[test] fn lanes_apply_in_order() { diff --git a/crates/compositor/src/compositor_macos.rs b/crates/compositor/src/compositor_macos.rs index 8eb51ed78..cef3da69b 100644 --- a/crates/compositor/src/compositor_macos.rs +++ b/crates/compositor/src/compositor_macos.rs @@ -2744,29 +2744,28 @@ impl Compositor { // composée, n'y voit que des pixels déjà floutés. self.draw_annotations(cmd_buf, &self.rt, scene_ref.as_ref(), g.source_t, g.s_ann, &g, true)?; - // La pointe et les clics récents, tels que les effets shader les lisent. - let mut fx_cursor = None; - let mut fx_clicks = Vec::new(); + // La pointe et les clics récents, tels que les effets shader les lisent : ceux de + // l'enregistrement, que le curseur soit dessiné ou non (`effect_pointer`). + let mut fx_pointer = crate::frame_geometry::EffectPointer::default(); // --- curseur --- (parité `compositor_windows.rs`, section « curseur custom ») if let Some(track) = cursor_ref.as_ref() { - let plan = crate::frame_geometry::plan_cursor( - &g, - &crate::frame_geometry::CursorPlanInput { - render_px: [rw, rh], - u_max, - v_max, - cfg, - live: lp, - scene: scene_ref.as_ref(), - track, - t: self.cursor_time.borrow().unwrap_or(frame / crate::frame_geometry::FPS), - }, - ) - .map(|p| p.for_backend(self.gpu.backend == crate::d3d::Backend::Cpu)); + let input = crate::frame_geometry::CursorPlanInput { + render_px: [rw, rh], + u_max, + v_max, + cfg, + live: lp, + scene: scene_ref.as_ref(), + track, + t: self.cursor_time.borrow().unwrap_or(frame / crate::frame_geometry::FPS), + }; + if scene_ref.as_ref().is_some_and(|s| !s.shader_effects.is_empty()) { + fx_pointer = crate::frame_geometry::effect_pointer(&g, &input); + } + let plan = crate::frame_geometry::plan_cursor(&g, &input) + .map(|p| p.for_backend(self.gpu.backend == crate::d3d::Backend::Cpu)); if let Some(plan) = plan { - fx_cursor = Some(plan.pointer); - fx_clicks = plan.clicks.clone(); let sprites = scene_ref .as_ref() .map(|s| s.cursor.cursor_sprites.clone()) @@ -2968,8 +2967,8 @@ impl Compositor { // D.1) : `image` avant les annotations et les sous-titres, `text` sur un calque des textes // à part, `both` sur l'image finale. Tout dans le command buffer de la frame. let fx_inputs = crate::shader_fx::FxFrameInputs { - cursor: fx_cursor, - clicks: fx_clicks, + cursor: fx_pointer.cursor, + clicks: fx_pointer.clicks, footage: g.footage_quad([rw, rh]), footage_aspect: g.footage_aspect([rw, rh]), camera: lp.has_webcam.then_some(g.w_dst), @@ -4958,6 +4957,28 @@ mod tests { } } + /// La scène de ces tests masque le curseur (`cursor.show` faux) : rien n'est dessiné, mais un + /// effet voit toujours la pointe et le clic de l'enregistrement (`effect_pointer`). Il peint + /// `ctx.cursor.z` en rouge et `ctx.clicks[0].w` en vert : jaune, il a vu les deux. + #[test] + fn a_hidden_cursor_still_hands_the_effect_its_pointer_and_clicks() { + let Ok(gpu) = crate::d3d::Gpu::create(false) else { + eprintln!("pas de device Metal — test sauté"); + return; + }; + const SEES: &str = "fn effect(uv: vec2) -> vec4 { return vec4(ctx.cursor.z, ctx.clicks[0].w, 0.0, 1.0); }"; + let comp = super::Compositor::new_sized(&gpu, 320, 180).expect("compositeur"); + comp.set_scene(Some(shader_fx_scene(serde_json::json!([fx(SEES, "image", 0)])))); + comp.set_cursor(crate::cursor::CursorTrack::new_with_visibility( + vec![(0.0, 0.5, 0.5), (4.0, 0.5, 0.5)], + vec![1.5], + vec![], + vec![(0.0, true)], + )); + comp.set_cursor_time(Some(2.0)); + assert_eq!(corner_at(&comp, 2.0), [255, 255, 0]); + } + /// Les lanes s'appliquent dans l'ordre : bleu puis inversion donne jaune, l'inverse donne bleu. #[test] fn lanes_apply_in_order() { diff --git a/crates/compositor/src/compositor_windows.rs b/crates/compositor/src/compositor_windows.rs index 5627c35b4..34c5b03a8 100644 --- a/crates/compositor/src/compositor_windows.rs +++ b/crates/compositor/src/compositor_windows.rs @@ -2578,30 +2578,28 @@ impl Compositor { // pixels déjà floutés. Les autres annotations restent le calque le plus haut (plus bas). self.draw_annotations(scene_ref.as_ref(), source_t, s_ann, &g, true); - // La pointe et les clics récents, tels que les effets shader les lisent. - let mut fx_cursor = None; - let mut fx_clicks = Vec::new(); + // La pointe et les clics récents, tels que les effets shader les lisent : ceux de + // l'enregistrement, que le curseur soit dessiné ou non (`effect_pointer`). + let mut fx_pointer = crate::frame_geometry::EffectPointer::default(); // --- curseur custom : suit le mapping src/dst (zoom+layout), click bounce, // et flou de mouvement (parité `compositor_macos.rs` et `compositor_linux.rs`) --- if let Some(track) = cursor_ref.as_ref() { - let plan = crate::frame_geometry::plan_cursor( - &g, - &crate::frame_geometry::CursorPlanInput { - render_px: [self.rw(), self.rh()], - u_max, - v_max, - cfg, - live: lp, - scene: scene_ref.as_ref(), - track, - t: self.cursor_t_override.borrow().unwrap_or(frame / FPS), - }, - ) - .map(|p| p.for_backend(self.cpu_backend)); + let input = crate::frame_geometry::CursorPlanInput { + render_px: [self.rw(), self.rh()], + u_max, + v_max, + cfg, + live: lp, + scene: scene_ref.as_ref(), + track, + t: self.cursor_t_override.borrow().unwrap_or(frame / FPS), + }; + if scene_ref.as_ref().is_some_and(|s| !s.shader_effects.is_empty()) { + fx_pointer = crate::frame_geometry::effect_pointer(&g, &input); + } + let plan = crate::frame_geometry::plan_cursor(&g, &input).map(|p| p.for_backend(self.cpu_backend)); if let Some(plan) = plan { - fx_cursor = Some(plan.pointer); - fx_clicks = plan.clicks.clone(); let cursor_sprites: HashMap = scene_ref .as_ref() .map(|s| s.cursor.cursor_sprites.clone()) @@ -2813,8 +2811,8 @@ impl Compositor { // D.1) : `image` avant les annotations et les sous-titres, `text` sur un calque des textes // à part, `both` sur l'image finale. let fx_inputs = crate::shader_fx::FxFrameInputs { - cursor: fx_cursor, - clicks: fx_clicks, + cursor: fx_pointer.cursor, + clicks: fx_pointer.clicks, footage: g.footage_quad([self.rw(), self.rh()]), footage_aspect: g.footage_aspect([self.rw(), self.rh()]), camera: lp.has_webcam.then_some(w_dst), @@ -4043,6 +4041,28 @@ mod tests { } } + /// La scène de ces tests masque le curseur (`cursor.show` faux) : rien n'est dessiné, mais un + /// effet voit toujours la pointe et le clic de l'enregistrement (`effect_pointer`). Il peint + /// `ctx.cursor.z` en rouge et `ctx.clicks[0].w` en vert : jaune, il a vu les deux. + #[test] + fn a_hidden_cursor_still_hands_the_effect_its_pointer_and_clicks() { + let Ok(gpu) = crate::d3d::Gpu::create_auto(false) else { + eprintln!("pas de device D3D11 — test sauté"); + return; + }; + const SEES: &str = "fn effect(uv: vec2) -> vec4 { return vec4(ctx.cursor.z, ctx.clicks[0].w, 0.0, 1.0); }"; + let comp = Compositor::new_sized(&gpu, 320, 180).expect("compositeur"); + comp.set_scene(Some(shader_fx_scene(serde_json::json!([fx(SEES, "image", 0)])))); + comp.set_cursor(crate::cursor::CursorTrack::new_with_visibility( + vec![(0.0, 0.5, 0.5), (4.0, 0.5, 0.5)], + vec![1.5], + vec![], + vec![(0.0, true)], + )); + comp.set_cursor_time(Some(2.0)); + assert_eq!(corner_at(&comp, &gpu, 2.0), [255, 255, 0]); + } + /// Les lanes s'appliquent dans l'ordre : bleu puis inversion donne jaune, l'inverse donne bleu. #[test] fn lanes_apply_in_order() { diff --git a/crates/compositor/src/frame_geometry.rs b/crates/compositor/src/frame_geometry.rs index c68321efe..86b881f27 100644 --- a/crates/compositor/src/frame_geometry.rs +++ b/crates/compositor/src/frame_geometry.rs @@ -3604,13 +3604,6 @@ pub struct CursorPlan { /// composée, le métrage et ses flous de confidentialité, que le backend lui copie juste avant /// de le dessiner. Seuls des pixels floutés passent ainsi à travers le verre. pub glass: bool, - /// Pour les effets shader (`shader_fx`) : la pointe en 0..1 de SORTIE, zoom, layout et tilt - /// appliqués — ce qu'un effet ne peut pas recalculer lui-même. - pub pointer: [f32; 2], - /// Les clics des `shader_fx::CLICK_WINDOW_S` dernières secondes, du plus récent au plus ancien, - /// au plus `shader_fx::MAX_CLICKS` : le point de la pointe AU MOMENT du clic, en 0..1 de sortie, - /// et l'âge en secondes. - pub clicks: Vec<([f32; 2], f32)>, } impl CursorPlan { @@ -3719,6 +3712,119 @@ fn click_impact_at( ) } +/// Pose un point de la piste (repère normalisé de l'écran) sur la sortie, à travers le zoom, le +/// layout et le tilt de la frame : ce que partagent le curseur dessiné (`plan_cursor`) et les +/// effets shader (`effect_pointer`), pour qu'un effet voie la pointe là où le curseur se dessine. +struct CursorPlacer<'a> { + g: &'a FrameGeometry, + track: &'a crate::cursor::CursorTrack, + uv_max: [f32; 2], + render_px: [f32; 2], + /// Le rect d'écran non incliné, en px. + s_px: [f32; 2], + tilt: Option, + /// Centre du plan en px de sortie. + quad_center_px: [f32; 2], + model3d: bool, +} + +impl<'a> CursorPlacer<'a> { + fn new(g: &'a FrameGeometry, input: &CursorPlanInput<'a>, model3d: bool) -> Self { + let [rw, rh] = input.render_px; + let s_px = [g.s_dst[2] * rw, g.s_dst[3] * rh]; + CursorPlacer { + g, + track: input.track, + uv_max: [input.u_max, input.v_max], + render_px: input.render_px, + s_px, + tilt: g.screen_tilt(s_px), + quad_center_px: [(g.s_dst[0] + g.s_dst[2] * 0.5) * rw, (g.s_dst[1] + g.s_dst[3] * 0.5) * rh], + model3d, + } + } + + /// La pointe à `t`. Le curseur modélisé touche le plan à chaque clic : il se pose sur le point + /// cliqué brut (`pinned_at`), pas sur la piste lissée qui traîne derrière la souris. Le sprite + /// plat garde la piste telle quelle, son rendu est celui d'avant. + fn at(&self, t: f32) -> Option<(f32, f32)> { + if self.model3d { self.track.pinned_at(t) } else { self.track.at(t) } + } + + /// `None` hors de la coupe courante (zoom serré, hors écran). + fn place(&self, cxy: Option<(f32, f32)>, dst: [f32; 4]) -> Option { + let [rw, rh] = self.render_px; + let [fx, fy] = cursor_plane_point(self.g.cut, self.uv_max, cxy?)?; + Some(match self.tilt.as_ref() { + Some(&quad) => CursorPlacement::Tilted { + plane_pt: [fx, fy], + quad, + center_px: self.quad_center_px, + screen_px: self.s_px, + render_px: [rw, rh], + }, + // Le curseur modélisé a toujours besoin d'un plan : sur écran droit, un plan + // IDENTITÉ taillé dans `dst`. À rotation nulle `rotated_quad_corners_px` rend les + // coins exacts (±w/2, ±h/2, échelle 1) : même caméra, même mode 15. + None if self.model3d => { + let screen_px = [dst[2] * rw, dst[3] * rh]; + CursorPlacement::Tilted { + plane_pt: [fx, fy], + quad: crate::regions::rotated_quad_corners_px( + screen_px[0], + screen_px[1], + [0.0; 3], + [0.0; 3], + ), + center_px: [(dst[0] + dst[2] * 0.5) * rw, (dst[1] + dst[3] * 0.5) * rh], + screen_px, + render_px: [rw, rh], + } + } + None => CursorPlacement::Upright { center: [dst[0] + fx * dst[2], dst[1] + fy * dst[3]] }, + }) + } +} + +/// Ce que les effets shader lisent du pointeur (`ctx.cursor`, `ctx.clicks`), en 0..1 de SORTIE, +/// zoom, layout et tilt appliqués : ce qu'un effet ne peut pas recalculer lui-même. +#[derive(Debug, Clone, Default, PartialEq)] +pub struct EffectPointer { + /// La pointe, là où le curseur se dessine ; `None` quand l'enregistrement l'a cachée + /// (`visible: false`) ou qu'elle sort de la coupe. + pub cursor: Option<[f32; 2]>, + /// Les clics des `shader_fx::CLICK_WINDOW_S` dernières secondes, du plus récent au plus ancien, + /// au plus `shader_fx::MAX_CLICKS` : le point de la pointe AU MOMENT du clic, et l'âge en + /// secondes. + pub clicks: Vec<([f32; 2], f32)>, +} + +/// La pointe et les clics récents pour les effets, que le curseur soit dessiné ou non. Masqué par +/// l'utilisateur (`cursor.show`, l'auto-hide, `hideCursor` d'un zoom), il reste la pointe de +/// l'enregistrement : un effet peut suivre la souris, ou remplacer le curseur par son propre +/// dessin. Seuls l'enregistrement qui la cache et la coupe qui la laisse dehors l'en privent. +pub fn effect_pointer(g: &FrameGeometry, input: &CursorPlanInput) -> EffectPointer { + let model3d = input.live.cursor_model3d + && modelled_sprite(input.scene, input.track.type_at(input.t)).is_some(); + let placer = CursorPlacer::new(g, input, model3d); + let cursor = input + .track + .visible_at(input.t) + .then(|| placer.place(placer.at(input.t), g.s_dst)) + .flatten() + .map(|p| p.upright_center()); + let clicks = input + .track + .clicks_with_points(input.t - crate::shader_fx::CLICK_WINDOW_S, input.t) + .collect::>() + .into_iter() + .rev() + .take(crate::shader_fx::MAX_CLICKS) + .filter_map(|(tc, p)| Some((placer.place(Some(p), g.s_dst)?.upright_center(), input.t - tc))) + .collect(); + EffectPointer { cursor, clicks } +} + /// `None` = rien à dessiner cette frame : curseur masqué, pointeur hors du rect source /// courant (zoom serré, hors écran), ou sprite réduit à rien au creux d'un click bounce /// extrême — un état normal en lecture, pas une erreur. @@ -3735,56 +3841,14 @@ pub fn plan_cursor(g: &FrameGeometry, input: &CursorPlanInput) -> Option, dst: [f32; 4]| -> Option { - cxy.and_then(|p| { - let [fx, fy] = cursor_plane_point(g.cut, [input.u_max, input.v_max], p)?; - Some(match tilt.as_ref() { - Some(&quad) => CursorPlacement::Tilted { - plane_pt: [fx, fy], - quad, - center_px: quad_center_px, - screen_px: s_px, - render_px: [rw, rh], - }, - // Le curseur modélisé a toujours besoin d'un plan : sur écran droit, un plan - // IDENTITÉ taillé dans `dst`. À rotation nulle `rotated_quad_corners_px` rend les - // coins exacts (±w/2, ±h/2, échelle 1) : même caméra, même mode 15. - None if model3d => { - let screen_px = [dst[2] * rw, dst[3] * rh]; - CursorPlacement::Tilted { - plane_pt: [fx, fy], - quad: crate::regions::rotated_quad_corners_px( - screen_px[0], - screen_px[1], - [0.0; 3], - [0.0; 3], - ), - center_px: [(dst[0] + dst[2] * 0.5) * rw, (dst[1] + dst[3] * 0.5) * rh], - screen_px, - render_px: [rw, rh], - } - } - None => CursorPlacement::Upright { - center: [dst[0] + fx * dst[2], dst[1] + fy * dst[3]], - }, - }) - }) - }; - // Le curseur modélisé touche le plan à chaque clic : il se pose sur le point cliqué brut - // (`pinned_at`), pas sur la piste lissée qui traîne derrière la souris. Le sprite plat garde - // la piste telle quelle, son rendu est celui d'avant. - let at = |t: f32| if model3d { input.track.pinned_at(t) } else { input.track.at(t) }; - let placement = place(at(input.t), g.s_dst)?; + let placer = CursorPlacer::new(g, input, model3d); + let (s_px, tilt, quad_center_px) = (placer.s_px, placer.tilt, placer.quad_center_px); + let place = |cxy: Option<(f32, f32)>, dst: [f32; 4]| placer.place(cxy, dst); + let placement = place(placer.at(input.t), g.s_dst)?; let lp = input.live; // `cursor_bounce_scale` est le clickBounce brut (0..5) : au-delà de 1/0.24 ≈ 4.17, le creux @@ -3823,7 +3887,7 @@ pub fn plan_cursor(g: &FrameGeometry, input: &CursorPlanInput) -> Option Option>() - .into_iter() - .rev() - .take(crate::shader_fx::MAX_CLICKS) - .filter_map(|(tc, p)| Some((place(Some(p), g.s_dst)?.upright_center(), input.t - tc))) - .collect(); - Some(CursorPlan { - pointer: placement.upright_center(), - clicks, placement, prev_placement, size_px, @@ -8873,44 +8925,79 @@ mod tests { assert!(!plan(true, 1.08).expect("visible").impacts.is_empty(), "garde : l'anneau du clic"); } + /// La piste des deux tests suivants : deux clics, à 1,0 s et à 1,2 s, puis la pointe s'en va. + /// `hidden_from` : l'instant où l'enregistrement cache la pointe (`visible: false`). + fn clicked_track(hidden_from: Option) -> crate::cursor::CursorTrack { + let mut visibility = vec![(0.0, true)]; + visibility.extend(hidden_from.map(|t| (t, false))); + crate::cursor::CursorTrack::new_with_visibility( + vec![(0.0, 0.3, 0.6), (1.0, 0.3, 0.6), (1.2, 0.5, 0.4), (2.0, 0.8, 0.2), (6.0, 0.8, 0.2)], + vec![1.0, 1.2], + vec![], + visibility, + ) + } + + fn cursor_input<'a>( + scene: &'a Scene, + cfg: &'a crate::config::Cfg, + track: &'a crate::cursor::CursorTrack, + t: f32, + ) -> CursorPlanInput<'a> { + CursorPlanInput { + render_px: [1920.0, 1080.0], + u_max: 1.0, + v_max: 1.0, + cfg, + live: LiveParams::default(), + scene: Some(scene), + track, + t, + } + } + /// Les effets shader lisent la pointe et les clics récents en uv de SORTIE : un clic garde le - /// point où la pointe était quand il a eu lieu, le plus récent vient en premier, et un clic - /// sort de la liste après la fenêtre. + /// point où la pointe était quand il a eu lieu, le plus récent vient en premier, un clic sort + /// de la liste après la fenêtre, et la pointe est là où le curseur se dessine. #[test] - fn the_plan_hands_effects_the_pointer_and_the_recent_clicks() { + fn effects_get_the_pointer_and_the_recent_clicks() { let scene = model_scene(); let cfg = crate::config::all().pop().expect("cfg"); let fg = full_frame_geometry(); - let track = crate::cursor::CursorTrack::new_with_visibility( - vec![(0.0, 0.3, 0.6), (1.0, 0.3, 0.6), (1.2, 0.5, 0.4), (2.0, 0.8, 0.2), (6.0, 0.8, 0.2)], - vec![1.0, 1.2], - vec![], - vec![(0.0, true)], - ); - let plan = |t: f32| { - plan_cursor( - &fg, - &CursorPlanInput { - render_px: [1920.0, 1080.0], - u_max: 1.0, - v_max: 1.0, - cfg: &cfg, - live: LiveParams::default(), - scene: Some(&scene), - track: &track, - t, - }, - ) - .expect("visible") - }; - assert!(plan(0.5).clicks.is_empty(), "pas encore de clic"); - let (first, second) = (plan(1.0).pointer, plan(1.2).pointer); - let clicks = plan(1.5).clicks; + let track = clicked_track(None); + let fx = |t: f32| effect_pointer(&fg, &cursor_input(&scene, &cfg, &track, t)); + assert!(fx(0.5).clicks.is_empty(), "pas encore de clic"); + let (first, second) = (fx(1.0).cursor.expect("pointe"), fx(1.2).cursor.expect("pointe")); + let clicks = fx(1.5).clicks; assert_eq!(clicks.len(), 2); assert_eq!((clicks[0].0, clicks[1].0), (second, first), "le plus récent d'abord, au point du clic"); assert!((clicks[0].1 - 0.3).abs() < 1e-4 && (clicks[1].1 - 0.5).abs() < 1e-4, "{clicks:?}"); - assert_ne!(plan(2.5).pointer, second, "la pointe a bougé depuis"); - assert_eq!(plan(1.1 + crate::shader_fx::CLICK_WINDOW_S).clicks.len(), 1, "le premier est sorti"); + assert_ne!(fx(2.5).cursor, Some(second), "la pointe a bougé depuis"); + assert_eq!(fx(1.1 + crate::shader_fx::CLICK_WINDOW_S).clicks.len(), 1, "le premier est sorti"); + let drawn = plan_cursor(&fg, &cursor_input(&scene, &cfg, &track, 2.5)).expect("visible"); + assert_eq!(fx(2.5).cursor, Some(drawn.placement.upright_center()), "là où le curseur se dessine"); + } + + /// Masqué par l'utilisateur, le curseur n'est plus dessiné, mais les effets gardent la pointe + /// et les clics : un halo suit toujours la souris, une onde part toujours du clic. Quand c'est + /// l'enregistrement qui cache la pointe, les effets la perdent, et gardent les clics d'avant. + #[test] + fn a_hidden_cursor_still_hands_effects_the_pointer_and_the_clicks() { + let shown = model_scene(); + let mut hidden = model_scene(); + hidden.cursor.show = false; + let cfg = crate::config::all().pop().expect("cfg"); + let fg = full_frame_geometry(); + let track = clicked_track(None); + assert!(plan_cursor(&fg, &cursor_input(&hidden, &cfg, &track, 1.5)).is_none(), "rien à dessiner"); + let seen = effect_pointer(&fg, &cursor_input(&hidden, &cfg, &track, 1.5)); + assert_eq!(seen, effect_pointer(&fg, &cursor_input(&shown, &cfg, &track, 1.5))); + assert!(seen.cursor.is_some() && seen.clicks.len() == 2, "{seen:?}"); + + let recorded_hidden = clicked_track(Some(1.3)); + let after = effect_pointer(&fg, &cursor_input(&shown, &cfg, &recorded_hidden, 1.5)); + assert_eq!(after.cursor, None, "l'enregistrement a caché la pointe"); + assert_eq!(after.clicks, seen.clicks, "les clics d'avant restent"); } /// Sous un angle fixe, l'écran tourne autour du centre de sa boîte ZOOMÉE, réduit de son diff --git a/docs/shader-effects.md b/docs/shader-effects.md index bedb0af89..24104cf4c 100644 --- a/docs/shader-effects.md +++ b/docs/shader-effects.md @@ -213,7 +213,7 @@ libre, et les erreurs pointent ainsi les lignes que l'agent a écrites. | `ctx.aspect` | `f32` | Largeur sur hauteur de la sortie, pour les distances circulaires en uv. | | `ctx.time` | `f32` | Secondes de programme, continues à travers coupes, clips et vitesses. | | `ctx.region` | `vec4` | x : progression de 0 à 1 sur la pilule entière. y : sa durée en s. z : secondes depuis son début. | -| `ctx.cursor` | `vec4` | xy : la pointe en uv de sortie. z : 1 si elle est à l'écran. | +| `ctx.cursor` | `vec4` | xy : la pointe en uv de sortie. z : 1 si elle est à l'écran, même quand le curseur est masqué. | | `ctx.clicks[i]` | `vec4` | Les 4 derniers clics des 2 dernières secondes, du plus récent au plus ancien. xy : le point du clic en uv de sortie. z : secondes écoulées depuis. w : 1 si l'entrée est occupée. | | `ctx.footage` | `vec4` | Le rect de l'enregistrement en uv de sortie (x, y, w, h), après zoom et layout. Sous un tilt 3D, c'est sa boîte englobante. | | `ctx.camera` | `vec4` | Le rect de la webcam en uv de sortie. Taille nulle sans caméra. | @@ -226,12 +226,17 @@ libre, et les erreurs pointent ainsi les lignes que l'agent a écrites. | `fx_hash(p)` | `f32` | Hasard déterministe dans [0, 1) pour un `vec2`, identique sur tous les GPU (PCG). | - **Tout ce qui est en uv de sortie** est déjà passé par le zoom, le layout, le cadre et le - tilt. Ces valeurs viennent de `plan_frame` et `plan_cursor` (`frame_geometry.rs`), que les + tilt. Ces valeurs viennent de `plan_frame` et `effect_pointer` (`frame_geometry.rs`), que les trois backends partagent. - **Le plan de l'enregistrement** suit le warp que le compositeur dessine : bilinéaire sous un angle fixe, homographie sous la caméra réelle (`FootageQuad`, `quad_inverse` des shaders). L'effet choisit son repère : l'image de sortie, ou ce plan. Le contrat ne décide pas lequel convient à quel effet. +- **Le curseur masqué** (« Afficher le curseur » coupé, l'auto-hide, le `hideCursor` d'un zoom) + n'est plus dessiné, mais `ctx.cursor` et `ctx.clicks` restent ceux de l'enregistrement : un + halo suit toujours la souris, une onde part toujours du clic, et un effet peut remplacer le + curseur par son propre dessin. `ctx.cursor.z` ne vaut 0 que si l'enregistrement a caché la + pointe ou si elle sort du cadre (un zoom serré) ; les clics d'avant restent. - **Le point d'un clic** est la pointe à l'instant du clic, au même endroit que l'impact intégré (mode 16). - **Le curseur dessiné** passe par le lissage et peut traîner un instant derrière ce point quand diff --git a/technical-documentation/architecture/native-compositor.md b/technical-documentation/architecture/native-compositor.md index 43bf0ab65..a59becc09 100644 --- a/technical-documentation/architecture/native-compositor.md +++ b/technical-documentation/architecture/native-compositor.md @@ -216,9 +216,13 @@ backend-specific. receives the annotations; the layer is cleared in a pass of its own, since `draw_annotations` opens none when nothing is visible. - **`ctx` comes from the frame geometry** and is in output uv: the pointer and the last four clicks - (`plan_cursor`), the footage rect (`FrameGeometry::footage_quad`, bounding box under a tilt), the - camera rect, the region's progress (over the whole pill in programme time when the app sends + (`effect_pointer`), the footage rect (`FrameGeometry::footage_quad`, bounding box under a tilt), + the camera rect, the region's progress (over the whole pill in programme time when the app sends `programmeStartSec`/`programmeEndSec`), and programme time. +- **A hidden cursor still feeds effects.** `effect_pointer` places the pointer and the clicks as + `plan_cursor` places the cursor, but whether or not the cursor is drawn: with *Show cursor* off, + an effect still follows the mouse and still sees each click. Only the recording hiding the + pointer, or a zoom leaving it out of frame, takes the pointer away. naga stays at 24. From 25 on, its HLSL output declares samplers in a D3D12 heap (`register(s0, space0)`), which FXC rejects for `ps_5_0` (X3721).