1use std::cell::RefCell;
20use std::collections::HashMap;
21use std::rc::Rc;
22use std::sync::Arc;
23
24use crate::{Camera2d, PickEvent, click_within_slop_dp, dp_to_world, effective_fit};
25use glam::{DVec2, DVec4};
26use renamite_model::{Scene, ScenePaint};
27use renamite_render_bridge::{PreparedDraw, PreparedScene};
28use repose_core::input::{PointerButton, PointerEvent, PointerEventKind, PointerKind};
29use repose_core::locals::effective_density_scale;
30use repose_core::{
31 BlendMode, ClipOp, Modifier, Px, Rect, RenderContext, SceneNode, View,
32};
33use repose_ui::{Box as UiBox, ViewExt, ZStack};
34
35use crate::actors::{PlayerHost, PlayerHostRef, ViewTransform};
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
39pub struct ActorId(pub usize);
40
41struct Entry {
43 host: PlayerHostRef,
44 images: Option<bool>,
46}
47
48pub struct ActorLibrary {
49 entries: Vec<Entry>,
50}
51
52impl Default for ActorLibrary {
53 fn default() -> Self {
54 Self::new()
55 }
56}
57
58impl ActorLibrary {
59 pub fn new() -> Self {
60 Self {
61 entries: Vec::new(),
62 }
63 }
64
65 pub fn push_ren_str(&mut self, source: &str) -> Result<ActorId, renamite_player::PlayerError> {
67 Ok(self.push(PlayerHost::from_ren_str(source)?))
68 }
69
70 pub fn push_ren_bytes(
72 &mut self,
73 bytes: &[u8],
74 ) -> Result<ActorId, renamite_player::PlayerError> {
75 Ok(self.push(PlayerHost::from_ren_bytes(bytes)?))
76 }
77
78 pub fn push(&mut self, host: PlayerHost) -> ActorId {
79 self.entries.push(Entry {
80 host: Rc::new(RefCell::new(host)),
81 images: None,
82 });
83 ActorId(self.entries.len() - 1)
84 }
85
86 pub fn len(&self) -> usize {
87 self.entries.len()
88 }
89
90 pub fn is_empty(&self) -> bool {
91 self.entries.is_empty()
92 }
93
94 pub fn host(&self, id: ActorId) -> Option<&PlayerHostRef> {
95 self.entries.get(id.0).map(|entry| &entry.host)
96 }
97}
98
99#[derive(Clone, Copy, Debug, PartialEq)]
103pub enum ActorItem {
104 Rig { actor: ActorId, frame: f64 },
107 Raster {
111 page: u32,
112 uv_min: [f32; 2],
113 uv_max: [f32; 2],
114 size: [f32; 2],
115 },
116 Rect { half: [f32; 2] },
120}
121
122#[derive(Clone, Debug)]
124pub struct ActorDraw {
125 pub item: ActorItem,
126 pub world: DVec2,
128 pub rotation: f64,
129 pub scale: DVec2,
131 pub anchor: DVec2,
133 pub tint: DVec4,
135 pub clip: Option<[f64; 4]>,
139 pub blend: BlendMode,
140 pub z: f32,
142}
143
144impl Default for ActorDraw {
145 fn default() -> Self {
146 Self {
147 item: ActorItem::Rect { half: [0.0, 0.0] },
148 world: DVec2::ZERO,
149 rotation: 0.0,
150 scale: DVec2::ONE,
151 anchor: DVec2::splat(0.5),
152 tint: DVec4::ONE,
153 clip: None,
154 blend: BlendMode::Alpha,
155 z: 0.0,
156 }
157 }
158}
159
160#[derive(Default)]
161pub struct ActorDrawList {
162 draws: Vec<ActorDraw>,
163}
164
165impl ActorDrawList {
166 pub fn new() -> Self {
167 Self::default()
168 }
169
170 pub fn from_draws(draws: Vec<ActorDraw>) -> Self {
173 Self { draws }
174 }
175
176 pub fn clear(&mut self) {
177 self.draws.clear();
178 }
179
180 pub fn push(&mut self, draw: ActorDraw) {
181 self.draws.push(draw);
182 }
183
184 pub fn len(&self) -> usize {
185 self.draws.len()
186 }
187
188 pub fn is_empty(&self) -> bool {
189 self.draws.is_empty()
190 }
191
192 pub fn as_slice(&self) -> &[ActorDraw] {
193 &self.draws
194 }
195}
196
197fn compose(a: [f64; 6], b: [f64; 6]) -> [f64; 6] {
200 [
201 b[0] * a[0] + b[2] * a[1],
202 b[1] * a[0] + b[3] * a[1],
203 b[0] * a[2] + b[2] * a[3],
204 b[1] * a[2] + b[3] * a[3],
205 b[0] * a[4] + b[2] * a[5] + b[4],
206 b[1] * a[4] + b[3] * a[5] + b[5],
207 ]
208}
209
210fn instance_affine(draw: &ActorDraw, size: DVec2) -> [f64; 6] {
213 let (sin, cos) = draw.rotation.sin_cos();
214 let (sx, sy) = (draw.scale.x, draw.scale.y);
215 let affine = [cos * sx, sin * sx, -sin * sy, cos * sy, 0.0, 0.0];
216 let pivot = DVec2::new(draw.anchor.x * size.x, draw.anchor.y * size.y);
217 let rotated = apply(affine, pivot);
218 [
219 affine[0],
220 affine[1],
221 affine[2],
222 affine[3],
223 draw.world.x - rotated.x,
224 draw.world.y - rotated.y,
225 ]
226}
227
228fn view_affine(view: &ViewTransform) -> [f64; 6] {
229 [
230 view.scale,
231 0.0,
232 0.0,
233 view.scale,
234 view.offset.x,
235 view.offset.y,
236 ]
237}
238
239fn tint_key(tint: DVec4) -> [u64; 4] {
240 [
241 tint.x.to_bits(),
242 tint.y.to_bits(),
243 tint.z.to_bits(),
244 tint.w.to_bits(),
245 ]
246}
247
248fn artboard(player: &renamite_player::Player) -> DVec2 {
249 let comp = player.composition();
250 match player.project.document.compositions.get(comp) {
251 Some(c) => DVec2::new(c.size.0 as f64, c.size.1 as f64),
252 None => DVec2::ONE,
253 }
254}
255
256fn clip_mesh(x0: f64, y0: f64, x1: f64, y1: f64) -> Arc<repose_core::VectorMeshData> {
257 let vertices: Arc<[repose_core::VectorVertex]> = [
258 ([x0 as f32, y0 as f32], [0.0, 0.0]),
259 ([x1 as f32, y0 as f32], [1.0, 0.0]),
260 ([x1 as f32, y1 as f32], [1.0, 1.0]),
261 ([x0 as f32, y1 as f32], [0.0, 1.0]),
262 ]
263 .iter()
264 .map(|(pos, uv)| repose_core::VectorVertex {
265 pos: *pos,
266 color: [1.0, 1.0, 1.0, 1.0],
267 uv: *uv,
268 })
269 .collect();
270 Arc::new(repose_core::VectorMeshData {
271 vertices,
272 indices: [0u32, 1, 2, 0, 2, 3].into(),
273 })
274}
275
276fn apply(a: [f64; 6], p: DVec2) -> DVec2 {
277 DVec2::new(
278 a[0] * p.x + a[2] * p.y + a[4],
279 a[1] * p.x + a[3] * p.y + a[5],
280 )
281}
282
283fn tint_scene(scene: &Scene, tint: DVec4) -> Scene {
287 let mut out = scene.clone();
288 for item in &mut out.items {
289 if let ScenePaint::Solid(color) = &mut item.paint {
290 color.r *= tint.x;
291 color.g *= tint.y;
292 color.b *= tint.z;
293 color.a *= tint.w;
294 }
295 }
296 out
297}
298
299fn place_images(scene: &mut Scene, instance: [f64; 6]) {
305 for item in &mut scene.items {
306 if let ScenePaint::Image { affine, .. } = &mut item.paint {
307 *affine = compose(instance, *affine);
308 }
309 }
310}
311
312fn has_images(scene: &Scene) -> bool {
313 scene
314 .items
315 .iter()
316 .any(|item| matches!(item.paint, ScenePaint::Image { .. }))
317}
318
319fn camera_view(camera: &Camera2d, canvas_dp: [f32; 2], world_size: [f32; 2]) -> ViewTransform {
324 let (scale, ox, oy) = effective_fit(canvas_dp, world_size, camera);
325 let [cx, cy] = camera.effective_center();
326 ViewTransform {
327 scale: scale as f64,
328 offset: DVec2::new(
329 f64::from(ox) - f64::from(cx - world_size[0] * 0.5) * f64::from(scale),
330 f64::from(oy) - f64::from(cy - world_size[1] * 0.5) * f64::from(scale),
331 ),
332 }
333}
334
335fn camera_view_px(
337 camera: &Camera2d,
338 canvas_dp: [f32; 2],
339 world_size: [f32; 2],
340 density: f64,
341) -> ViewTransform {
342 let view = camera_view(camera, canvas_dp, world_size);
343 ViewTransform {
344 scale: view.scale * density,
345 offset: view.offset * density,
346 }
347}
348
349fn retarget_draws(base: &[PreparedDraw], instance: [f64; 6]) -> Vec<PreparedDraw> {
352 base.iter()
353 .map(|draw| match draw {
354 PreparedDraw::Vector {
355 mesh,
356 transform,
357 paint,
358 clips,
359 blend,
360 } => PreparedDraw::Vector {
361 mesh: mesh.clone(),
362 transform: compose(instance, transform.map(f64::from)).map(|value| value as f32),
363 paint: *paint,
364 clips: clips.clone(),
365 blend: *blend,
366 },
367 PreparedDraw::Image {
368 handle,
369 rect,
370 transform,
371 tint,
372 fit,
373 clips,
374 blend,
375 } => PreparedDraw::Image {
376 handle: *handle,
377 rect: *rect,
378 transform: *transform,
379 tint: *tint,
380 fit: *fit,
381 clips: clips.clone(),
382 blend: *blend,
383 },
384 })
385 .collect()
386}
387
388#[derive(Clone, Copy, PartialEq, Eq, Hash)]
389struct PrepKey {
390 actor: usize,
391 frame: u64,
392 tint: [u64; 4],
393}
394
395#[derive(Clone)]
398pub struct RasterAtlas {
399 pub desc: crate::BatchDesc,
400 pub uploads: std::sync::Arc<[crate::AtlasUpload]>,
401 pub batch_id: String,
402}
403
404pub fn actors_view_mixed(
409 library: Rc<RefCell<ActorLibrary>>,
410 draws: Rc<ActorDrawList>,
411 camera: Camera2d,
412 world_size: [f32; 2],
413 ctx: RenderContext,
414 atlas: Option<Rc<RasterAtlas>>,
415) -> View {
416 debug_assert!(
417 camera.roll.abs() < f32::EPSILON,
418 "actors_view_mixed does not apply Camera2d::roll"
419 );
420
421 let prepared: Rc<RefCell<HashMap<PrepKey, PreparedScene>>> = Rc::default();
422 let order: Rc<RefCell<Vec<u32>>> = Rc::default();
423
424 let modifier = Modifier::new().fill_max_size().hit_passthrough().painter(
425 move |scene: &mut repose_core::Scene, rect: Rect, _alpha: f32| {
426 let density = effective_density_scale().max(1e-6);
427 let canvas_dp = [rect.w / density, rect.h / density];
428 if canvas_dp[0] <= 1.0 || canvas_dp[1] <= 1.0 {
429 return;
430 }
431 let view = camera_view_px(&camera, canvas_dp, world_size, f64::from(density));
432
433 let entries = draws.as_slice();
434 {
435 let mut order = order.borrow_mut();
436 order.clear();
437 order.extend(0..entries.len() as u32);
438 order.sort_by(|a, b| compare_draws(&entries[*a as usize], &entries[*b as usize]));
439 }
440
441 prepared.borrow_mut().clear();
442 let sorted: Vec<u32> = order.borrow().clone();
443 let mut library = library.borrow_mut();
444 let mut cursor = 0usize;
445 let mut run_index = 0u32;
446 while cursor < sorted.len() {
447 let head = &entries[sorted[cursor] as usize];
448 let group = (medium_key(head.item), item_key(head.item), head.z.to_bits());
449 let mut end = cursor + 1;
450 while end < sorted.len() {
451 let next = &entries[sorted[end] as usize];
452 if (medium_key(next.item), item_key(next.item), next.z.to_bits()) != group
453 || next.clip.is_some()
454 {
455 break;
456 }
457 end += 1;
458 }
459 let run = cursor..end;
460 cursor = end;
461 let run_id = run_index;
462 run_index += 1;
463 emit_run(
464 run,
465 run_id,
466 &sorted,
467 entries,
468 &group.0,
469 &mut library,
470 &prepared,
471 &view,
472 &camera,
473 canvas_dp,
474 world_size,
475 density,
476 atlas.as_deref(),
477 rect,
478 scene,
479 &ctx,
480 );
481 }
482 },
483 );
484 UiBox(modifier)
485}
486
487#[allow(clippy::too_many_arguments)]
488fn emit_run(
489 run: std::ops::Range<usize>,
490 run_id: u32,
491 sorted: &[u32],
492 entries: &[ActorDraw],
493 medium: &Medium,
494 library: &mut ActorLibrary,
495 prepared: &Rc<RefCell<HashMap<PrepKey, PreparedScene>>>,
496 view: &ViewTransform,
497 camera: &Camera2d,
498 canvas_dp: [f32; 2],
499 world_size: [f32; 2],
500 density: f32,
501 atlas: Option<&RasterAtlas>,
502 rect: Rect,
503 scene: &mut repose_core::Scene,
504 ctx: &RenderContext,
505) {
506 match medium {
507 Medium::Rect => {
508 let mesh = unit_quad();
509 let mut instances = Vec::with_capacity(run.len());
510 for i in run {
511 let draw = &entries[sorted[i] as usize];
512 let ActorItem::Rect { half } = draw.item else {
513 continue;
514 };
515 instances.push(rect_instance(draw, half, view));
516 }
517 if instances.is_empty() {
518 return;
519 }
520 scene.nodes.push(SceneNode::VectorMeshInstanced {
521 mesh,
522 transforms: instances,
523 paint: repose_core::PaintDesc::Solid,
524 blend: BlendMode::Alpha,
525 });
526 }
527
528 Medium::Raster => {
529 let Some(atlas) = atlas else {
530 return;
531 };
532 let mut batch = crate::SpriteBatch::with_id(
533 format!("{}.r{}", atlas.batch_id, run_id),
534 atlas.desc.clone(),
535 );
536 batch.set_camera(camera.fit_matrix(canvas_dp, world_size));
537 for i in run {
538 let draw = &entries[sorted[i] as usize];
539 let ActorItem::Raster {
540 page,
541 uv_min,
542 uv_max,
543 size,
544 } = draw.item
545 else {
546 continue;
547 };
548 batch.push_sprite(&sprite_instance(draw, page, uv_min, uv_max, size));
549 }
550 let fit = crate::effective_fit(canvas_dp, world_size, camera);
551 scene.nodes.push(SceneNode::PushClip {
557 rect: fitted_box(rect, world_size, fit, camera.roll, density),
558 radius: [Px(0.0); 4],
559 op: ClipOp::Intersect,
560 });
561 scene
562 .nodes
563 .push(crate::BatchFrame::new(batch, atlas.uploads.clone()).node(rect));
564 scene.nodes.push(SceneNode::PopClip);
565 }
566
567 Medium::Rig => {
568 let head = &entries[sorted[run.start] as usize];
569 let ActorItem::Rig { actor, frame } = head.item else {
570 return;
571 };
572 let Some(entry) = library.entries.get_mut(actor.0) else {
573 return;
574 };
575 let PlayerHost {
576 player,
577 renderer,
578 dirty_images,
579 ..
580 } = &mut *entry.host.borrow_mut();
581 player.scrub(frame);
582 let raw = player.scene();
583 let size = artboard(player);
584 let images = *entry.images.get_or_insert_with(|| has_images(raw));
585 if *dirty_images {
586 renderer.sync_document_images(&player.project.document, ctx);
587 *dirty_images = false;
588 }
589
590 let key = PrepKey {
591 actor: actor.0,
592 frame: frame.to_bits(),
593 tint: tint_key(head.tint),
594 };
595 let source = if head.tint == DVec4::ONE {
596 raw.clone()
597 } else {
598 tint_scene(raw, head.tint)
599 };
600
601 if !images {
602 let mut cache = prepared.borrow_mut();
603 if !cache.contains_key(&key) {
604 cache.insert(key, renderer.prepare(&source, view));
605 }
606 let base = cache.get(&key).expect("inserted above");
607 if run.len() > 1 && base.is_batchable() {
608 let instances: Vec<[f64; 6]> = run
609 .clone()
610 .map(|i| instance_affine(&entries[sorted[i] as usize], size))
611 .collect();
612 renderer.append_repose_scene_instanced(base, &instances, scene);
613 return;
614 }
615 }
616
617 for i in run {
618 let draw = &entries[sorted[i] as usize];
619 let instance = instance_affine(draw, size);
620 if images {
621 let mut placed = source.clone();
622 place_images(&mut placed, instance);
623 let base = renderer.prepare(&placed, view);
624 let retargeted = PreparedScene {
625 draws: retarget_draws(&base.draws, instance),
626 clips: base.clips,
627 };
628 open_clip(scene, draw.clip, instance, view);
629 renderer.append_repose_scene(&retargeted, scene);
630 close_clip(scene, draw.clip.is_some());
631 continue;
632 }
633 let mut cache = prepared.borrow_mut();
634 let base = cache.get_mut(&key).expect("inserted above");
635 let retargeted = PreparedScene {
636 draws: retarget_draws(&base.draws, instance),
637 clips: std::mem::take(&mut base.clips),
638 };
639 drop(cache);
640 open_clip(scene, draw.clip, instance, view);
641 renderer.append_repose_scene(&retargeted, scene);
642 close_clip(scene, draw.clip.is_some());
643 if let Some(base) = prepared.borrow_mut().get_mut(&key) {
644 base.clips = retargeted.clips;
645 }
646 }
647 }
648 }
649}
650
651fn fitted_box(
660 rect: Rect,
661 world_size: [f32; 2],
662 fit: (f32, f32, f32),
663 roll: f32,
664 density: f32,
665) -> Rect {
666 let (s, ox, oy) = fit;
667 let (bw, bh) = (world_size[0] * s * density, world_size[1] * s * density);
668 let cx = rect.x + ox * density + bw * 0.5;
669 let cy = rect.y + oy * density + bh * 0.5;
670 let (hw, hh) = (bw * 0.5, bh * 0.5);
671 let (c, sn) = (roll.cos().abs(), roll.sin().abs());
672 let (ex, ey) = (hw * c + hh * sn, hw * sn + hh * c);
673 let aabb = Rect {
674 x: cx - ex,
675 y: cy - ey,
676 w: 2.0 * ex,
677 h: 2.0 * ey,
678 };
679 if !aabb.x.is_finite() || !aabb.y.is_finite() || !aabb.w.is_finite() || !aabb.h.is_finite() {
680 return rect;
681 }
682 let x = aabb.x.max(rect.x);
683 let y = aabb.y.max(rect.y);
684 let clip = Rect {
685 x,
686 y,
687 w: ((aabb.x + aabb.w).min(rect.x + rect.w) - x).max(0.0),
688 h: ((aabb.y + aabb.h).min(rect.y + rect.h) - y).max(0.0),
689 };
690 let x0 = clip.x.ceil();
691 let y0 = clip.y.ceil();
692 if (clip.x + clip.w).floor() - x0 >= 1.0 && (clip.y + clip.h).floor() - y0 >= 1.0 {
693 clip
694 } else {
695 rect
696 }
697}
698
699#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
700enum Medium {
701 Rig,
702 Raster,
703 Rect,
704}
705
706fn medium_key(item: ActorItem) -> Medium {
707 match item {
708 ActorItem::Rig { .. } => Medium::Rig,
709 ActorItem::Raster { .. } => Medium::Raster,
710 ActorItem::Rect { .. } => Medium::Rect,
711 }
712}
713
714fn compare_draws(a: &ActorDraw, b: &ActorDraw) -> std::cmp::Ordering {
721 match a.z.partial_cmp(&b.z).unwrap_or(std::cmp::Ordering::Equal) {
722 std::cmp::Ordering::Equal if batch_order() => medium_key(a.item)
723 .cmp(&medium_key(b.item))
724 .then_with(|| item_key(a.item).cmp(&item_key(b.item)))
725 .then_with(|| tint_key(a.tint).cmp(&tint_key(b.tint))),
726 other => other,
727 }
728}
729
730fn batch_order() -> bool {
731 static BATCH: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
732 *BATCH.get_or_init(|| std::env::var("REPAME_DRAW_ORDER").is_ok_and(|v| v == "batch"))
733}
734
735fn item_key(item: ActorItem) -> u64 {
736 match item {
737 ActorItem::Rig { actor, frame } => (actor.0 as u64) << 32 | (frame.to_bits() & 0xffff_ffff),
738 ActorItem::Raster { page, .. } => page as u64,
739 ActorItem::Rect { .. } => 0,
740 }
741}
742
743fn unit_quad() -> Arc<repose_core::VectorMeshData> {
744 thread_local! {
745 static QUAD: Arc<repose_core::VectorMeshData> = Arc::new(repose_core::VectorMeshData {
746 vertices: [
747 ([-1.0, -1.0], [0.0, 0.0]),
748 ([1.0, -1.0], [1.0, 0.0]),
749 ([1.0, 1.0], [1.0, 1.0]),
750 ([-1.0, 1.0], [0.0, 1.0]),
751 ]
752 .iter()
753 .map(|(pos, uv)| repose_core::VectorVertex {
754 pos: *pos,
755 color: [1.0, 1.0, 1.0, 1.0],
756 uv: *uv,
757 })
758 .collect::<Arc<[_]>>(),
759 indices: [0u32, 1, 2, 0, 2, 3].into(),
760 });
761 }
762 QUAD.with(Arc::clone)
763}
764
765fn rect_instance(draw: &ActorDraw, half: [f32; 2], view: &ViewTransform) -> [f32; 6] {
766 let full = [2.0 * half[0], 2.0 * half[1]];
767 let world = compose(instance_affine_size(draw, full), view_affine(view));
768 [
769 world[0] as f32,
770 world[1] as f32,
771 world[2] as f32,
772 world[3] as f32,
773 world[4] as f32,
774 world[5] as f32,
775 ]
776}
777
778fn instance_affine_size(draw: &ActorDraw, size: [f32; 2]) -> [f64; 6] {
781 let (sin, cos) = draw.rotation.sin_cos();
782 let (sx, sy) = (draw.scale.x, draw.scale.y);
783 let affine = [cos * sx, sin * sx, -sin * sy, cos * sy, 0.0, 0.0];
784 let pivot = DVec2::new(
785 draw.anchor.x * f64::from(size[0]),
786 draw.anchor.y * f64::from(size[1]),
787 );
788 let rotated = apply(affine, pivot);
789 [
790 affine[0],
791 affine[1],
792 affine[2],
793 affine[3],
794 draw.world.x - rotated.x,
795 draw.world.y - rotated.y,
796 ]
797}
798
799fn sprite_instance(
800 draw: &ActorDraw,
801 page: u32,
802 uv_min: [f32; 2],
803 uv_max: [f32; 2],
804 size: [f32; 2],
805) -> crate::SpriteInstance {
806 let (sx, sy) = (draw.scale.x as f32, draw.scale.y as f32);
807 crate::SpriteInstance {
808 center: glam::Vec2::new(draw.world.x as f32, draw.world.y as f32),
809 rotation: draw.rotation as f32,
810 size: glam::Vec2::new(size[0] * sx.abs(), size[1] * sy.abs()),
811 anchor: glam::Vec2::new(draw.anchor.x as f32, draw.anchor.y as f32),
812 flip_x: sx < 0.0,
813 flip_y: sy < 0.0,
814 uv_min: glam::Vec2::new(uv_min[0], uv_min[1]),
815 uv_max: glam::Vec2::new(uv_max[0], uv_max[1]),
816 color: [
817 draw.tint.x as f32,
818 draw.tint.y as f32,
819 draw.tint.z as f32,
820 draw.tint.w as f32,
821 ],
822 page,
823 z: draw.z,
824 blend: match draw.blend {
825 BlendMode::Add => crate::SpriteBlend::Additive,
826 BlendMode::Multiply => crate::SpriteBlend::Multiply,
827 _ => crate::SpriteBlend::Alpha,
828 },
829 light_mask: 1,
830 }
831}
832
833fn open_clip(
834 scene: &mut repose_core::Scene,
835 clip: Option<[f64; 4]>,
836 instance: [f64; 6],
837 view: &ViewTransform,
838) {
839 let Some([x, y, w, h]) = clip else {
840 return;
841 };
842 let world = compose(instance, view_affine(view));
843 let corners = [
844 apply(world, DVec2::new(x, y)),
845 apply(world, DVec2::new(x + w, y)),
846 apply(world, DVec2::new(x + w, y + h)),
847 apply(world, DVec2::new(x, y + h)),
848 ];
849 let (lo, hi) = corners.iter().fold(
850 (DVec2::splat(f64::INFINITY), DVec2::splat(f64::NEG_INFINITY)),
851 |(lo, hi), p| (lo.min(*p), hi.max(*p)),
852 );
853 scene.nodes.push(SceneNode::PushVectorClip {
854 mesh: clip_mesh(lo.x, lo.y, hi.x, hi.y),
855 op: repose_core::ClipOp::Intersect,
856 });
857}
858
859fn close_clip(scene: &mut repose_core::Scene, pushed: bool) {
860 if pushed {
861 scene.nodes.push(SceneNode::PopVectorClip);
862 }
863}
864
865pub fn actors_view_world(
867 library: Rc<RefCell<ActorLibrary>>,
868 draws: Rc<ActorDrawList>,
869 camera: Camera2d,
870 world_size: [f32; 2],
871 ctx: RenderContext,
872) -> View {
873 actors_view_mixed(library, draws, camera, world_size, ctx, None)
874}
875
876struct PickState {
880 sink: Rc<dyn Fn(PickEvent)>,
881 camera: Camera2d,
882 world_size: [f32; 2],
883 canvas_dp: Rc<RefCell<[f32; 2]>>,
884 press: HashMap<u64, [f32; 2]>,
885}
886
887impl PickState {
888 fn project(&self, position: repose_core::Vec2) -> ([f32; 2], glam::Vec2) {
889 let density = effective_density_scale().max(1e-6);
890 let dp = [position.x as f32 / density, position.y as f32 / density];
891 let canvas = *self.canvas_dp.borrow();
892 if std::env::var("NT_PICK_TRACE").is_ok() {
893 eprintln!(
894 "nt-actors: canvas_dp={canvas:?} world_size={:?}",
895 self.world_size
896 );
897 }
898 let fit = effective_fit(canvas, self.world_size, &self.camera);
899 let world = dp_to_world(dp, self.world_size, self.camera.effective_center(), fit);
900 (
906 [position.x as f32, position.y as f32],
907 glam::Vec2::new(world[0], world[1]),
908 )
909 }
910
911 fn is_contact(&self, event: &PointerEvent) -> bool {
912 matches!(event.kind, PointerKind::Touch | PointerKind::Pen)
913 }
914}
915
916pub fn actors_view_world_with_events(
920 library: Rc<RefCell<ActorLibrary>>,
921 draws: Rc<ActorDrawList>,
922 camera: Camera2d,
923 world_size: [f32; 2],
924 canvas_dp: [f32; 2],
925 ctx: RenderContext,
926 on_event: Rc<dyn Fn(PickEvent)>,
927) -> View {
928 actors_view_mixed_with_events(
929 library, draws, camera, world_size, canvas_dp, ctx, None, on_event,
930 )
931}
932
933pub fn actors_view_mixed_with_events(
936 library: Rc<RefCell<ActorLibrary>>,
937 draws: Rc<ActorDrawList>,
938 camera: Camera2d,
939 world_size: [f32; 2],
940 canvas_dp: [f32; 2],
941 ctx: RenderContext,
942 atlas: Option<Rc<RasterAtlas>>,
943 on_event: Rc<dyn Fn(PickEvent)>,
944) -> View {
945 let surface: Rc<RefCell<[f32; 2]>> =
946 Rc::new(RefCell::new([canvas_dp[0].max(1.0), canvas_dp[1].max(1.0)]));
947 let state: Rc<RefCell<PickState>> = Rc::new(RefCell::new(PickState {
948 sink: on_event,
949 camera,
950 world_size,
951 canvas_dp: surface.clone(),
952 press: HashMap::new(),
953 }));
954
955 let surface_view = actors_view_mixed(library, draws, camera, world_size, ctx, atlas);
956
957 let down = state.clone();
958 let up = state.clone();
959 let move_state = state.clone();
960 let leave = state.clone();
961 let sized = surface.clone();
962 let sized_paint = sized.clone();
963 let modifier = Modifier::new()
964 .fill_max_size()
965 .painter(
968 move |_scene: &mut repose_core::Scene, rect: repose_core::Rect, _alpha: f32| {
969 let mut cell = sized_paint.borrow_mut();
970 if cell[0] < 1.0 || cell[1] < 1.0 {
971 *cell = [rect.w.max(1.0), rect.h.max(1.0)];
972 }
973 },
974 )
975 .on_size_changed(move |size| {
976 *sized.borrow_mut() = [size.x.max(1.0), size.y.max(1.0)];
977 })
978 .on_pointer_down(move |event| {
979 let mut state = down.borrow_mut();
980 let (screen, world) = state.project(event.window_position);
981 let button = match event.event {
982 PointerEventKind::Down(button) => button,
983 _ => PointerButton::Primary,
984 };
985 state.press.insert(event.id.0, screen);
986 (state.sink)(PickEvent::Press {
987 world,
988 screen,
989 button,
990 });
991 if state.is_contact(&event) {
992 (state.sink)(PickEvent::TouchDown {
993 id: event.id.0,
994 screen,
995 });
996 }
997 })
998 .on_pointer_up(move |event| {
999 let mut state = up.borrow_mut();
1000 let (screen, world) = state.project(event.window_position);
1001 let button = match event.event {
1002 PointerEventKind::Up(button) => button,
1003 _ => PointerButton::Primary,
1004 };
1005 let origin = state.press.remove(&event.id.0);
1006 let density = effective_density_scale().max(1e-6);
1007 (state.sink)(PickEvent::Release {
1008 world,
1009 screen,
1010 button,
1011 });
1012 if state.is_contact(&event) {
1013 (state.sink)(PickEvent::TouchUp { id: event.id.0 });
1014 }
1015 if origin.is_some_and(|start| click_within_slop_dp(start, screen, density)) {
1016 (state.sink)(PickEvent::Click {
1017 world,
1018 screen,
1019 button,
1020 });
1021 }
1022 })
1023 .on_pointer_move(move |event| {
1024 let state = move_state.borrow();
1025 let (screen, world) = state.project(event.window_position);
1026 (state.sink)(PickEvent::Hover { world, screen });
1027 if state.is_contact(&event) {
1028 (state.sink)(PickEvent::TouchMove {
1029 id: event.id.0,
1030 screen,
1031 });
1032 }
1033 })
1034 .on_pointer_leave(move |event| {
1035 let mut state = leave.borrow_mut();
1036 if state.press.remove(&event.id.0).is_some() {
1037 (state.sink)(PickEvent::TouchUp { id: event.id.0 });
1038 }
1039 });
1040
1041 ZStack(Modifier::new().fill_max_size())
1044 .child(surface_view)
1045 .child(UiBox(modifier))
1046}
1047
1048#[cfg(test)]
1049mod tests {
1050 use super::*;
1051 use crate::{effective_fit, world_to_dp};
1052 use renamite_animation::{Animated, Frame, FrameRate};
1053 use renamite_model::{
1054 Color, Document, FillRule, Node, NodeKind, Parent, ShapeKind, StyleKind, StylePaint,
1055 };
1056 use renamite_render_bridge::SceneRenderer;
1057 use std::cell::RefCell;
1058 use std::rc::Rc;
1059
1060 fn rig(size: DVec2, color: Color) -> Document {
1061 let mut doc = Document::empty();
1062 {
1063 let comp = doc.main_composition_mut().unwrap();
1064 comp.size = (size.x as u32, size.y as u32);
1065 comp.rate = FrameRate { num: 12, den: 1 };
1066 comp.range = (Frame(0), Frame(1));
1067 }
1068 let root = doc.create_node(Node::new("rig", NodeKind::Group));
1069 doc.attach(root, Parent::Comp(doc.main), 0).unwrap();
1070 let shape = doc.create_node(Node::new(
1071 "box",
1072 NodeKind::Shape(ShapeKind::Rect {
1073 pos: Animated::new(DVec2::new(size.x * 0.5, size.y * 0.5)),
1074 size: Animated::new(size),
1075 rounded: Animated::new(0.0),
1076 }),
1077 ));
1078 doc.attach(shape, Parent::Node(root), 0).unwrap();
1079 let fill = doc.create_node(Node::new(
1080 "fill",
1081 NodeKind::Style(StyleKind::Fill {
1082 paint: StylePaint::Solid {
1083 color: Animated::new(color),
1084 },
1085 rule: FillRule::NonZero,
1086 }),
1087 ));
1088 doc.attach(fill, Parent::Node(root), 1).unwrap();
1089 doc
1090 }
1091
1092 fn view_affine(view: &ViewTransform) -> [f64; 6] {
1093 [
1094 view.scale,
1095 0.0,
1096 0.0,
1097 view.scale,
1098 view.offset.x,
1099 view.offset.y,
1100 ]
1101 }
1102
1103 fn close(a: DVec2, b: DVec2) -> bool {
1104 (a - b).length() < 1e-4
1105 }
1106
1107 #[test]
1110 fn camera_view_agrees_with_world_to_dp() {
1111 let canvas_dp = [640.0, 360.0];
1112 let world_size = [800.0, 600.0];
1113 for center in [
1114 DVec2::new(0.0, 0.0),
1115 DVec2::new(400.0, 300.0),
1116 DVec2::new(913.0, -47.0),
1117 ] {
1118 for zoom in [1.0f32, 2.5, 0.4] {
1119 let camera = Camera2d {
1120 center: center.as_vec2(),
1121 zoom,
1122 ..Camera2d::default()
1123 };
1124 let view = camera_view(&camera, canvas_dp, world_size);
1125 let affine = view_affine(&view);
1126 for world in [
1127 DVec2::new(0.0, 0.0),
1128 DVec2::new(400.0, 300.0),
1129 DVec2::new(799.0, 599.0),
1130 ] {
1131 let via_engine = world_to_dp(
1132 [world.x as f32, world.y as f32],
1133 world_size,
1134 camera.effective_center(),
1135 effective_fit(canvas_dp, world_size, &camera),
1136 );
1137 let via_actor = apply(affine, world);
1138 assert!(
1139 close(
1140 via_actor,
1141 DVec2::new(via_engine[0] as f64, via_engine[1] as f64)
1142 ),
1143 "center={center:?} zoom={zoom} world={world:?}: actor {via_actor:?} vs engine {via_engine:?}"
1144 );
1145 }
1146 }
1147 }
1148 }
1149
1150 #[test]
1151 fn compose_is_apply_a_then_b() {
1152 let a = [2.0, 0.0, 0.0, 2.0, 10.0, 20.0];
1153 let b = [1.0, 0.0, 0.0, 1.0, -4.0, 6.0];
1154 let p = DVec2::new(3.0, 7.0);
1155 assert!(close(apply(compose(a, b), p), apply(b, apply(a, p))));
1156 }
1157
1158 #[test]
1159 fn compose_is_associative() {
1160 let a = [2.0, 0.5, -0.5, 3.0, 10.0, 20.0];
1161 let b = [1.0, 0.25, 0.75, 1.0, -4.0, 6.0];
1162 let c = [0.5, 0.0, 0.0, 0.5, 100.0, -50.0];
1163 let left = compose(compose(a, b), c);
1164 let right = compose(a, compose(b, c));
1165 for p in [DVec2::ZERO, DVec2::new(1.0, 2.0), DVec2::new(-9.0, 4.5)] {
1166 assert!(close(apply(left, p), apply(right, p)));
1167 }
1168 }
1169
1170 #[test]
1172 fn instance_affine_places_the_anchor_on_world() {
1173 let size = DVec2::new(24.0, 32.0);
1174 for anchor in [
1175 DVec2::new(0.5, 0.5),
1176 DVec2::ZERO,
1177 DVec2::ONE,
1178 DVec2::new(0.25, 0.75),
1179 ] {
1180 let draw = ActorDraw {
1181 world: DVec2::new(120.0, -40.0),
1182 anchor,
1183 ..ActorDraw::default()
1184 };
1185 let affine = instance_affine(&draw, size);
1186 assert!(
1187 close(apply(affine, anchor * size), draw.world),
1188 "anchor {anchor:?}"
1189 );
1190 }
1191 }
1192
1193 #[test]
1194 fn instance_affine_rotation_and_flip() {
1195 let size = DVec2::new(10.0, 10.0);
1196
1197 let turned = ActorDraw {
1200 world: DVec2::ZERO,
1201 anchor: DVec2::splat(0.5),
1202 rotation: std::f64::consts::FRAC_PI_2,
1203 ..ActorDraw::default()
1204 };
1205 assert!(close(
1206 apply(instance_affine(&turned, size), DVec2::new(10.0, 5.0)),
1207 DVec2::new(0.0, 5.0)
1208 ));
1209
1210 let mirrored = ActorDraw {
1213 world: DVec2::splat(5.0),
1214 anchor: DVec2::splat(0.5),
1215 scale: DVec2::new(-1.0, 1.0),
1216 ..ActorDraw::default()
1217 };
1218 assert!(close(
1219 apply(instance_affine(&mirrored, size), DVec2::new(10.0, 5.0)),
1220 DVec2::new(0.0, 5.0)
1221 ));
1222 }
1223
1224 #[test]
1228 fn retarget_matches_baking_the_placement_into_the_scene() {
1229 let size = DVec2::new(24.0, 32.0);
1230 let doc = rig(size, Color::rgba(0.2, 0.6, 0.9, 1.0));
1231 let scene = renamite_model::evaluate(&doc, doc.main, 0.0);
1232 let view = ViewTransform {
1233 scale: 2.5,
1234 offset: DVec2::new(13.0, -7.0),
1235 };
1236
1237 for draw in [
1238 ActorDraw::default(),
1239 ActorDraw {
1240 world: DVec2::new(300.0, -125.0),
1241 ..ActorDraw::default()
1242 },
1243 ActorDraw {
1244 world: DVec2::new(-64.0, 41.0),
1245 rotation: 0.7,
1246 scale: DVec2::new(1.75, 0.5),
1247 anchor: DVec2::new(0.0, 1.0),
1248 ..ActorDraw::default()
1249 },
1250 ActorDraw {
1251 world: DVec2::new(10.0, 20.0),
1252 rotation: -2.1,
1253 scale: DVec2::new(-1.0, 2.0),
1254 anchor: DVec2::new(0.25, 0.75),
1255 ..ActorDraw::default()
1256 },
1257 ] {
1258 let instance = instance_affine(&draw, size);
1259
1260 let fast = SceneRenderer::new().prepare(&scene, &view);
1261 let fast = retarget_draws(&fast.draws, instance);
1262
1263 let mut baked = scene.clone();
1264 for item in &mut baked.items {
1265 item.path.apply_affine(kurbo::Affine::new(instance));
1266 }
1267 let slow = SceneRenderer::new().prepare(&baked, &view);
1268
1269 assert_eq!(fast.len(), slow.draws.len());
1270 for (fast, slow) in fast.iter().zip(&slow.draws) {
1271 let (
1272 PreparedDraw::Vector {
1273 mesh: fast_mesh,
1274 transform: fast_transform,
1275 ..
1276 },
1277 PreparedDraw::Vector {
1278 mesh: slow_mesh,
1279 transform: slow_transform,
1280 ..
1281 },
1282 ) = (fast, slow)
1283 else {
1284 panic!("expected vector draws");
1285 };
1286 let bounds = |mesh: &repose_core::VectorMeshData, t: &[f32; 6]| {
1289 mesh.vertices
1290 .iter()
1291 .map(|v| {
1292 DVec2::new(
1293 f64::from(t[0]) * f64::from(v.pos[0])
1294 + f64::from(t[2]) * f64::from(v.pos[1])
1295 + f64::from(t[4]),
1296 f64::from(t[1]) * f64::from(v.pos[0])
1297 + f64::from(t[3]) * f64::from(v.pos[1])
1298 + f64::from(t[5]),
1299 )
1300 })
1301 .fold(
1302 (DVec2::splat(f64::INFINITY), DVec2::splat(f64::NEG_INFINITY)),
1303 |(lo, hi), p| (lo.min(p), hi.max(p)),
1304 )
1305 };
1306 let (fast_bounds, slow_bounds) = (
1307 bounds(fast_mesh, fast_transform),
1308 bounds(slow_mesh, slow_transform),
1309 );
1310 assert!(
1311 close(fast_bounds.0, slow_bounds.0) && close(fast_bounds.1, slow_bounds.1),
1312 "placement {draw:?}\nfast {fast_bounds:?}\nslow {slow_bounds:?}"
1313 );
1314 }
1315 }
1316 }
1317
1318 #[test]
1319 fn tint_scene_scales_only_solid_fills() {
1320 let doc = rig(DVec2::new(4.0, 4.0), Color::rgba(0.5, 0.5, 0.5, 1.0));
1321 let scene = renamite_model::evaluate(&doc, doc.main, 0.0);
1322 let tinted = tint_scene(&scene, DVec4::new(0.5, 0.25, 1.0, 0.5));
1323 let ScenePaint::Solid(color) = tinted.items[0].paint else {
1324 panic!("expected solid");
1325 };
1326 assert!((color.r - 0.25).abs() < 1e-9, "r was {}", color.r);
1327 assert!((color.g - 0.125).abs() < 1e-9, "g was {}", color.g);
1328 assert!((color.b - 0.5).abs() < 1e-9, "b was {}", color.b);
1329 assert!((color.a - 0.5).abs() < 1e-9, "a was {}", color.a);
1330 }
1331
1332 #[test]
1333 fn library_round_trips_rigs_and_reports_ids() {
1334 let mut library = ActorLibrary::new();
1335 assert!(library.is_empty());
1336 let first = library
1337 .push_ren_str(crate::MINIMAL_REN)
1338 .expect("minimal document parses");
1339 let second = library
1340 .push_ren_str(crate::MINIMAL_REN)
1341 .expect("minimal document parses");
1342 assert_eq!(library.len(), 2);
1343 assert_eq!(first, ActorId(0));
1344 assert_eq!(second, ActorId(1));
1345 assert!(library.host(second).is_some());
1346 assert!(library.host(ActorId(2)).is_none());
1347 }
1348
1349 #[test]
1350 fn draw_list_sorts_by_z() {
1351 let mut list = ActorDrawList::new();
1352 for z in [3.0, -1.0, 2.0, 0.0] {
1353 list.push(ActorDraw {
1354 z,
1355 ..ActorDraw::default()
1356 });
1357 }
1358 let mut sorted = list.as_slice().to_vec();
1359 sorted.sort_by(|a, b| a.z.partial_cmp(&b.z).unwrap());
1360 assert_eq!(
1361 sorted.iter().map(|d| d.z).collect::<Vec<_>>(),
1362 vec![-1.0, 0.0, 2.0, 3.0]
1363 );
1364 }
1365
1366 #[test]
1367 fn mixed_media_share_one_z_order() {
1368 let rig = |z: f32| ActorDraw {
1369 item: ActorItem::Rig {
1370 actor: ActorId(0),
1371 frame: 0.0,
1372 },
1373 z,
1374 ..ActorDraw::default()
1375 };
1376 let raster = |z: f32| ActorDraw {
1377 item: ActorItem::Raster {
1378 page: 0,
1379 uv_min: [0.0, 0.0],
1380 uv_max: [1.0, 1.0],
1381 size: [8.0, 8.0],
1382 },
1383 z,
1384 ..ActorDraw::default()
1385 };
1386 let rect = |z: f32| ActorDraw {
1387 item: ActorItem::Rect { half: [4.0, 4.0] },
1388 z,
1389 ..ActorDraw::default()
1390 };
1391 let mut draws = [rig(1.0), raster(0.0), rect(2.0), rig(0.5)];
1392 draws.sort_by(compare_draws);
1393 let media: Vec<Medium> = draws.iter().map(|d| medium_key(d.item)).collect();
1394 assert_eq!(
1395 media,
1396 vec![Medium::Raster, Medium::Rig, Medium::Rig, Medium::Rect],
1397 "z is the major key across media: raster@0, rig@0.5, rig@1, rect@2"
1398 );
1399 }
1400
1401 #[test]
1402 fn world_view_constructs_without_a_backend() {
1403 let mut library = ActorLibrary::new();
1404 library.push_ren_str(crate::MINIMAL_REN).unwrap();
1405 let mut draws = ActorDrawList::new();
1406 draws.push(ActorDraw {
1407 world: DVec2::new(12.0, 34.0),
1408 ..ActorDraw::default()
1409 });
1410 let ctx = RenderContext::new();
1411 let _view = actors_view_world(
1412 Rc::new(RefCell::new(library)),
1413 Rc::new(draws),
1414 Camera2d::default(),
1415 [800.0, 600.0],
1416 ctx,
1417 );
1418 }
1419
1420 #[test]
1424 fn project_emits_physical_px_and_unprojects_from_dp() {
1425 let canvas_dp = [800.0, 600.0];
1426 let world_size = [320.0, 240.0];
1427 let camera = Camera2d::default();
1428 let density = effective_density_scale().max(1e-6);
1429 let pick = PickState {
1430 sink: Rc::new(|_| {}),
1431 camera,
1432 world_size,
1433 canvas_dp: Rc::new(RefCell::new(canvas_dp)),
1434 press: HashMap::new(),
1435 };
1436 let physical = repose_core::Vec2 { x: 640.0, y: 480.0 };
1437 let (screen, world) = pick.project(physical);
1438 assert_eq!(screen, [640.0, 480.0], "screen must stay physical px");
1439 let dp = [640.0 / density, 480.0 / density];
1440 let fit = effective_fit(canvas_dp, world_size, &camera);
1441 let want = dp_to_world(dp, world_size, camera.effective_center(), fit);
1442 assert!(
1443 (world.x - want[0]).abs() < 1e-3 && (world.y - want[1]).abs() < 1e-3,
1444 "world must unproject through the dp screen, got {world:?} want {want:?}"
1445 );
1446 }
1447}