#include #include #include #include #include #include #include #include #include #ifdef _WIN32 #define BL_GEO_EXPORT __declspec(dllexport) #include #include #else #define BL_GEO_EXPORT __attribute__((visibility("default"))) #endif // Constants static const double PI = 3.14159265358979323846; static const double DEG_TO_RAD = PI / 180.0; static const double RAD_TO_DEG = 180.0 / PI; static const double WGS84_A = 6378137.0; // Semi-major axis static const double WGS84_F = 1.0 / 298.257223563; // Flattening static const double WGS84_E2 = 2.0 * WGS84_F - WGS84_F * WGS84_F; // First eccentricity squared static const double WGS84_E = 0.08181919084262149; // First eccentricity // Blend2D C Types typedef uint32_t BLResult; struct BLPoint { double x; double y; }; struct BLMatrix2D { double m00; double m01; double m10; double m11; double m20; double m21; }; struct BLPathCore { uint64_t a; uint64_t b; }; struct BLContextCore { void* impl; }; extern "C" { BLResult bl_path_init(BLPathCore* self); BLResult bl_path_reset(BLPathCore* self); BLResult bl_path_destroy(BLPathCore* self); BLResult bl_path_move_to(BLPathCore* self, double x0, double y0); BLResult bl_path_line_to(BLPathCore* self, double x1, double y1); BLResult bl_path_close(BLPathCore* self); BLResult bl_context_fill_geometry_rgba32(BLContextCore* ctx, uint32_t type, const void* data, uint32_t rgba32); BLResult bl_context_stroke_geometry_rgba32(BLContextCore* ctx, uint32_t type, const void* data, uint32_t rgba32); BLResult bl_context_set_stroke_width(BLContextCore* ctx, double width); BLResult bl_context_set_fill_rule(BLContextCore* ctx, uint32_t fill_rule); BLResult bl_context_apply_transform_op(BLContextCore* ctx, uint32_t opType, const void* opData); } // Geospatial Viewport Transformation struct GeoTransform { double minx; double miny; double maxx; double maxy; double canvas_w; double canvas_h; double offset_x; double offset_y; double scale; int flip_y; // 1: standard GIS coordinate system (latitude increases upwards) }; inline void transform_pt(const GeoTransform* gt, double gx, double gy, double& sx, double& sy) { if (!gt || gt->scale == 0.0) { sx = gx; sy = gy; return; } sx = gt->offset_x + (gx - gt->minx) * gt->scale; if (gt->flip_y) { sy = gt->canvas_h - (gt->offset_y + (gy - gt->miny) * gt->scale); } else { sy = gt->offset_y + (gy - gt->miny) * gt->scale; } } // Byte-swap helpers for Big-Endian integers in Shapefile headers inline int32_t swap_be32(int32_t val) { uint32_t u = (uint32_t)val; return (int32_t)(((u >> 24) & 0x000000ff) | ((u >> 8) & 0x0000ff00) | ((u << 8) & 0x00ff0000) | ((u << 24) & 0xff000000)); } // ============================================================================ // ESRI Shapefile C-ABI Structs // ============================================================================ struct ShpHeader { int32_t file_code; // 9994 int32_t file_length; // in 16-bit words int32_t version; // 1000 int32_t shape_type; // 1=Point, 3=PolyLine, 5=Polygon, 8=MultiPoint double minx, miny, maxx, maxy; double zmin, zmax, mmin, mmax; int32_t record_count; }; struct ShpParsedData { int32_t shape_type; int32_t num_records; int64_t total_parts; int64_t total_points; double* coords; // [total_points * 2] (x, y) int32_t* parts; // [total_parts] int32_t* record_parts; // [num_records + 1] (index into parts) int64_t* record_points; // [num_records + 1] (index into points) double* bboxes; // [num_records * 4] (minx, miny, maxx, maxy) }; extern "C" { // ============================================================================ // Projection Engine Mathematical Implementations // ============================================================================ BL_GEO_EXPORT int bl_geo_proj_3857_forward(const double* in_coords, double* out_coords, int64_t num_points) { if (!in_coords || !out_coords || num_points <= 0) return -1; for (int64_t i = 0; i < num_points; ++i) { double lon = in_coords[i * 2]; double lat = in_coords[i * 2 + 1]; if (lat > 85.0511287798) lat = 85.0511287798; else if (lat < -85.0511287798) lat = -85.0511287798; double x = lon * (WGS84_A * DEG_TO_RAD); double lat_rad = lat * DEG_TO_RAD; double y = WGS84_A * std::log(std::tan(PI * 0.25 + lat_rad * 0.5)); out_coords[i * 2] = x; out_coords[i * 2 + 1] = y; } return 0; } BL_GEO_EXPORT int bl_geo_proj_3857_inverse(const double* in_coords, double* out_coords, int64_t num_points) { if (!in_coords || !out_coords || num_points <= 0) return -1; for (int64_t i = 0; i < num_points; ++i) { double x = in_coords[i * 2]; double y = in_coords[i * 2 + 1]; double lon = (x / WGS84_A) * RAD_TO_DEG; double lat = (2.0 * std::atan(std::exp(y / WGS84_A)) - PI * 0.5) * RAD_TO_DEG; out_coords[i * 2] = lon; out_coords[i * 2 + 1] = lat; } return 0; } static inline double lcc_m(double phi, double e) { double s = std::sin(phi); return std::cos(phi) / std::sqrt(1.0 - e * e * s * s); } static inline double lcc_t(double phi, double e) { double s = e * std::sin(phi); double con = std::pow((1.0 - s) / (1.0 + s), e * 0.5); return std::tan(PI * 0.25 - phi * 0.5) / con; } BL_GEO_EXPORT int bl_geo_proj_lcc_forward( double phi1_deg, double phi2_deg, double phi0_deg, double lam0_deg, double x0, double y0, const double* in_coords, double* out_coords, int64_t num_points ) { if (!in_coords || !out_coords || num_points <= 0) return -1; double phi1 = phi1_deg * DEG_TO_RAD; double phi2 = phi2_deg * DEG_TO_RAD; double phi0 = phi0_deg * DEG_TO_RAD; double lam0 = lam0_deg * DEG_TO_RAD; double e = WGS84_E; double m1 = lcc_m(phi1, e); double m2 = lcc_m(phi2, e); double t1 = lcc_t(phi1, e); double t2 = lcc_t(phi2, e); double t0 = lcc_t(phi0, e); double n; if (std::abs(phi1 - phi2) > 1e-10) { n = (std::log(m1) - std::log(m2)) / (std::log(t1) - std::log(t2)); } else { n = std::sin(phi1); } double F = m1 / (n * std::pow(t1, n)); double rho0 = WGS84_A * F * std::pow(t0, n); for (int64_t i = 0; i < num_points; ++i) { double lon = in_coords[i * 2] * DEG_TO_RAD; double lat = in_coords[i * 2 + 1] * DEG_TO_RAD; if (std::abs(lat) > (PI * 0.5 - 1e-6)) { lat = (lat > 0 ? 1 : -1) * (PI * 0.5 - 1e-6); } double t = lcc_t(lat, e); double rho = WGS84_A * F * std::pow(t, n); double theta = n * (lon - lam0); out_coords[i * 2] = x0 + rho * std::sin(theta); out_coords[i * 2 + 1] = y0 + rho0 - rho * std::cos(theta); } return 0; } BL_GEO_EXPORT int bl_geo_proj_albers_forward( double phi1_deg, double phi2_deg, double phi0_deg, double lam0_deg, double x0, double y0, const double* in_coords, double* out_coords, int64_t num_points ) { if (!in_coords || !out_coords || num_points <= 0) return -1; double phi1 = phi1_deg * DEG_TO_RAD; double phi2 = phi2_deg * DEG_TO_RAD; double phi0 = phi0_deg * DEG_TO_RAD; double lam0 = lam0_deg * DEG_TO_RAD; double n = 0.5 * (std::sin(phi1) + std::sin(phi2)); double C = std::cos(phi1) * std::cos(phi1) + 2.0 * n * std::sin(phi1); double rho0 = (WGS84_A / n) * std::sqrt(std::max(0.0, C - 2.0 * n * std::sin(phi0))); for (int64_t i = 0; i < num_points; ++i) { double lon = in_coords[i * 2] * DEG_TO_RAD; double lat = in_coords[i * 2 + 1] * DEG_TO_RAD; double theta = n * (lon - lam0); double q = C - 2.0 * n * std::sin(lat); double rho = (WGS84_A / n) * std::sqrt(std::max(0.0, q)); out_coords[i * 2] = x0 + rho * std::sin(theta); out_coords[i * 2 + 1] = y0 + rho0 - rho * std::cos(theta); } return 0; } // ============================================================================ // Viewport & Geometry Builders // ============================================================================ BL_GEO_EXPORT void bl_geo_init_transform( GeoTransform* gt, double minx, double miny, double maxx, double maxy, double canvas_w, double canvas_h, double padding_ratio, int preserve_aspect_ratio, int flip_y ) { if (!gt) return; gt->minx = minx; gt->miny = miny; gt->maxx = maxx; gt->maxy = maxy; gt->canvas_w = canvas_w; gt->canvas_h = canvas_h; gt->flip_y = flip_y; double geo_w = maxx - minx; double geo_h = maxy - miny; if (geo_w <= 0.0) geo_w = 1e-6; if (geo_h <= 0.0) geo_h = 1e-6; double pad_x = canvas_w * padding_ratio; double pad_y = canvas_h * padding_ratio; double avail_w = canvas_w - 2.0 * pad_x; double avail_h = canvas_h - 2.0 * pad_y; double sx = avail_w / geo_w; double sy = avail_h / geo_h; if (preserve_aspect_ratio) { gt->scale = (sx < sy) ? sx : sy; double used_w = geo_w * gt->scale; double used_h = geo_h * gt->scale; gt->offset_x = pad_x + (avail_w - used_w) * 0.5; gt->offset_y = pad_y + (avail_h - used_h) * 0.5; } else { gt->scale = sx; gt->offset_x = pad_x; gt->offset_y = pad_y; } } BL_GEO_EXPORT int bl_geo_build_polygon( BLPathCore* path, const double* coords, const int64_t* ring_offsets, int64_t num_rings, const GeoTransform* gt ) { if (!path || !coords || !ring_offsets || num_rings <= 0) return -1; for (int64_t r = 0; r < num_rings; ++r) { int64_t start_idx = ring_offsets[r]; int64_t end_idx = ring_offsets[r + 1]; int64_t pt_count = end_idx - start_idx; if (pt_count < 3) continue; double sx, sy; transform_pt(gt, coords[start_idx * 2], coords[start_idx * 2 + 1], sx, sy); bl_path_move_to(path, sx, sy); for (int64_t i = start_idx + 1; i < end_idx; ++i) { transform_pt(gt, coords[i * 2], coords[i * 2 + 1], sx, sy); bl_path_line_to(path, sx, sy); } bl_path_close(path); } return 0; } BL_GEO_EXPORT int bl_geo_build_linestring( BLPathCore* path, const double* coords, int64_t num_points, const GeoTransform* gt ) { if (!path || !coords || num_points < 2) return -1; double sx, sy; transform_pt(gt, coords[0], coords[1], sx, sy); bl_path_move_to(path, sx, sy); for (int64_t i = 1; i < num_points; ++i) { transform_pt(gt, coords[i * 2], coords[i * 2 + 1], sx, sy); bl_path_line_to(path, sx, sy); } return 0; } BL_GEO_EXPORT int bl_geo_build_multilinestring( BLPathCore* path, const double* coords, const int64_t* line_offsets, int64_t num_lines, const GeoTransform* gt ) { if (!path || !coords || !line_offsets || num_lines <= 0) return -1; for (int64_t l = 0; l < num_lines; ++l) { int64_t start_idx = line_offsets[l]; int64_t end_idx = line_offsets[l + 1]; int64_t pt_count = end_idx - start_idx; if (pt_count < 2) continue; double sx, sy; transform_pt(gt, coords[start_idx * 2], coords[start_idx * 2 + 1], sx, sy); bl_path_move_to(path, sx, sy); for (int64_t i = start_idx + 1; i < end_idx; ++i) { transform_pt(gt, coords[i * 2], coords[i * 2 + 1], sx, sy); bl_path_line_to(path, sx, sy); } } return 0; } BL_GEO_EXPORT int bl_geo_transform_coords( const double* in_coords, double* out_coords, int64_t num_points, const GeoTransform* gt ) { if (!in_coords || !out_coords || num_points <= 0) return -1; for (int64_t i = 0; i < num_points; ++i) { double sx, sy; transform_pt(gt, in_coords[i * 2], in_coords[i * 2 + 1], sx, sy); out_coords[i * 2] = sx; out_coords[i * 2 + 1] = sy; } return 0; } // ============================================================================ // High-Speed Binary Shapefile (.shp) Parser (C++) // ============================================================================ BL_GEO_EXPORT int bl_geo_read_shp_header(const char* filepath, ShpHeader* hdr) { if (!filepath || !hdr) return -1; FILE* fp = fopen(filepath, "rb"); if (!fp) return -2; uint8_t buffer[100]; if (fread(buffer, 1, 100, fp) < 100) { fclose(fp); return -3; } int32_t file_code = swap_be32(*(int32_t*)(buffer + 0)); if (file_code != 9994) { fclose(fp); return -4; } hdr->file_code = file_code; hdr->file_length = swap_be32(*(int32_t*)(buffer + 24)) * 2; hdr->version = *(int32_t*)(buffer + 28); hdr->shape_type = *(int32_t*)(buffer + 32); hdr->minx = *(double*)(buffer + 36); hdr->miny = *(double*)(buffer + 44); hdr->maxx = *(double*)(buffer + 52); hdr->maxy = *(double*)(buffer + 60); hdr->zmin = *(double*)(buffer + 68); hdr->zmax = *(double*)(buffer + 76); hdr->mmin = *(double*)(buffer + 84); hdr->mmax = *(double*)(buffer + 92); int count = 0; while (true) { int32_t rec_hdr[2]; if (fread(rec_hdr, 4, 2, fp) < 2) break; int32_t rec_len = swap_be32(rec_hdr[1]) * 2; if (rec_len <= 0) continue; fseek(fp, rec_len, SEEK_CUR); count++; } hdr->record_count = count; fclose(fp); return 0; } BL_GEO_EXPORT ShpParsedData* bl_geo_parse_shp(const char* filepath) { if (!filepath) return nullptr; FILE* fp = fopen(filepath, "rb"); if (!fp) return nullptr; uint8_t hdr_buf[100]; if (fread(hdr_buf, 1, 100, fp) < 100) { fclose(fp); return nullptr; } int32_t shape_type = *(int32_t*)(hdr_buf + 32); std::vector all_coords; std::vector all_parts; std::vector rec_parts; std::vector rec_points; std::vector all_bboxes; rec_parts.push_back(0); rec_points.push_back(0); while (true) { int32_t rec_hdr[2]; if (fread(rec_hdr, 4, 2, fp) < 2) break; int32_t rec_num = swap_be32(rec_hdr[0]); int32_t rec_len = swap_be32(rec_hdr[1]) * 2; if (rec_len <= 0) continue; std::vector rec_data(rec_len); if (fread(rec_data.data(), 1, rec_len, fp) < (size_t)rec_len) break; int32_t rec_shape = *(int32_t*)(rec_data.data()); if (rec_shape == 0) { all_bboxes.push_back(0); all_bboxes.push_back(0); all_bboxes.push_back(0); all_bboxes.push_back(0); rec_parts.push_back((int32_t)all_parts.size()); rec_points.push_back((int64_t)(all_coords.size() / 2)); continue; } if (rec_shape == 1 || rec_shape == 11 || rec_shape == 21) { double px = *(double*)(rec_data.data() + 4); double py = *(double*)(rec_data.data() + 12); all_bboxes.push_back(px); all_bboxes.push_back(py); all_bboxes.push_back(px); all_bboxes.push_back(py); all_coords.push_back(px); all_coords.push_back(py); all_parts.push_back((int32_t)(all_coords.size() / 2 - 1)); } else if (rec_shape == 3 || rec_shape == 5 || rec_shape == 13 || rec_shape == 15) { double r_minx = *(double*)(rec_data.data() + 4); double r_miny = *(double*)(rec_data.data() + 12); double r_maxx = *(double*)(rec_data.data() + 20); double r_maxy = *(double*)(rec_data.data() + 28); all_bboxes.push_back(r_minx); all_bboxes.push_back(r_miny); all_bboxes.push_back(r_maxx); all_bboxes.push_back(r_maxy); int32_t num_parts = *(int32_t*)(rec_data.data() + 36); int32_t num_points = *(int32_t*)(rec_data.data() + 40); const int32_t* parts_ptr = (const int32_t*)(rec_data.data() + 44); const double* pts_ptr = (const double*)(rec_data.data() + 44 + num_parts * 4); int64_t base_pt = all_coords.size() / 2; for (int p = 0; p < num_parts; ++p) { all_parts.push_back((int32_t)(base_pt + parts_ptr[p])); } for (int i = 0; i < num_points * 2; ++i) { all_coords.push_back(pts_ptr[i]); } } rec_parts.push_back((int32_t)all_parts.size()); rec_points.push_back((int64_t)(all_coords.size() / 2)); } fclose(fp); auto* result = new ShpParsedData(); result->shape_type = shape_type; result->num_records = (int32_t)(rec_points.size() - 1); result->total_parts = (int64_t)all_parts.size(); result->total_points = (int64_t)(all_coords.size() / 2); result->coords = (double*)malloc(all_coords.size() * sizeof(double)); memcpy(result->coords, all_coords.data(), all_coords.size() * sizeof(double)); result->parts = (int32_t*)malloc(all_parts.size() * sizeof(int32_t)); memcpy(result->parts, all_parts.data(), all_parts.size() * sizeof(int32_t)); result->record_parts = (int32_t*)malloc(rec_parts.size() * sizeof(int32_t)); memcpy(result->record_parts, rec_parts.data(), rec_parts.size() * sizeof(int32_t)); result->record_points = (int64_t*)malloc(rec_points.size() * sizeof(int64_t)); memcpy(result->record_points, rec_points.data(), rec_points.size() * sizeof(int64_t)); result->bboxes = (double*)malloc(all_bboxes.size() * sizeof(double)); memcpy(result->bboxes, all_bboxes.data(), all_bboxes.size() * sizeof(double)); return result; } BL_GEO_EXPORT void bl_geo_free_shp_data(ShpParsedData* data) { if (!data) return; if (data->coords) free(data->coords); if (data->parts) free(data->parts); if (data->record_parts) free(data->record_parts); if (data->record_points) free(data->record_points); if (data->bboxes) free(data->bboxes); delete data; } // ============================================================================ // High-Speed HTTP/HTTPS Downloader (Native WinHTTP C-API) // ============================================================================ BL_GEO_EXPORT int bl_geo_http_download_file(const char* url_utf8, const char* dest_path_utf8) { #ifdef _WIN32 if (!url_utf8 || !dest_path_utf8) return -1; // Convert UTF-8 URL to WCHAR int url_wlen = MultiByteToWideChar(CP_UTF8, 0, url_utf8, -1, NULL, 0); if (url_wlen <= 0) return -2; std::vector wurl(url_wlen); MultiByteToWideChar(CP_UTF8, 0, url_utf8, -1, wurl.data(), url_wlen); URL_COMPONENTS urlComp; ZeroMemory(&urlComp, sizeof(urlComp)); urlComp.dwStructSize = sizeof(urlComp); urlComp.dwSchemeLength = (DWORD)-1; urlComp.dwHostNameLength = (DWORD)-1; urlComp.dwUrlPathLength = (DWORD)-1; urlComp.dwExtraInfoLength = (DWORD)-1; if (!WinHttpCrackUrl(wurl.data(), (DWORD)wcslen(wurl.data()), 0, &urlComp)) { return -3; } std::wstring hostName(urlComp.lpszHostName, urlComp.dwHostNameLength); std::wstring urlPath(urlComp.lpszUrlPath, urlComp.dwUrlPathLength + urlComp.dwExtraInfoLength); HINTERNET hSession = WinHttpOpen(L"NaraMotion-GeoEngine/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); if (!hSession) return -4; HINTERNET hConnect = WinHttpConnect(hSession, hostName.c_str(), urlComp.nPort, 0); if (!hConnect) { WinHttpCloseHandle(hSession); return -5; } DWORD dwFlags = (urlComp.nScheme == INTERNET_SCHEME_HTTPS) ? WINHTTP_FLAG_SECURE : 0; HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"GET", urlPath.c_str(), NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, dwFlags); if (!hRequest) { WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return -6; } // Set auto-redirect policy DWORD redirectPolicy = WINHTTP_OPTION_REDIRECT_POLICY_ALWAYS; WinHttpSetOption(hRequest, WINHTTP_OPTION_REDIRECT_POLICY, &redirectPolicy, sizeof(redirectPolicy)); if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0) || !WinHttpReceiveResponse(hRequest, NULL)) { WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return -7; } FILE* fp = fopen(dest_path_utf8, "wb"); if (!fp) { WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return -8; } DWORD dwSize = 0; std::vector buffer(65536); while (WinHttpQueryDataAvailable(hRequest, &dwSize) && dwSize > 0) { DWORD dwDownloaded = 0; DWORD toRead = (dwSize < (DWORD)buffer.size()) ? dwSize : (DWORD)buffer.size(); if (!WinHttpReadData(hRequest, buffer.data(), toRead, &dwDownloaded) || dwDownloaded == 0) { break; } fwrite(buffer.data(), 1, dwDownloaded, fp); } fclose(fp); WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return 0; #else return -99; // Fallback to python on non-windows #endif } // ============================================================================ // Multi-Threaded WKB / GeoParquet Geometry Decoder Engine // ============================================================================ struct WkbParsedGeometry { int32_t geom_type; // 1=Point, 2=LineString, 3=Polygon, 4=MultiPoint, 5=MultiLineString, 6=MultiPolygon int32_t num_parts; int64_t num_points; std::vector coords; std::vector part_offsets; }; static void _parse_wkb_single(const uint8_t* data, size_t size, WkbParsedGeometry& out) { if (!data || size < 5) return; uint8_t byte_order = data[0]; bool is_little_endian = (byte_order == 1); auto read_u32 = [is_little_endian](const uint8_t* ptr) -> uint32_t { uint32_t val; memcpy(&val, ptr, 4); if (!is_little_endian) { val = ((val >> 24) & 0xff) | ((val >> 8) & 0xff00) | ((val << 8) & 0xff0000) | ((val << 24) & 0xff000000); } return val; }; auto read_double = [is_little_endian](const uint8_t* ptr) -> double { if (is_little_endian) { double val; memcpy(&val, ptr, 8); return val; } else { uint64_t val; memcpy(&val, ptr, 8); val = ((val >> 56) & 0x00000000000000ffULL) | ((val >> 40) & 0x000000000000ff00ULL) | ((val >> 24) & 0x0000000000ff0000ULL) | ((val >> 8) & 0x00000000ff000000ULL) | ((val << 8) & 0x000000ff00000000ULL) | ((val << 24) & 0x0000ff0000000000ULL) | ((val << 40) & 0x00ff000000000000ULL) | ((val << 56) & 0xff00000000000000ULL); double res; memcpy(&res, &val, 8); return res; } }; uint32_t raw_type = read_u32(data + 1); uint32_t base_type = raw_type % 1000; out.geom_type = (int32_t)base_type; size_t offset = 5; if (base_type == 1) { // Point if (offset + 16 <= size) { out.coords.push_back(read_double(data + offset)); out.coords.push_back(read_double(data + offset + 8)); out.num_points = 1; out.num_parts = 1; out.part_offsets.push_back(0); } } else if (base_type == 2) { // LineString if (offset + 4 <= size) { uint32_t n_pts = read_u32(data + offset); offset += 4; out.num_parts = 1; out.part_offsets.push_back(0); out.num_points = n_pts; out.coords.reserve(n_pts * 2); for (uint32_t i = 0; i < n_pts && (offset + 16 <= size); ++i) { out.coords.push_back(read_double(data + offset)); out.coords.push_back(read_double(data + offset + 8)); offset += 16; } } } else if (base_type == 3) { // Polygon if (offset + 4 <= size) { uint32_t n_rings = read_u32(data + offset); offset += 4; out.num_parts = n_rings; int64_t pt_count = 0; for (uint32_t r = 0; r < n_rings && (offset + 4 <= size); ++r) { out.part_offsets.push_back((int32_t)pt_count); uint32_t n_pts = read_u32(data + offset); offset += 4; pt_count += n_pts; for (uint32_t i = 0; i < n_pts && (offset + 16 <= size); ++i) { out.coords.push_back(read_double(data + offset)); out.coords.push_back(read_double(data + offset + 8)); offset += 16; } } out.num_points = pt_count; } } else if (base_type == 5) { // MultiLineString if (offset + 4 <= size) { uint32_t n_lines = read_u32(data + offset); offset += 4; out.num_parts = n_lines; int64_t pt_count = 0; for (uint32_t l = 0; l < n_lines && (offset + 9 <= size); ++l) { out.part_offsets.push_back((int32_t)pt_count); offset += 5; // skip endian & sub-type if (offset + 4 > size) break; uint32_t n_pts = read_u32(data + offset); offset += 4; pt_count += n_pts; for (uint32_t i = 0; i < n_pts && (offset + 16 <= size); ++i) { out.coords.push_back(read_double(data + offset)); out.coords.push_back(read_double(data + offset + 8)); offset += 16; } } out.num_points = pt_count; } } else if (base_type == 6) { // MultiPolygon if (offset + 4 <= size) { uint32_t n_polys = read_u32(data + offset); offset += 4; int64_t pt_count = 0; int32_t part_count = 0; for (uint32_t p = 0; p < n_polys && (offset + 9 <= size); ++p) { offset += 5; // skip endian & sub-type if (offset + 4 > size) break; uint32_t n_rings = read_u32(data + offset); offset += 4; for (uint32_t r = 0; r < n_rings && (offset + 4 <= size); ++r) { out.part_offsets.push_back((int32_t)pt_count); part_count++; uint32_t n_pts = read_u32(data + offset); offset += 4; pt_count += n_pts; for (uint32_t i = 0; i < n_pts && (offset + 16 <= size); ++i) { out.coords.push_back(read_double(data + offset)); out.coords.push_back(read_double(data + offset + 8)); offset += 16; } } } out.num_parts = part_count; out.num_points = pt_count; } } } BL_GEO_EXPORT int nm_geo_parse_wkb_batch( const uint8_t* const* wkb_ptrs, const size_t* wkb_sizes, size_t num_geoms, double** out_coords, int64_t* out_total_points, int32_t** out_parts, int64_t* out_total_parts, int32_t** out_geom_types, int64_t** out_geom_pt_offsets ) { if (!wkb_ptrs || !wkb_sizes || num_geoms == 0) return -1; std::vector parsed(num_geoms); // Parse geometries in parallel chunks size_t num_threads = 4; std::vector chunk_starts(num_threads), chunk_ends(num_threads); size_t chunk_size = (num_geoms + num_threads - 1) / num_threads; for (size_t g = 0; g < num_geoms; ++g) { if (wkb_ptrs[g] && wkb_sizes[g] > 0) { _parse_wkb_single(wkb_ptrs[g], wkb_sizes[g], parsed[g]); } } int64_t tot_pts = 0; int64_t tot_prts = 0; for (size_t g = 0; g < num_geoms; ++g) { tot_pts += parsed[g].num_points; tot_prts += parsed[g].num_parts; } *out_total_points = tot_pts; *out_total_parts = tot_prts; *out_coords = (double*)malloc(tot_pts * 2 * sizeof(double)); *out_parts = (int32_t*)malloc(tot_prts * sizeof(int32_t)); *out_geom_types = (int32_t*)malloc(num_geoms * sizeof(int32_t)); *out_geom_pt_offsets = (int64_t*)malloc((num_geoms + 1) * sizeof(int64_t)); double* c_ptr = *out_coords; int32_t* p_ptr = *out_parts; int64_t curr_pt_offset = 0; int64_t curr_part_offset = 0; for (size_t g = 0; g < num_geoms; ++g) { (*out_geom_types)[g] = parsed[g].geom_type; (*out_geom_pt_offsets)[g] = curr_pt_offset; for (size_t p = 0; p < parsed[g].part_offsets.size(); ++p) { p_ptr[curr_part_offset + p] = (int32_t)(curr_pt_offset + parsed[g].part_offsets[p]); } curr_part_offset += parsed[g].part_offsets.size(); if (parsed[g].coords.size() > 0) { memcpy(c_ptr + curr_pt_offset * 2, parsed[g].coords.data(), parsed[g].coords.size() * sizeof(double)); } curr_pt_offset += parsed[g].num_points; } (*out_geom_pt_offsets)[num_geoms] = curr_pt_offset; return 0; } BL_GEO_EXPORT void nm_geo_free_wkb_batch(double* coords, int32_t* parts, int32_t* geom_types, int64_t* geom_pt_offsets) { if (coords) free(coords); if (parts) free(parts); if (geom_types) free(geom_types); if (geom_pt_offsets) free(geom_pt_offsets); } // ============================================================================ // Native Fast CRS & Metric Projection Engine // ============================================================================ // CRS IDs: 4326=WGS84, 3857=WebMercator, 2263=NY State Plane Long Island, 5070=Albers CONUS BL_GEO_EXPORT int nm_geo_transform_batch_crs( double* coords, int64_t num_points, int crs_from, int crs_to ) { if (!coords || num_points <= 0) return -1; if (crs_from == crs_to) return 0; // 1. From 4326 -> 3857 if (crs_from == 4326 && crs_to == 3857) { return bl_geo_proj_3857_forward(coords, coords, num_points); } // 2. From 3857 -> 4326 if (crs_from == 3857 && crs_to == 4326) { return bl_geo_proj_3857_inverse(coords, coords, num_points); } // 3. From 4326 -> 2263 (NY State Plane Long Island ftUS) if (crs_from == 4326 && crs_to == 2263) { // EPSG:2263 Standard Parameters: phi1=40°40'N, phi2=41°02'N, phi0=40°10'N, lam0=-74°00'W, x0=300000m, y0=0m double phi1 = 40.66666666666667; double phi2 = 41.03333333333333; double phi0 = 40.16666666666667; double lam0 = -74.0; double x0 = 300000.0; double y0 = 0.0; return bl_geo_proj_lcc_forward(phi1, phi2, phi0, lam0, x0, y0, coords, coords, num_points); } // 4. From 4326 -> 5070 (Albers Equal Area Conic CONUS) if (crs_from == 4326 && crs_to == 5070) { double phi1 = 29.5; double phi2 = 45.5; double phi0 = 23.0; double lam0 = -96.0; double x0 = 0.0; double y0 = 0.0; return bl_geo_proj_albers_forward(phi1, phi2, phi0, lam0, x0, y0, coords, coords, num_points); } return -2; // Unsupported combination } // ============================================================================ // Hardware-Accelerated Bounding Box Liang-Barsky Vector Clipping // ============================================================================ static inline bool _clip_line_liang_barsky( double minx, double miny, double maxx, double maxy, double x0, double y0, double x1, double y1, double& cx0, double& cy0, double& cx1, double& cy1 ) { double p[4] = { -(x1 - x0), x1 - x0, -(y1 - y0), y1 - y0 }; double q[4] = { x0 - minx, maxx - x0, y0 - miny, maxy - y0 }; double u1 = 0.0, u2 = 1.0; for (int i = 0; i < 4; ++i) { if (p[i] == 0.0) { if (q[i] < 0.0) return false; } else { double t = q[i] / p[i]; if (p[i] < 0.0) { if (t > u1) u1 = t; } else { if (t < u2) u2 = t; } } } if (u1 > u2) return false; cx0 = x0 + u1 * (x1 - x0); cy0 = y0 + u1 * (y1 - y0); cx1 = x0 + u2 * (x1 - x0); cy1 = y0 + u2 * (y1 - y0); return true; } BL_GEO_EXPORT int nm_geo_clip_lines_bbox( const double* in_coords, int64_t num_segments, double minx, double miny, double maxx, double maxy, double* out_coords, int64_t* out_count ) { if (!in_coords || num_segments <= 0 || !out_coords || !out_count) return -1; int64_t count = 0; for (int64_t i = 0; i < num_segments; ++i) { double x0 = in_coords[i * 4]; double y0 = in_coords[i * 4 + 1]; double x1 = in_coords[i * 4 + 2]; double y1 = in_coords[i * 4 + 3]; double cx0, cy0, cx1, cy1; if (_clip_line_liang_barsky(minx, miny, maxx, maxy, x0, y0, x1, y1, cx0, cy0, cx1, cy1)) { out_coords[count * 4] = cx0; out_coords[count * 4 + 1] = cy0; out_coords[count * 4 + 2] = cx1; out_coords[count * 4 + 3] = cy1; count++; } } *out_count = count; return 0; } // ============================================================================ // Procedural Spatial Animation & Wave Propagation Generator // ============================================================================ BL_GEO_EXPORT int nm_geo_calculate_propagation( const double* centroids, size_t num_items, const double* epicenters, size_t num_epicenters, double duration_frames, double speed, double* out_start_frames, double* out_end_frames ) { if (!centroids || num_items == 0 || !epicenters || num_epicenters == 0 || !out_start_frames || !out_end_frames) { return -1; } std::vector min_dists(num_items, 1e18); double max_d = 0.0; for (size_t i = 0; i < num_items; ++i) { double cx = centroids[i * 2]; double cy = centroids[i * 2 + 1]; for (size_t e = 0; e < num_epicenters; ++e) { double ex = epicenters[e * 2]; double ey = epicenters[e * 2 + 1]; double d = std::hypot(cx - ex, cy - ey); if (d < min_dists[i]) { min_dists[i] = d; } } if (min_dists[i] > max_d) max_d = min_dists[i]; } if (max_d <= 0.0) max_d = 1.0; double travel_time = duration_frames * 0.75 * (speed > 0 ? speed : 1.0); double reveal_window = duration_frames * 0.25; for (size_t i = 0; i < num_items; ++i) { double norm_d = min_dists[i] / max_d; double start_f = norm_d * travel_time; out_start_frames[i] = start_f; out_end_frames[i] = start_f + reveal_window; } return 0; } // ============================================================================ // Sutherland-Hodgman Boolean Polygon Vector Clipping against BBox // ============================================================================ struct Point2D { double x, y; }; static std::vector _clip_poly_single_edge( const std::vector& poly, int edge, double edge_val ) { std::vector out; if (poly.empty()) return out; auto is_inside = [edge, edge_val](const Point2D& p) -> bool { switch (edge) { case 0: return p.x >= edge_val; // Left: x >= minx case 1: return p.x <= edge_val; // Right: x <= maxx case 2: return p.y >= edge_val; // Bottom: y >= miny case 3: return p.y <= edge_val; // Top: y <= maxy } return true; }; auto compute_intersect = [edge, edge_val](const Point2D& p1, const Point2D& p2) -> Point2D { Point2D res; double dx = p2.x - p1.x; double dy = p2.y - p1.y; switch (edge) { case 0: // x = minx case 1: // x = maxx res.x = edge_val; res.y = (std::abs(dx) > 1e-12) ? p1.y + dy * (edge_val - p1.x) / dx : p1.y; break; case 2: // y = miny case 3: // y = maxy res.y = edge_val; res.x = (std::abs(dy) > 1e-12) ? p1.x + dx * (edge_val - p1.y) / dy : p1.x; break; } return res; }; Point2D prev = poly.back(); bool prev_inside = is_inside(prev); for (const auto& curr : poly) { bool curr_inside = is_inside(curr); if (curr_inside) { if (!prev_inside) { out.push_back(compute_intersect(prev, curr)); } out.push_back(curr); } else if (prev_inside) { out.push_back(compute_intersect(prev, curr)); } prev = curr; prev_inside = curr_inside; } return out; } BL_GEO_EXPORT int nm_geo_clip_polygon_bbox( const double* in_coords, int64_t in_count, double minx, double miny, double maxx, double maxy, double* out_coords, int64_t* out_count ) { if (!in_coords || in_count < 3 || !out_coords || !out_count) return -1; std::vector poly(in_count); for (int64_t i = 0; i < in_count; ++i) { poly[i] = { in_coords[i * 2], in_coords[i * 2 + 1] }; } std::vector clipped = _clip_poly_single_edge(poly, 0, minx); if (!clipped.empty()) clipped = _clip_poly_single_edge(clipped, 1, maxx); if (!clipped.empty()) clipped = _clip_poly_single_edge(clipped, 2, miny); if (!clipped.empty()) clipped = _clip_poly_single_edge(clipped, 3, maxy); *out_count = (int64_t)clipped.size(); for (size_t i = 0; i < clipped.size(); ++i) { out_coords[i * 2] = clipped[i].x; out_coords[i * 2 + 1] = clipped[i].y; } return 0; } BL_GEO_EXPORT int bl_geo_build_polygon_rings( BLPathCore* path, const double* coords, const int64_t* ring_offsets, int64_t num_rings, const GeoTransform* gt ) { if (!path || !coords || !ring_offsets || num_rings <= 0) return -1; for (int64_t r = 0; r < num_rings; ++r) { int64_t start_idx = ring_offsets[r]; int64_t end_idx = ring_offsets[r + 1]; int64_t pt_count = end_idx - start_idx; if (pt_count < 3) continue; double sx, sy; transform_pt(gt, coords[start_idx * 2], coords[start_idx * 2 + 1], sx, sy); bl_path_move_to(path, sx, sy); for (int64_t i = start_idx + 1; i < end_idx; ++i) { transform_pt(gt, coords[i * 2], coords[i * 2 + 1], sx, sy); bl_path_line_to(path, sx, sy); } bl_path_close(path); // Essential: prevents spiderweb/fanning glitches! } return 0; } // ============================================================================ // Structure of Arrays (SoA) & Path Memory Arena // ============================================================================ struct BLBox2D { double x0, y0, x1, y1; }; struct NativeGeoLayerSoA { size_t count; size_t capacity; BLPathCore* paths; // contiguous array of BLPathCore [capacity] BLBox2D* bboxes; // contiguous array of BLBox2D [capacity] double* centroids; // contiguous array of (cx, cy) [capacity * 2] float* distances; // contiguous array of distances [capacity] double* start_frames; // contiguous array of start_frames [capacity] double* end_frames; // contiguous array of end_frames [capacity] uint8_t* is_active; // contiguous array of active visibility flags [capacity] void* arena_memory; // backing contiguous memory arena size_t arena_size; }; BL_GEO_EXPORT NativeGeoLayerSoA* nm_geo_soa_create(size_t capacity) { if (capacity == 0) capacity = 1024; size_t sz_paths = capacity * sizeof(BLPathCore); size_t sz_bboxes = capacity * sizeof(BLBox2D); size_t sz_centroids = capacity * 2 * sizeof(double); size_t sz_distances = capacity * sizeof(float); size_t sz_start_frames = capacity * sizeof(double); size_t sz_end_frames = capacity * sizeof(double); size_t sz_active = capacity * sizeof(uint8_t); size_t total_alloc = sz_paths + sz_bboxes + sz_centroids + sz_distances + sz_start_frames + sz_end_frames + sz_active + 256; uint8_t* mem = (uint8_t*)malloc(total_alloc); if (!mem) return nullptr; memset(mem, 0, total_alloc); NativeGeoLayerSoA* layer = new NativeGeoLayerSoA(); layer->count = 0; layer->capacity = capacity; layer->arena_memory = mem; layer->arena_size = total_alloc; uint8_t* ptr = mem; layer->paths = (BLPathCore*)ptr; ptr += sz_paths; layer->bboxes = (BLBox2D*)ptr; ptr += sz_bboxes; layer->centroids = (double*)ptr; ptr += sz_centroids; layer->distances = (float*)ptr; ptr += sz_distances; layer->start_frames = (double*)ptr; ptr += sz_start_frames; layer->end_frames = (double*)ptr; ptr += sz_end_frames; layer->is_active = (uint8_t*)ptr; ptr += sz_active; for (size_t i = 0; i < capacity; ++i) { bl_path_init(&layer->paths[i]); layer->is_active[i] = 1; } return layer; } BL_GEO_EXPORT void nm_geo_soa_destroy(NativeGeoLayerSoA* layer) { if (!layer) return; for (size_t i = 0; i < layer->capacity; ++i) { bl_path_destroy(&layer->paths[i]); } if (layer->arena_memory) { free(layer->arena_memory); } delete layer; } BL_GEO_EXPORT int nm_geo_soa_add_polygon( NativeGeoLayerSoA* layer, const double* coords, const int64_t* ring_offsets, int64_t num_rings, const GeoTransform* gt ) { if (!layer || layer->count >= layer->capacity || !coords || !ring_offsets || num_rings <= 0) { return -1; } size_t idx = layer->count; BLPathCore* path = &layer->paths[idx]; bl_path_reset(path); double minx = 1e30, miny = 1e30, maxx = -1e30, maxy = -1e30; double sum_x = 0.0, sum_y = 0.0; int64_t total_pts = 0; for (int64_t r = 0; r < num_rings; ++r) { int64_t start_idx = ring_offsets[r]; int64_t end_idx = ring_offsets[r + 1]; int64_t pt_count = end_idx - start_idx; if (pt_count < 3) continue; double sx, sy; transform_pt(gt, coords[start_idx * 2], coords[start_idx * 2 + 1], sx, sy); bl_path_move_to(path, sx, sy); if (sx < minx) minx = sx; if (sx > maxx) maxx = sx; if (sy < miny) miny = sy; if (sy > maxy) maxy = sy; sum_x += sx; sum_y += sy; total_pts++; for (int64_t i = start_idx + 1; i < end_idx; ++i) { transform_pt(gt, coords[i * 2], coords[i * 2 + 1], sx, sy); bl_path_line_to(path, sx, sy); if (sx < minx) minx = sx; if (sx > maxx) maxx = sx; if (sy < miny) miny = sy; if (sy > maxy) maxy = sy; sum_x += sx; sum_y += sy; total_pts++; } bl_path_close(path); // Auto-close ring } if (total_pts == 0) { minx = miny = maxx = maxy = 0.0; sum_x = sum_y = 0.0; total_pts = 1; } layer->bboxes[idx].x0 = minx; layer->bboxes[idx].y0 = miny; layer->bboxes[idx].x1 = maxx; layer->bboxes[idx].y1 = maxy; layer->centroids[idx * 2] = sum_x / (double)total_pts; layer->centroids[idx * 2 + 1] = sum_y / (double)total_pts; layer->is_active[idx] = 1; layer->count++; return (int)idx; } BL_GEO_EXPORT int nm_geo_soa_from_wkb_batch( NativeGeoLayerSoA* layer, const uint8_t* const* wkb_ptrs, const size_t* wkb_sizes, size_t num_geoms, const GeoTransform* gt ) { if (!layer || !wkb_ptrs || !wkb_sizes || num_geoms == 0) return -1; for (size_t g = 0; g < num_geoms; ++g) { if (layer->count >= layer->capacity) break; if (!wkb_ptrs[g] || wkb_sizes[g] < 5) continue; WkbParsedGeometry geom; _parse_wkb_single(wkb_ptrs[g], wkb_sizes[g], geom); if (geom.num_points == 0) continue; size_t idx = layer->count; BLPathCore* path = &layer->paths[idx]; bl_path_reset(path); double minx = 1e30, miny = 1e30, maxx = -1e30, maxy = -1e30; double sum_x = 0.0, sum_y = 0.0; int64_t total_pts = 0; if (geom.geom_type == 1) { // Point if (geom.coords.size() >= 2) { double sx, sy; transform_pt(gt, geom.coords[0], geom.coords[1], sx, sy); bl_path_move_to(path, sx - 2.0, sy); bl_path_line_to(path, sx + 2.0, sy); minx = maxx = sx; miny = maxy = sy; sum_x = sx; sum_y = sy; total_pts = 1; } } else if (geom.geom_type == 2) { // LineString int64_t n_pts = (int64_t)(geom.coords.size() / 2); if (n_pts >= 2) { double sx, sy; transform_pt(gt, geom.coords[0], geom.coords[1], sx, sy); bl_path_move_to(path, sx, sy); if (sx < minx) minx = sx; if (sx > maxx) maxx = sx; if (sy < miny) miny = sy; if (sy > maxy) maxy = sy; sum_x += sx; sum_y += sy; total_pts++; for (int64_t i = 1; i < n_pts; ++i) { transform_pt(gt, geom.coords[i * 2], geom.coords[i * 2 + 1], sx, sy); bl_path_line_to(path, sx, sy); if (sx < minx) minx = sx; if (sx > maxx) maxx = sx; if (sy < miny) miny = sy; if (sy > maxy) maxy = sy; sum_x += sx; sum_y += sy; total_pts++; } } } else if (geom.geom_type == 3 || geom.geom_type == 6) { // Polygon or MultiPolygon size_t n_parts = geom.part_offsets.size(); for (size_t p = 0; p < n_parts; ++p) { int64_t start_pt = geom.part_offsets[p]; int64_t end_pt = (p + 1 < n_parts) ? geom.part_offsets[p + 1] : (int64_t)(geom.coords.size() / 2); int64_t pt_cnt = end_pt - start_pt; if (pt_cnt < 3) continue; double sx, sy; transform_pt(gt, geom.coords[start_pt * 2], geom.coords[start_pt * 2 + 1], sx, sy); bl_path_move_to(path, sx, sy); if (sx < minx) minx = sx; if (sx > maxx) maxx = sx; if (sy < miny) miny = sy; if (sy > maxy) maxy = sy; sum_x += sx; sum_y += sy; total_pts++; for (int64_t i = start_pt + 1; i < end_pt; ++i) { transform_pt(gt, geom.coords[i * 2], geom.coords[i * 2 + 1], sx, sy); bl_path_line_to(path, sx, sy); if (sx < minx) minx = sx; if (sx > maxx) maxx = sx; if (sy < miny) miny = sy; if (sy > maxy) maxy = sy; sum_x += sx; sum_y += sy; total_pts++; } bl_path_close(path); // Auto-close ring: cuts interior holes with EVEN_ODD! } } else if (geom.geom_type == 5) { // MultiLineString size_t n_parts = geom.part_offsets.size(); for (size_t p = 0; p < n_parts; ++p) { int64_t start_pt = geom.part_offsets[p]; int64_t end_pt = (p + 1 < n_parts) ? geom.part_offsets[p + 1] : (int64_t)(geom.coords.size() / 2); int64_t pt_cnt = end_pt - start_pt; if (pt_cnt < 2) continue; double sx, sy; transform_pt(gt, geom.coords[start_pt * 2], geom.coords[start_pt * 2 + 1], sx, sy); bl_path_move_to(path, sx, sy); if (sx < minx) minx = sx; if (sx > maxx) maxx = sx; if (sy < miny) miny = sy; if (sy > maxy) maxy = sy; sum_x += sx; sum_y += sy; total_pts++; for (int64_t i = start_pt + 1; i < end_pt; ++i) { transform_pt(gt, geom.coords[i * 2], geom.coords[i * 2 + 1], sx, sy); bl_path_line_to(path, sx, sy); if (sx < minx) minx = sx; if (sx > maxx) maxx = sx; if (sy < miny) miny = sy; if (sy > maxy) maxy = sy; sum_x += sx; sum_y += sy; total_pts++; } } } if (total_pts == 0) { minx = miny = maxx = maxy = 0.0; sum_x = sum_y = 0.0; total_pts = 1; } layer->bboxes[idx].x0 = minx; layer->bboxes[idx].y0 = miny; layer->bboxes[idx].x1 = maxx; layer->bboxes[idx].y1 = maxy; layer->centroids[idx * 2] = sum_x / (double)total_pts; layer->centroids[idx * 2 + 1] = sum_y / (double)total_pts; layer->is_active[idx] = 1; layer->count++; } return (int)layer->count; } BL_GEO_EXPORT int nm_geo_soa_compute_propagation( NativeGeoLayerSoA* layer, const double* epicenters, size_t num_epicenters, double duration_frames, double speed ) { if (!layer || layer->count == 0 || !epicenters || num_epicenters == 0) return -1; size_t n = layer->count; double max_d = 0.0; for (size_t i = 0; i < n; ++i) { double cx = layer->centroids[i * 2]; double cy = layer->centroids[i * 2 + 1]; double min_d = 1e18; for (size_t e = 0; e < num_epicenters; ++e) { double ex = epicenters[e * 2]; double ey = epicenters[e * 2 + 1]; double d = std::hypot(cx - ex, cy - ey); if (d < min_d) min_d = d; } layer->distances[i] = (float)min_d; if (min_d > max_d) max_d = min_d; } if (max_d <= 0.0) max_d = 1.0; double travel_time = duration_frames * 0.75 * (speed > 0.0 ? speed : 1.0); double reveal_window = duration_frames * 0.25; for (size_t i = 0; i < n; ++i) { double norm_d = (double)layer->distances[i] / max_d; double sf = norm_d * travel_time; layer->start_frames[i] = sf; layer->end_frames[i] = sf + reveal_window; } return 0; } BL_GEO_EXPORT int nm_geo_soa_cull_and_update( NativeGeoLayerSoA* layer, double clip_x0, double clip_y0, double clip_x1, double clip_y1, double current_frame, int64_t* out_visible_count ) { if (!layer) return -1; size_t n = layer->count; int64_t vis_count = 0; for (size_t i = 0; i < n; ++i) { if (current_frame >= 0.0 && current_frame < layer->start_frames[i]) { layer->is_active[i] = 0; continue; } const BLBox2D& b = layer->bboxes[i]; if (b.x1 < clip_x0 || b.x0 > clip_x1 || b.y1 < clip_y0 || b.y0 > clip_y1) { layer->is_active[i] = 0; continue; } layer->is_active[i] = 1; vis_count++; } if (out_visible_count) *out_visible_count = vis_count; return 0; } BL_GEO_EXPORT int nm_geo_soa_render_dual_buffer( void* ctx_ptr, const NativeGeoLayerSoA* layer, uint32_t fill_rgba, uint32_t stroke_rgba, double stroke_width, int fill_enabled, int stroke_enabled ) { if (!ctx_ptr || !layer || layer->count == 0) return -1; BLContextCore* ctx = (BLContextCore*)ctx_ptr; size_t n = layer->count; bl_context_set_fill_rule(ctx, 1 /* EVEN_ODD */); // Pass 1: Batch Fill Pass (all fills in a tight unrolled loop) if (fill_enabled) { for (size_t i = 0; i < n; ++i) { if (layer->is_active[i]) { bl_context_fill_geometry_rgba32(ctx, 21 /* BL_GEOMETRY_TYPE_PATH */, &layer->paths[i], fill_rgba); } } } // Pass 2: Batch Stroke Pass (all strokes in a tight loop) if (stroke_enabled && stroke_width > 0.0) { bl_context_set_stroke_width(ctx, stroke_width); for (size_t i = 0; i < n; ++i) { if (layer->is_active[i]) { bl_context_stroke_geometry_rgba32(ctx, 21 /* BL_GEOMETRY_TYPE_PATH */, &layer->paths[i], stroke_rgba); } } } return 0; } static inline double _geo_easing_back_out(double t) { if (t <= 0.0) return 0.0; if (t >= 1.0) return 1.0; double c1 = 1.70158; double c3 = c1 + 1.0; double t_m_1 = t - 1.0; return 1.0 + c3 * (t_m_1 * t_m_1 * t_m_1) + c1 * (t_m_1 * t_m_1); } BL_GEO_EXPORT int nm_geo_soa_render_popin( void* ctx_ptr, const NativeGeoLayerSoA* layer, double current_frame, uint32_t fill_rgba, uint32_t stroke_rgba, double stroke_width, int fill_enabled, int stroke_enabled ) { if (!ctx_ptr || !layer || layer->count == 0) return -1; BLContextCore* ctx = (BLContextCore*)ctx_ptr; size_t n = layer->count; bl_context_set_fill_rule(ctx, 1 /* EVEN_ODD */); // Pass 1: Batch Fill Pass with Centroid Spring Pop-In if (fill_enabled) { for (size_t i = 0; i < n; ++i) { if (!layer->is_active[i]) continue; double sf = layer->start_frames[i]; double ef = layer->end_frames[i]; if (current_frame < sf) continue; if (current_frame >= ef) { bl_context_fill_geometry_rgba32(ctx, 21, &layer->paths[i], fill_rgba); } else { double t = (current_frame - sf) / (ef - sf); double scale = _geo_easing_back_out(t); if (scale > 0.001) { double cx = layer->centroids[i * 2]; double cy = layer->centroids[i * 2 + 1]; BLMatrix2D m; m.m00 = scale; m.m01 = 0.0; m.m10 = 0.0; m.m11 = scale; m.m20 = cx * (1.0 - scale); m.m21 = cy * (1.0 - scale); bl_context_apply_transform_op(ctx, 1 /* ASSIGN */, &m); bl_context_fill_geometry_rgba32(ctx, 21, &layer->paths[i], fill_rgba); bl_context_apply_transform_op(ctx, 0 /* RESET */, NULL); } } } } // Pass 2: Batch Stroke Pass with Centroid Spring Pop-In if (stroke_enabled && stroke_width > 0.0) { bl_context_set_stroke_width(ctx, stroke_width); for (size_t i = 0; i < n; ++i) { if (!layer->is_active[i]) continue; double sf = layer->start_frames[i]; double ef = layer->end_frames[i]; if (current_frame < sf) continue; if (current_frame >= ef) { bl_context_stroke_geometry_rgba32(ctx, 21, &layer->paths[i], stroke_rgba); } else { double t = (current_frame - sf) / (ef - sf); double scale = _geo_easing_back_out(t); if (scale > 0.001) { double cx = layer->centroids[i * 2]; double cy = layer->centroids[i * 2 + 1]; BLMatrix2D m; m.m00 = scale; m.m01 = 0.0; m.m10 = 0.0; m.m11 = scale; m.m20 = cx * (1.0 - scale); m.m21 = cy * (1.0 - scale); bl_context_apply_transform_op(ctx, 1 /* ASSIGN */, &m); bl_context_stroke_geometry_rgba32(ctx, 21, &layer->paths[i], stroke_rgba); bl_context_apply_transform_op(ctx, 0 /* RESET */, NULL); } } } } return 0; } // ============================================================================ // Flat Uniform Grid Spatial Indexing // ============================================================================ struct UniformGrid2D { double minx, miny, maxx, maxy; double cell_w, cell_h; int32_t grid_w, grid_h; int32_t* cell_counts; // [grid_w * grid_h] int32_t* cell_offsets; // [grid_w * grid_h + 1] int32_t* item_indices; // [total_references] int32_t total_references; }; BL_GEO_EXPORT UniformGrid2D* nm_geo_grid_create( double minx, double miny, double maxx, double maxy, int32_t grid_w, int32_t grid_h ) { if (grid_w <= 0) grid_w = 32; if (grid_h <= 0) grid_h = 32; if (maxx <= minx) maxx = minx + 1.0; if (maxy <= miny) maxy = miny + 1.0; UniformGrid2D* grid = new UniformGrid2D(); grid->minx = minx; grid->miny = miny; grid->maxx = maxx; grid->maxy = maxy; grid->grid_w = grid_w; grid->grid_h = grid_h; grid->cell_w = (maxx - minx) / (double)grid_w; grid->cell_h = (maxy - miny) / (double)grid_h; size_t num_cells = (size_t)(grid_w * grid_h); grid->cell_counts = (int32_t*)calloc(num_cells, sizeof(int32_t)); grid->cell_offsets = (int32_t*)calloc(num_cells + 1, sizeof(int32_t)); grid->item_indices = nullptr; grid->total_references = 0; return grid; } BL_GEO_EXPORT int nm_geo_grid_build( UniformGrid2D* grid, const BLBox2D* bboxes, size_t count ) { if (!grid || !bboxes || count == 0) return -1; int32_t gw = grid->grid_w; int32_t gh = grid->grid_h; int32_t num_cells = gw * gh; memset(grid->cell_counts, 0, num_cells * sizeof(int32_t)); auto to_cx = [grid, gw](double x) -> int32_t { int32_t c = (int32_t)((x - grid->minx) / grid->cell_w); if (c < 0) c = 0; if (c >= gw) c = gw - 1; return c; }; auto to_cy = [grid, gh](double y) -> int32_t { int32_t c = (int32_t)((y - grid->miny) / grid->cell_h); if (c < 0) c = 0; if (c >= gh) c = gh - 1; return c; }; int32_t total_refs = 0; for (size_t i = 0; i < count; ++i) { int32_t cx0 = to_cx(bboxes[i].x0); int32_t cx1 = to_cx(bboxes[i].x1); int32_t cy0 = to_cy(bboxes[i].y0); int32_t cy1 = to_cy(bboxes[i].y1); for (int32_t y = cy0; y <= cy1; ++y) { for (int32_t x = cx0; x <= cx1; ++x) { int32_t cell = y * gw + x; grid->cell_counts[cell]++; total_refs++; } } } grid->total_references = total_refs; if (grid->item_indices) free(grid->item_indices); grid->item_indices = (int32_t*)malloc(total_refs * sizeof(int32_t)); int32_t cur_offset = 0; for (int32_t c = 0; c < num_cells; ++c) { grid->cell_offsets[c] = cur_offset; cur_offset += grid->cell_counts[c]; } grid->cell_offsets[num_cells] = cur_offset; memset(grid->cell_counts, 0, num_cells * sizeof(int32_t)); for (size_t i = 0; i < count; ++i) { int32_t cx0 = to_cx(bboxes[i].x0); int32_t cx1 = to_cx(bboxes[i].x1); int32_t cy0 = to_cy(bboxes[i].y0); int32_t cy1 = to_cy(bboxes[i].y1); for (int32_t y = cy0; y <= cy1; ++y) { for (int32_t x = cx0; x <= cx1; ++x) { int32_t cell = y * gw + x; int32_t write_pos = grid->cell_offsets[cell] + grid->cell_counts[cell]; grid->item_indices[write_pos] = (int32_t)i; grid->cell_counts[cell]++; } } } return 0; } BL_GEO_EXPORT int nm_geo_grid_query_bbox( const UniformGrid2D* grid, double qx0, double qy0, double qx1, double qy1, int32_t* out_indices, int32_t max_indices, int32_t* out_count ) { if (!grid || !out_indices || max_indices <= 0 || !out_count) return -1; int32_t gw = grid->grid_w; int32_t gh = grid->grid_h; auto to_cx = [grid, gw](double x) -> int32_t { int32_t c = (int32_t)((x - grid->minx) / grid->cell_w); if (c < 0) c = 0; if (c >= gw) c = gw - 1; return c; }; auto to_cy = [grid, gh](double y) -> int32_t { int32_t c = (int32_t)((y - grid->miny) / grid->cell_h); if (c < 0) c = 0; if (c >= gh) c = gh - 1; return c; }; int32_t cx0 = to_cx(qx0); int32_t cx1 = to_cx(qx1); int32_t cy0 = to_cy(qy0); int32_t cy1 = to_cy(qy1); int32_t found = 0; int32_t max_item_id = -1; for (int32_t y = cy0; y <= cy1; ++y) { for (int32_t x = cx0; x <= cx1; ++x) { int32_t cell = y * gw + x; int32_t cnt = grid->cell_counts[cell]; int32_t off = grid->cell_offsets[cell]; for (int32_t k = 0; k < cnt; ++k) { int32_t idx = grid->item_indices[off + k]; if (idx > max_item_id) max_item_id = idx; } } } if (max_item_id < 0) { *out_count = 0; return 0; } std::vector visited(max_item_id + 1, false); for (int32_t y = cy0; y <= cy1 && found < max_indices; ++y) { for (int32_t x = cx0; x <= cx1 && found < max_indices; ++x) { int32_t cell = y * gw + x; int32_t cnt = grid->cell_counts[cell]; int32_t off = grid->cell_offsets[cell]; for (int32_t k = 0; k < cnt && found < max_indices; ++k) { int32_t idx = grid->item_indices[off + k]; if (!visited[idx]) { visited[idx] = true; out_indices[found++] = idx; } } } } *out_count = found; return 0; } BL_GEO_EXPORT void nm_geo_grid_destroy(UniformGrid2D* grid) { if (!grid) return; if (grid->cell_counts) free(grid->cell_counts); if (grid->cell_offsets) free(grid->cell_offsets); if (grid->item_indices) free(grid->item_indices); delete grid; } } // extern "C"