""" Unit tests for NaraMotion v0.26.0 High-Performance Engine Upgrades: - Structure of Arrays (SoA) and Path Arena Allocator (NativeGeoLayerSoA) - Direct Native WKB Batch Ingestion (Bypassing Python / Shapely) - Flat Uniform Grid Spatial Indexing in C++ (UniformGridIndex) - Dual-Buffer Polygon Render Pipeline (Batch Fills + Batch Strokes) - Local Centroid Spring Pop-In Animation (Scale = Easing::back_out(t)) - BBox Overdraw & Viewport Culling - High-Speed Retained GeoLayer Integration """ import os import pytest import numpy as np import naramotion as nm from naramotion import ( GeoCanvas, NativeGeoLayerSoA, UniformGridIndex, RetainedGeoLayer, Image, Context, GeoTransform, Format, VideoWriter, ) try: import shapely.geometry as sg import shapely _HAS_SHAPELY = True except ImportError: _HAS_SHAPELY = False def test_soa_lifecycle_and_polygon_addition(): """Verify NativeGeoLayerSoA memory allocation, polygon addition, and SoA arrays.""" layer = NativeGeoLayerSoA(capacity=100) assert len(layer) == 0 # Add a rectangle polygon coords = np.array([[10, 10], [90, 10], [90, 90], [10, 90], [10, 10]], dtype=np.float64) rings = [0, 5] idx = layer.add_polygon(coords, rings) assert idx == 0 assert len(layer) == 1 assert layer.count == 1 # Add second polygon coords2 = np.array([[100, 100], [200, 100], [200, 200], [100, 200], [100, 100]], dtype=np.float64) idx2 = layer.add_polygon(coords2, rings) assert idx2 == 1 assert len(layer) == 2 def test_soa_direct_wkb_batch_ingestion(): """Verify parsing WKB buffers directly in C++ into BLPathCore with holes.""" if not _HAS_SHAPELY: pytest.skip("Shapely required for WKB generation in test") # Outer box with a square hole cut out poly_with_hole = sg.Polygon( [(0, 0), (100, 0), (100, 100), (0, 100), (0, 0)], [[(25, 25), (75, 25), (75, 75), (25, 75), (25, 25)]] ) wkb1 = shapely.to_wkb(poly_with_hole) # MultiPolygon mpoly = sg.MultiPolygon([ sg.Polygon([(110, 0), (150, 0), (150, 40), (110, 40)]), sg.Polygon([(160, 0), (200, 0), (200, 40), (160, 40)]), ]) wkb2 = shapely.to_wkb(mpoly) layer = NativeGeoLayerSoA(capacity=50) parsed_count = layer.from_wkb_batch([wkb1, wkb2]) assert parsed_count == 2 assert len(layer) == 2 # Verify rendering to context succeeds img = Image(300, 200, Format.PRGB32) ctx = Context(img) res = layer.render_dual_buffer(ctx, fill_color="#38BDF8", stroke_color="#FFFFFF", stroke_width=1.5) ctx.end() assert res == 0 def test_soa_propagation_and_culling(): """Verify shockwave calculation and vectorized BBox viewport culling.""" layer = NativeGeoLayerSoA(capacity=10) # Feature A: near (0, 0) layer.add_polygon(np.array([[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]], dtype=np.float64), [0, 5]) # Feature B: far (100, 100) layer.add_polygon(np.array([[100, 100], [110, 100], [110, 110], [100, 110], [100, 100]], dtype=np.float64), [0, 5]) # Propagation from (0, 0) res = layer.compute_propagation(epicenters=[(0.0, 0.0)], duration_frames=100.0, speed=1.0) assert res == 0 # Viewport enclosing only Feature A: Feature B should be culled vis_count = layer.cull_and_update(viewport_bbox=(-5, -5, 50, 50), current_frame=10.0) assert vis_count == 1 # Viewport enclosing both at frame 0 (Feature B wave hasn't arrived) vis_early = layer.cull_and_update(viewport_bbox=(-5, -5, 200, 200), current_frame=5.0) assert vis_early == 1 # Viewport enclosing both at frame 90 (both waves arrived) vis_late = layer.cull_and_update(viewport_bbox=(-5, -5, 200, 200), current_frame=90.0) assert vis_late == 2 def test_soa_dual_buffer_and_spring_popin_rendering(): """Verify C++ Dual-Buffer batch pass and Centroid Spring Pop-In (back_out) scaling.""" layer = NativeGeoLayerSoA(capacity=10) coords = np.array([[50, 50], [150, 50], [150, 150], [50, 150], [50, 50]], dtype=np.float64) layer.add_polygon(coords, [0, 5]) layer.compute_propagation(epicenters=[(100.0, 100.0)], duration_frames=60.0) img = Image(300, 300, Format.PRGB32) ctx = Context(img) # 1. Dual-Buffer Batch Render res1 = layer.render_dual_buffer(ctx, fill_color="#00FFCC", stroke_color="#000000", stroke_width=2.0) assert res1 == 0 # 2. Centroid Spring Pop-In at mid-frame (elastic overshoot) res2 = layer.render_popin(ctx, current_frame=30.0, fill_color="#FF0055", stroke_color="#FFFFFF", stroke_width=1.0) assert res2 == 0 # 3. Centroid Spring Pop-In after completion (scale = 1.0) res3 = layer.render_popin(ctx, current_frame=70.0, fill_color="#FF0055", stroke_color="#FFFFFF", stroke_width=1.0) assert res3 == 0 ctx.end() def test_uniform_grid_spatial_index(): """Verify flat uniform grid spatial queries in C++.""" # Bounds: [0, 0, 1000, 1000] with 10x10 cells grid = UniformGridIndex(bounds=(0.0, 0.0, 1000.0, 1000.0), grid_w=10, grid_h=10) bboxes = np.array([ [10.0, 10.0, 50.0, 50.0], # In cell (0, 0) [510.0, 510.0, 550.0, 550.0], # In cell (5, 5) [910.0, 910.0, 950.0, 950.0], # In cell (9, 9) ], dtype=np.float64) built = grid.build_from_bboxes(bboxes) assert built == 0 # Query bottom-left region q1 = grid.query_bbox(0.0, 0.0, 100.0, 100.0) assert 0 in q1 assert 1 not in q1 assert 2 not in q1 # Query center region q2 = grid.query_bbox(500.0, 500.0, 600.0, 600.0) assert 1 in q2 assert 0 not in q2 assert 2 not in q2 # Query entire area q_all = grid.query_bbox(0.0, 0.0, 1000.0, 1000.0) assert len(q_all) == 3 def test_geocanvas_add_polygon_layer(): """Verify GeoCanvas integration with NativeGeoLayerSoA and RetainedGeoLayer.""" canvas = GeoCanvas(400, 300) # 1. Using NativeGeoLayerSoA layer_soa = NativeGeoLayerSoA(capacity=10) coords = np.array([[50, 50], [150, 50], [150, 150], [50, 150], [50, 50]], dtype=np.float64) layer_soa.add_polygon(coords, [0, 5]) canvas.add_polygon_layer(layer_soa, fill_color="#00E5FF", stroke_color="#FFFFFF", stroke_width=2.0) # 2. Using RetainedGeoLayer with popin and frame if _HAS_SHAPELY: poly = sg.Polygon([(200, 50), (280, 50), (280, 130), (200, 130)]) retained = canvas.create_retained_layer([poly], fill_color="#FF007F") canvas.add_polygon_layer(retained, popin=True, frame=10.0) img = canvas.render() assert img.width == 400 assert img.height == 300 def test_video_writer_buffer_pipe_streaming(): """Verify VideoWriter zero-copy buffer pipe access.""" img = Image(320, 240, Format.PRGB32) with Context(img) as ctx: ctx.fill_all("#111827") ctx.fill_circle(160, 120, 40, "#00E5FF") # Direct memoryview check buf = img.buffer assert isinstance(buf, memoryview) assert len(buf) == 320 * 240 * 4