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