// // Copyright (c) 2009-2010 Mikko Mononen memon@inside.org // // This software is provided 'as-is', without any express or implied // warranty. In no event will the authors be held liable for any damages // arising from the use of this software. // Permission is granted to anyone to use this software for any purpose, // including commercial applications, and to alter it and redistribute it // freely, subject to the following restrictions: // 1. The origin of this software must not be misrepresented; you must not // claim that you wrote the original software. If you use this software // in a product, an acknowledgment in the product documentation would be // appreciated but is not required. // 2. Altered source versions must be plainly marked as such, and must not be // misrepresented as being the original software. // 3. This notice may not be removed or altered from any source distribution. // #include "Sample_SoloMesh.h" #include #include #include #include "ConvexVolumeTool.h" #include "CrowdTool.h" #include "DetourDebugDraw.h" #include "DetourNavMesh.h" #include "DetourNavMeshBuilder.h" #include "InputGeom.h" #include "NavMeshPruneTool.h" #include "NavMeshTesterTool.h" #include "OffMeshConnectionTool.h" #include "Recast.h" #include "RecastDebugDraw.h" #include "RecastDump.h" #include "Sample.h" #include "SDL_opengl.h" #include #ifdef WIN32 # define snprintf _snprintf #endif Sample_SoloMesh::Sample_SoloMesh() { setTool(new NavMeshTesterTool); } Sample_SoloMesh::~Sample_SoloMesh() { cleanup(); } void Sample_SoloMesh::cleanup() { delete [] triareas; triareas = 0; rcFreeHeightField(heightfield); heightfield = 0; rcFreeCompactHeightfield(compactHeightfield); compactHeightfield = 0; rcFreeContourSet(contourSet); contourSet = 0; rcFreePolyMesh(polyMesh); polyMesh = 0; rcFreePolyMeshDetail(detailMesh); detailMesh = 0; dtFreeNavMesh(navMesh); navMesh = 0; } void Sample_SoloMesh::handleSettings() { handleCommonSettings(); ImGui::Separator(); if (ImGui::Button("Save")) { saveAll("solo_navmesh.bin", navMesh); } ImGui::SameLine(); if (ImGui::Button("Load")) { dtFreeNavMesh(navMesh); navMesh = loadAll("solo_navmesh.bin"); navQuery->init(navMesh, 2048); } ImGui::Text("Build Time: %.1fms", totalBuildTimeMs); ImGui::Separator(); } void Sample_SoloMesh::handleTools() { const SampleToolType type = !tool ? SampleToolType::NONE : tool->type(); if (ImGui::RadioButton("Test Navmesh", type == SampleToolType::NAVMESH_TESTER)) { setTool(new NavMeshTesterTool); } if (ImGui::RadioButton("Prune Navmesh", type == SampleToolType::NAVMESH_PRUNE)) { setTool(new NavMeshPruneTool); } if (ImGui::RadioButton("Create Off-Mesh Connections", type == SampleToolType::OFFMESH_CONNECTION)) { setTool(new OffMeshConnectionTool); } if (ImGui::RadioButton("Create Convex Volumes", type == SampleToolType::CONVEX_VOLUME)) { setTool(new ConvexVolumeTool); } if (ImGui::RadioButton("Create Crowds", type == SampleToolType::CROWD)) { setTool(new CrowdTool); } ImGui::Separator(); ImGui::Indent(); if (tool) { tool->handleMenu(); } ImGui::Unindent(); } void Sample_SoloMesh::UI_DrawModeOption(const char* name, const DrawMode drawMode, const bool enabled) { ImGui::BeginDisabled(!enabled); bool checked = currentDrawMode == drawMode; if (ImGui::Checkbox(name, &checked)) { currentDrawMode = drawMode; } ImGui::EndDisabled(); } void Sample_SoloMesh::handleDebugMode() { ImGui::Text("Draw Mode"); UI_DrawModeOption("Input Mesh", DrawMode::MESH, true); UI_DrawModeOption("Navmesh", DrawMode::NAVMESH, navMesh != nullptr); UI_DrawModeOption("Navmesh Invis", DrawMode::NAVMESH_INVIS, navMesh != nullptr); UI_DrawModeOption("Navmesh Trans", DrawMode::NAVMESH_TRANS, navMesh != nullptr); UI_DrawModeOption("Navmesh BVTree", DrawMode::NAVMESH_BVTREE, navMesh != nullptr); UI_DrawModeOption("Navmesh Nodes", DrawMode::NAVMESH_NODES, navQuery != nullptr); UI_DrawModeOption("Voxels", DrawMode::VOXELS, heightfield != nullptr); UI_DrawModeOption("Walkable Voxels", DrawMode::VOXELS_WALKABLE, heightfield != nullptr); UI_DrawModeOption("Compact", DrawMode::COMPACT, compactHeightfield != nullptr); UI_DrawModeOption("Compact Distance", DrawMode::COMPACT_DISTANCE, compactHeightfield != nullptr); UI_DrawModeOption("Compact Regions", DrawMode::COMPACT_REGIONS, compactHeightfield != nullptr); UI_DrawModeOption("Region Connections", DrawMode::REGION_CONNECTIONS, contourSet != nullptr); UI_DrawModeOption("Raw Contours", DrawMode::RAW_CONTOURS, contourSet != nullptr); UI_DrawModeOption("Both Contours", DrawMode::BOTH_CONTOURS, contourSet != nullptr); UI_DrawModeOption("Contours", DrawMode::CONTOURS, contourSet != nullptr); UI_DrawModeOption("Poly Mesh", DrawMode::POLYMESH, polyMesh != nullptr); UI_DrawModeOption("Poly Mesh Detail", DrawMode::POLYMESH_DETAIL, detailMesh != nullptr); } void Sample_SoloMesh::handleRender() { if (!inputGeometry) { return; } glEnable(GL_FOG); glDepthMask(GL_TRUE); const float texScale = 1.0f / (cellSize * 10.0f); if (currentDrawMode != DrawMode::NAVMESH_TRANS) { // Draw mesh duDebugDrawTriMeshSlope( &debugDraw, inputGeometry->verts.data(), inputGeometry->getVertCount(), inputGeometry->tris.data(), inputGeometry->normals.data(), inputGeometry->getTriCount(), agentMaxSlope, texScale); inputGeometry->drawOffMeshConnections(&debugDraw); } glDisable(GL_FOG); glDepthMask(GL_FALSE); // Draw bounds const float* navmeshBoundsMin = inputGeometry->getNavMeshBoundsMin(); const float* navmeshBoundsMax = inputGeometry->getNavMeshBoundsMax(); duDebugDrawBoxWire(&debugDraw, navmeshBoundsMin[0],navmeshBoundsMin[1],navmeshBoundsMin[2], navmeshBoundsMax[0],navmeshBoundsMax[1],navmeshBoundsMax[2], duRGBA(255,255,255,128), 1.0f); debugDraw.begin(DU_DRAW_POINTS, 5.0f); debugDraw.vertex(navmeshBoundsMin[0],navmeshBoundsMin[1],navmeshBoundsMin[2],duRGBA(255,255,255,128)); debugDraw.end(); if (navMesh && navQuery && (currentDrawMode == DrawMode::NAVMESH || currentDrawMode == DrawMode::NAVMESH_TRANS || currentDrawMode == DrawMode::NAVMESH_BVTREE || currentDrawMode == DrawMode::NAVMESH_NODES || currentDrawMode == DrawMode::NAVMESH_INVIS)) { if (currentDrawMode != DrawMode::NAVMESH_INVIS) { duDebugDrawNavMeshWithClosedList(&debugDraw, *navMesh, *navQuery, navMeshDrawFlags); } if (currentDrawMode == DrawMode::NAVMESH_BVTREE) { duDebugDrawNavMeshBVTree(&debugDraw, *navMesh); } if (currentDrawMode == DrawMode::NAVMESH_NODES) { duDebugDrawNavMeshNodes(&debugDraw, *navQuery); } duDebugDrawNavMeshPolysWithFlags(&debugDraw, *navMesh, SAMPLE_POLYFLAGS_DISABLED, duRGBA(0,0,0,128)); } glDepthMask(GL_TRUE); if (compactHeightfield && currentDrawMode == DrawMode::COMPACT) { duDebugDrawCompactHeightfieldSolid(&debugDraw, *compactHeightfield); } if (compactHeightfield && currentDrawMode == DrawMode::COMPACT_DISTANCE) { duDebugDrawCompactHeightfieldDistance(&debugDraw, *compactHeightfield); } if (compactHeightfield && currentDrawMode == DrawMode::COMPACT_REGIONS) { duDebugDrawCompactHeightfieldRegions(&debugDraw, *compactHeightfield); } if (heightfield && currentDrawMode == DrawMode::VOXELS) { glEnable(GL_FOG); duDebugDrawHeightfieldSolid(&debugDraw, *heightfield); glDisable(GL_FOG); } if (heightfield && currentDrawMode == DrawMode::VOXELS_WALKABLE) { glEnable(GL_FOG); duDebugDrawHeightfieldWalkable(&debugDraw, *heightfield); glDisable(GL_FOG); } if (contourSet && currentDrawMode == DrawMode::RAW_CONTOURS) { glDepthMask(GL_FALSE); duDebugDrawRawContours(&debugDraw, *contourSet); glDepthMask(GL_TRUE); } if (contourSet && currentDrawMode == DrawMode::BOTH_CONTOURS) { glDepthMask(GL_FALSE); duDebugDrawRawContours(&debugDraw, *contourSet, 0.5f); duDebugDrawContours(&debugDraw, *contourSet); glDepthMask(GL_TRUE); } if (contourSet && currentDrawMode == DrawMode::CONTOURS) { glDepthMask(GL_FALSE); duDebugDrawContours(&debugDraw, *contourSet); glDepthMask(GL_TRUE); } if (compactHeightfield && contourSet && currentDrawMode == DrawMode::REGION_CONNECTIONS) { duDebugDrawCompactHeightfieldRegions(&debugDraw, *compactHeightfield); glDepthMask(GL_FALSE); duDebugDrawRegionConnections(&debugDraw, *contourSet); glDepthMask(GL_TRUE); } if (polyMesh && currentDrawMode == DrawMode::POLYMESH) { glDepthMask(GL_FALSE); duDebugDrawPolyMesh(&debugDraw, *polyMesh); glDepthMask(GL_TRUE); } if (detailMesh && currentDrawMode == DrawMode::POLYMESH_DETAIL) { glDepthMask(GL_FALSE); duDebugDrawPolyMeshDetail(&debugDraw, *detailMesh); glDepthMask(GL_TRUE); } inputGeometry->drawConvexVolumes(&debugDraw); if (tool) { tool->handleRender(); } renderToolStates(); glDepthMask(GL_TRUE); } void Sample_SoloMesh::handleRenderOverlay(double* proj, double* model, int* view) { if (tool) { tool->handleRenderOverlay(proj, model, view); } renderOverlayToolStates(proj, model, view); } void Sample_SoloMesh::handleMeshChanged(InputGeom* geom) { Sample::handleMeshChanged(geom); dtFreeNavMesh(navMesh); navMesh = 0; if (tool) { tool->reset(); tool->init(this); } resetToolStates(); initToolStates(this); } bool Sample_SoloMesh::handleBuild() { if (!inputGeometry || inputGeometry->verts.empty()) { buildContext->log(RC_LOG_ERROR, "buildNavigation: Input mesh is not specified."); return false; } cleanup(); const float* boundsMin = inputGeometry->getNavMeshBoundsMin(); const float* boundsMax = inputGeometry->getNavMeshBoundsMax(); const float* verts = inputGeometry->verts.data(); const int numVerts = static_cast(inputGeometry->verts.size()) / 3; const int* tris = inputGeometry->tris.data(); const int numTris = static_cast(inputGeometry->tris.size()) / 3; // // Step 1. Initialize build config. // // Init build configuration from GUI memset(&config, 0, sizeof(config)); config.cs = cellSize; config.ch = cellHeight; config.walkableSlopeAngle = agentMaxSlope; config.walkableHeight = static_cast(ceilf(agentHeight / config.ch)); config.walkableClimb = static_cast(floorf(agentMaxClimb / config.ch)); config.walkableRadius = static_cast(ceilf(agentRadius / config.cs)); config.maxEdgeLen = static_cast(edgeMaxLen / cellSize); config.maxSimplificationError = edgeMaxError; config.minRegionArea = static_cast(rcSqr(regionMinSize)); // Note: area = size*size config.mergeRegionArea = static_cast(rcSqr(regionMergeSize)); // Note: area = size*size config.maxVertsPerPoly = vertsPerPoly; config.detailSampleDist = detailSampleDist < 0.9f ? 0 : cellSize * detailSampleDist; config.detailSampleMaxError = cellHeight * detailSampleMaxError; // Set the area where the navigation will be built. // Here the bounds of the input mesh are used, but the // area could be specified by a user defined box, etc. rcVcopy(config.bmin, boundsMin); rcVcopy(config.bmax, boundsMax); rcCalcGridSize(config.bmin, config.bmax, config.cs, &config.width, &config.height); // Reset build times gathering. buildContext->resetTimers(); buildContext->startTimer(RC_TIMER_TOTAL); // Start the build process. buildContext->log(RC_LOG_PROGRESS, "Building navigation:"); buildContext->log(RC_LOG_PROGRESS, " - %d x %d cells", config.width, config.height); buildContext->log(RC_LOG_PROGRESS, " - %.1fK verts, %.1fK tris", static_cast(numVerts) / 1000.0f, static_cast(numTris) / 1000.0f); // // Step 2. Rasterize input meshes. // // Allocate voxel heightfield where we will store our rasterized input data. heightfield = rcAllocHeightfield(); if (!heightfield) { buildContext->log(RC_LOG_ERROR, "buildNavigation: Out of memory 'heightfield'."); return false; } if (!rcCreateHeightfield(buildContext, *heightfield, config.width, config.height, config.bmin, config.bmax, config.cs, config.ch)) { buildContext->log(RC_LOG_ERROR, "buildNavigation: Could not create solid heightfield."); return false; } // Allocate array that can hold triangle area types. // This is used to store terrain type information and to mark // triangles as unwalkable. // If you have multiple meshes you need to process, allocate // an array which can hold the max number of triangles you need to process. triareas = new unsigned char[numTris]; if (!triareas) { buildContext->log(RC_LOG_ERROR, "buildNavigation: Out of memory 'm_triareas' (%d).", numTris); return false; } memset(triareas, 0, numTris * sizeof(unsigned char)); // Record which triangles in the input mesh are walkable. // This information is recorded in m_triareas rcMarkWalkableTriangles(buildContext, config.walkableSlopeAngle, verts, numVerts, tris, numTris, triareas); // Rasterize the input mesh // If your have multiple meshes, you can transform them, calculate the // terrain type for each mesh and rasterize them here. if (!rcRasterizeTriangles(buildContext, verts, numVerts, tris, triareas, numTris, *heightfield, config.walkableClimb)) { buildContext->log(RC_LOG_ERROR, "buildNavigation: Could not rasterize triangles."); return false; } // // Step 3. Filter walkable surfaces. // // Once all geometry is rasterized, we do initial pass of filtering to // remove unwanted overhangs caused by the conservative rasterization // as well as spans where the character cannot possibly stand. if (filterLowHangingObstacles) { rcFilterLowHangingWalkableObstacles(buildContext, config.walkableClimb, *heightfield); } if (filterLedgeSpans) { rcFilterLedgeSpans(buildContext, config.walkableHeight, config.walkableClimb, *heightfield); } if (filterWalkableLowHeightSpans) { rcFilterWalkableLowHeightSpans(buildContext, config.walkableHeight, *heightfield); } // // Step 4. Partition walkable surface into simple regions. // // Compact the heightfield so that it is faster to work with. // This will result more cache coherent data. This step will also // generate neighbor connection information between walkable cells. compactHeightfield = rcAllocCompactHeightfield(); if (!compactHeightfield) { buildContext->log(RC_LOG_ERROR, "buildNavigation: Out of memory 'chf'."); return false; } if (!rcBuildCompactHeightfield(buildContext, config.walkableHeight, config.walkableClimb, *heightfield, *compactHeightfield)) { buildContext->log(RC_LOG_ERROR, "buildNavigation: Could not build compact data."); return false; } // Erode the walkable area by agent radius. // This allows us to path an agent through the navmesh as if it was a single point if (!rcErodeWalkableArea(buildContext, config.walkableRadius, *compactHeightfield)) { buildContext->log(RC_LOG_ERROR, "buildNavigation: Could not erode."); return false; } // (Optional) Marks the surface type of voxels in an area defined by a convex volume. // Useful to mark areas of differing cost. const ConvexVolume* vols = inputGeometry->getConvexVolumes(); for (int i = 0; i < inputGeometry->getConvexVolumeCount(); ++i) { rcMarkConvexPolyArea(buildContext, vols[i].verts, vols[i].nverts, vols[i].hmin, vols[i].hmax, (unsigned char)vols[i].area, *compactHeightfield); } // Partition the heightfield into contiguous regions that will each be // triangulated into navigation polygons. // // There are 3 partitioning methods, each with their own pros and cons: // 1) Watershed partitioning // - the classic Recast partitioning // - creates the nicest tessellation // - usually slowest // - the are some corner cases where this method creates holes and // overlaps in the resulting region data. // - holes may appear when a small obstacle is close to a large open // area. This will not cause triangulation to fail. // - overlaps may occur if you have narrow spiral corridors // e.g. spiral stairs. This will cause triangulation to fail. // * Generally the best choice if you are precompute the navmesh and/or // there are large open areas in the input geometry. // 2) Monotone partitioning // - fastest // - guaranteed to partition the heightfield into regions without holes // or overlaps // - Can create long, thin polygons which sometimes cause paths with detours // * Use this if you want fast navmesh generation // 3) Layer partitioning // - quite fast // - partitions the heighfield into non-overlapping regions // - relies on the triangulation code to cope with holes, which makes // this slower than monotone partitioning // - produces better triangles than monotone partitioning // - does not have the corner cases of watershed partitioning // - can be slow and create a slightly ugly tessellation (still better // than monotone) if you have large open areas with small obstacles. // This is less of a problem if you use a tiled navmesh. // * A good choice for a tiled navmesh with small to medium-sized tiles if (partitionType == SAMPLE_PARTITION_WATERSHED) { // Prepare for region partitioning, by calculating distance field along the walkable surface. if (!rcBuildDistanceField(buildContext, *compactHeightfield)) { buildContext->log(RC_LOG_ERROR, "buildNavigation: Could not build distance field."); return false; } // Partition the walkable surface into contiguous regions. if (!rcBuildRegions(buildContext, *compactHeightfield, 0, config.minRegionArea, config.mergeRegionArea)) { buildContext->log(RC_LOG_ERROR, "buildNavigation: Could not build watershed regions."); return false; } } else if (partitionType == SAMPLE_PARTITION_MONOTONE) { // Partition the walkable surface into contiguous regions. // Monotone partitioning does not need distancefield. if (!rcBuildRegionsMonotone(buildContext, *compactHeightfield, 0, config.minRegionArea, config.mergeRegionArea)) { buildContext->log(RC_LOG_ERROR, "buildNavigation: Could not build monotone regions."); return false; } } else // SAMPLE_PARTITION_LAYERS { // Partition the walkable surface into contiguous regions. // Layer partitioning does not need distancefield. if (!rcBuildLayerRegions(buildContext, *compactHeightfield, 0, config.minRegionArea)) { buildContext->log(RC_LOG_ERROR, "buildNavigation: Could not build layer regions."); return false; } } // // Step 5. Trace and simplify region contours. // // Create contour. contourSet = rcAllocContourSet(); if (!contourSet) { buildContext->log(RC_LOG_ERROR, "buildNavigation: Out of memory 'cset'."); return false; } if (!rcBuildContours(buildContext, *compactHeightfield, config.maxSimplificationError, config.maxEdgeLen, *contourSet)) { buildContext->log(RC_LOG_ERROR, "buildNavigation: Could not create contours."); return false; } // // Step 6. Triangulate contours to build navmesh polygons. // polyMesh = rcAllocPolyMesh(); if (!polyMesh) { buildContext->log(RC_LOG_ERROR, "buildNavigation: Out of memory 'pmesh'."); return false; } if (!rcBuildPolyMesh(buildContext, *contourSet, config.maxVertsPerPoly, *polyMesh)) { buildContext->log(RC_LOG_ERROR, "buildNavigation: Could not triangulate contours."); return false; } // // Step 7. Create a navmesh from the triangulated polygons. // // Calculates additional information necessary to run pathing queries. // detailMesh = rcAllocPolyMeshDetail(); if (!detailMesh) { buildContext->log(RC_LOG_ERROR, "buildNavigation: Out of memory 'pmdtl'."); return false; } if (!rcBuildPolyMeshDetail(buildContext, *polyMesh, *compactHeightfield, config.detailSampleDist, config.detailSampleMaxError, *detailMesh)) { buildContext->log(RC_LOG_ERROR, "buildNavigation: Could not build detail mesh."); return false; } // At this point the navigation mesh data is ready to use. // See duDebugDrawPolyMesh or dtCreateNavMeshData as examples how to access // the navmesh data. // // (Optional) Step 8. Create Detour data from Recast poly mesh. // // The GUI may allow more max points per polygon than Detour can handle. // Only build the detour navmesh if we do not exceed the limit. if (config.maxVertsPerPoly <= DT_VERTS_PER_POLYGON) { unsigned char* navData = 0; int navDataSize = 0; // Update poly flags from areas. for (int i = 0; i < polyMesh->npolys; ++i) { if (polyMesh->areas[i] == RC_WALKABLE_AREA) { polyMesh->areas[i] = SAMPLE_POLYAREA_GROUND; } if (polyMesh->areas[i] == SAMPLE_POLYAREA_GROUND || polyMesh->areas[i] == SAMPLE_POLYAREA_GRASS || polyMesh->areas[i] == SAMPLE_POLYAREA_ROAD) { polyMesh->flags[i] = SAMPLE_POLYFLAGS_WALK; } else if (polyMesh->areas[i] == SAMPLE_POLYAREA_WATER) { polyMesh->flags[i] = SAMPLE_POLYFLAGS_SWIM; } else if (polyMesh->areas[i] == SAMPLE_POLYAREA_DOOR) { polyMesh->flags[i] = SAMPLE_POLYFLAGS_WALK | SAMPLE_POLYFLAGS_DOOR; } } dtNavMeshCreateParams params; memset(¶ms, 0, sizeof(params)); params.verts = polyMesh->verts; params.vertCount = polyMesh->nverts; params.polys = polyMesh->polys; params.polyAreas = polyMesh->areas; params.polyFlags = polyMesh->flags; params.polyCount = polyMesh->npolys; params.nvp = polyMesh->nvp; params.detailMeshes = detailMesh->meshes; params.detailVerts = detailMesh->verts; params.detailVertsCount = detailMesh->nverts; params.detailTris = detailMesh->tris; params.detailTriCount = detailMesh->ntris; params.offMeshConVerts = inputGeometry->getOffMeshConnectionVerts(); params.offMeshConRad = inputGeometry->getOffMeshConnectionRads(); params.offMeshConDir = inputGeometry->getOffMeshConnectionDirs(); params.offMeshConAreas = inputGeometry->getOffMeshConnectionAreas(); params.offMeshConFlags = inputGeometry->getOffMeshConnectionFlags(); params.offMeshConUserID = inputGeometry->getOffMeshConnectionId(); params.offMeshConCount = inputGeometry->getOffMeshConnectionCount(); params.walkableHeight = agentHeight; params.walkableRadius = agentRadius; params.walkableClimb = agentMaxClimb; rcVcopy(params.bmin, polyMesh->bmin); rcVcopy(params.bmax, polyMesh->bmax); params.cs = config.cs; params.ch = config.ch; params.buildBvTree = true; if (!dtCreateNavMeshData(¶ms, &navData, &navDataSize)) { buildContext->log(RC_LOG_ERROR, "Could not build Detour navmesh."); return false; } navMesh = dtAllocNavMesh(); if (!navMesh) { dtFree(navData); buildContext->log(RC_LOG_ERROR, "Could not create Detour navmesh"); return false; } dtStatus status = navMesh->init(navData, navDataSize, DT_TILE_FREE_DATA); if (dtStatusFailed(status)) { dtFree(navData); buildContext->log(RC_LOG_ERROR, "Could not init Detour navmesh"); return false; } status = navQuery->init(navMesh, 2048); if (dtStatusFailed(status)) { buildContext->log(RC_LOG_ERROR, "Could not init Detour navmesh query"); return false; } } // Stop build timers buildContext->stopTimer(RC_TIMER_TOTAL); auto totalTime = buildContext->getAccumulatedTime(RC_TIMER_TOTAL); totalBuildTimeMs = static_cast(totalTime) / 1000.0f; // Show performance stats. duLogBuildTimes(*buildContext, totalTime); buildContext->log(RC_LOG_PROGRESS, ">> Polymesh: %d vertices %d polygons", polyMesh->nverts, polyMesh->npolys); if (tool) { tool->init(this); } initToolStates(this); return true; }