feat(simulation): add Worker AI polygon runtime and terrain navigation

This commit is contained in:
DCCONSTRUCTIONS
2026-09-25 16:40:45 +03:00
parent a7c64e009d
commit f01bd39037
88 changed files with 9918 additions and 108 deletions
@@ -0,0 +1,10 @@
FROM ros:humble-ros-base-jammy
LABEL com.nodedc.product="mission-core" \
com.nodedc.stack="ai-polygon-route-author" \
com.nodedc.role="offline-route-author" \
com.nodedc.managed-by="ai-polygon-worker"
RUN apt-get update && apt-get install -y --no-install-recommends g++ librecast-dev \
&& rm -rf /var/lib/apt/lists/*
COPY habitat_route.cpp /src/habitat_route.cpp
RUN g++ -O2 -std=c++17 -I/usr/include/recastnavigation /src/habitat_route.cpp -lDetour -o /usr/local/bin/habitat-route
ENTRYPOINT ["habitat-route"]
@@ -0,0 +1,25 @@
# Offline Habitat route authoring
This tool uses Ubuntu's packaged Recast/Detour library to extract a complete
route from a supplied Habitat navmesh. It runs on Worker only. It outputs task
waypoints, never runtime collision knowledge for the AI composition.
Build the `Dockerfile` into `ndc-ai-polygon-route-author:habitat-v1` using the
isolated anonymous Docker configuration described in the Worker runbook. Mount
only the source asset directory read-only, disable container networking, and run:
```
habitat-route /data/scene.navmesh startX startY startZ goalX goalY goalZ
```
Coordinates remain Habitat Y-up in the output. Mission Core conversion is
`(x,y,z) -> (x,-z,y)`. The source navmesh's agent radius and step capability may
be incompatible with the 1 x 1 m rover. Extracted points require full-footprint
support/collision checks and a real closed-loop run before acceptance. Partial
paths, incompatible binary layouts, and disconnected endpoints are rejected.
Sources: Habitat-Sim `src/esp/nav/PathFinder.cpp` and `PathFinder.h` define the
version 1/2 binary container; Recast Navigation's Detour performs the actual
path search. The loader supports 32-bit Detour references and the v2 56-byte
settings layout. The image build and source/output hashes belong in the
private qualification receipt. No asset is modified by route extraction.
@@ -0,0 +1,49 @@
// Offline mission authoring using the supplied Habitat/Detour navmesh.
// Binary layout: facebookresearch/habitat-sim src/esp/nav/PathFinder.cpp.
// No navmesh or privileged scene geometry is supplied to runtime inference.
#include <DetourNavMesh.h>
#include <DetourNavMeshQuery.h>
#include <DetourAlloc.h>
#include <fstream>
#include <iostream>
#include <iomanip>
#include <stdexcept>
#include <cmath>
struct Header { int magic, version, tiles; dtNavMeshParams params; };
struct Tile { dtTileRef ref; int size; };
void require(bool ok, const char* message) { if (!ok) throw std::runtime_error(message); }
int main(int argc, char** argv) { try {
require(argc == 8, "navmesh start-x start-y start-z goal-x goal-y goal-z required");
std::ifstream file(argv[1], std::ios::binary); Header h{};
require(bool(file.read(reinterpret_cast<char*>(&h), sizeof(h))), "header read failed");
require(h.magic == (('M'<<24)|('S'<<16)|('E'<<8)|'T') && (h.version==1 || h.version==2), "unsupported navmesh");
require(h.tiles > 0 && h.tiles < 100000, "invalid tile count");
// v2 stores thirteen float settings and four boolean flags (56 bytes).
if (h.version == 2) file.seekg(56, std::ios::cur);
dtNavMesh* mesh = dtAllocNavMesh(); require(mesh, "allocation failed");
require(dtStatusSucceed(mesh->init(&h.params)), "init failed");
for (int i=0;i<h.tiles;i++) {
Tile t{}; require(bool(file.read(reinterpret_cast<char*>(&t),sizeof(t))), "tile header failed");
require(t.ref && t.size>0 && t.size<100000000, "invalid tile");
auto* data=static_cast<unsigned char*>(dtAlloc(t.size,DT_ALLOC_PERM));
require(data && bool(file.read(reinterpret_cast<char*>(data),t.size)), "tile read failed");
require(dtStatusSucceed(mesh->addTile(data,t.size,DT_TILE_FREE_DATA,t.ref,nullptr)), "tile version mismatch");
}
require(file.peek()==std::char_traits<char>::eof(), "unexpected trailing bytes");
dtNavMeshQuery query; require(dtStatusSucceed(query.init(mesh,65535)), "query init failed");
float start[3],goal[3],nearStart[3],nearGoal[3],extent[3]={2,4,2};
for(int i=0;i<3;i++){start[i]=std::stof(argv[i+2]);goal[i]=std::stof(argv[i+5]);}
dtQueryFilter filter; dtPolyRef first=0,last=0;
require(dtStatusSucceed(query.findNearestPoly(start,extent,&filter,&first,nearStart)) && first,"start outside navmesh");
require(dtStatusSucceed(query.findNearestPoly(goal,extent,&filter,&last,nearGoal)) && last,"goal outside navmesh");
dtPolyRef path[4096];int count=0;
auto status=query.findPath(first,last,nearStart,nearGoal,&filter,path,&count,4096);
require(dtStatusSucceed(status) && !dtStatusDetail(status,DT_BUFFER_TOO_SMALL) && count && path[count-1]==last,"no complete route");
float points[4096*3];int n=0;
status=query.findStraightPath(nearStart,nearGoal,path,count,points,nullptr,nullptr,&n,4096,DT_STRAIGHTPATH_ALL_CROSSINGS);
require(dtStatusSucceed(status) && !dtStatusDetail(status,DT_BUFFER_TOO_SMALL) && n>=2,"route extraction failed");
double length=0;for(int i=1;i<n;i++){double d=0;for(int j=0;j<3;j++)d+=std::pow(points[i*3+j]-points[(i-1)*3+j],2);length+=std::sqrt(d);}
std::cout<<std::setprecision(10)<<"{\"length_m\":"<<length<<",\"points_habitat\":[";
for(int i=0;i<n;i++){if(i)std::cout<<',';std::cout<<'['<<points[i*3]<<','<<points[i*3+1]<<','<<points[i*3+2]<<']';}
std::cout<<"]}"<<std::endl;dtFreeNavMesh(mesh);return 0;
} catch(const std::exception& e){std::cerr<<e.what()<<std::endl;return 1;} }