diff --git a/src/Draw/TKViewerTest/ViewerTest/ViewerTest_ViewerCommands.cxx b/src/Draw/TKViewerTest/ViewerTest/ViewerTest_ViewerCommands.cxx index 3c086b5191..dbdce83c47 100644 --- a/src/Draw/TKViewerTest/ViewerTest/ViewerTest_ViewerCommands.cxx +++ b/src/Draw/TKViewerTest/ViewerTest/ViewerTest_ViewerCommands.cxx @@ -20,6 +20,8 @@ #include +#include +#include #include #include #include @@ -5002,8 +5004,14 @@ static int VGrid(Draw_Interpretor& /*theDI*/, int theArgNb, const char** theArgV Aspect_GridType aType = aViewer->GridType(); Aspect_GridDrawMode aMode = aViewer->GridDrawMode(); NCollection_Vec2 aNewOriginXY, aNewStepXY, aNewSizeXY; - double aNewRotAngle = 0.0, aNewZOffset = 0.0; - bool hasOrigin = false, hasStep = false, hasRotAngle = false, hasSize = false, hasZOffset = false; + double aNewRadius = 0.0, aNewRotAngle = 0.0, aNewZOffset = 0.0; + double aNewArcStart = 0.0, aNewArcEnd = 0.0; + Quantity_Color aNewColor, aNewTenthColor; + bool hasOrigin = false, hasStep = false, hasRotAngle = false, hasSize = false, hasRadius = false, + hasZOffset = false; + bool isInfinite = false, hasInfOff = false, hasScale = false, hasArc = false; + bool hasColor = false, hasTenthColor = false; + Aspect_GridParams aGridParams; ViewerTest_AutoUpdater anUpdateTool(ViewerTest::GetAISContext(), aView); for (int anArgIter = 1; anArgIter < theArgNb; ++anArgIter) { @@ -5025,6 +5033,10 @@ static int VGrid(Draw_Interpretor& /*theDI*/, int theArgNb, const char** theArgV { aType = Aspect_GT_Circular; } + else if (anArgNext == "inf" || anArgNext == "infinite") + { + isInfinite = true; + } else { Message::SendFail() << "Syntax error at '" << anArgNext << "'"; @@ -5082,10 +5094,10 @@ static int VGrid(Draw_Interpretor& /*theDI*/, int theArgNb, const char** theArgV } else if (anArgIter + 1 < theArgNb && anArg == "-radius") { - hasSize = true; + hasRadius = true; ++anArgIter; - aNewSizeXY.SetValues(Draw::Atof(theArgVec[anArgIter]), 0.0); - if (aNewStepXY.x() <= 0.0) + aNewRadius = Draw::Atof(theArgVec[anArgIter]); + if (aNewRadius <= 0.0) { Message::SendFail() << "Syntax error: wrong size '" << theArgVec[anArgIter] << "'"; return 1; @@ -5096,7 +5108,7 @@ static int VGrid(Draw_Interpretor& /*theDI*/, int theArgNb, const char** theArgV hasSize = true; aNewSizeXY.SetValues(Draw::Atof(theArgVec[anArgIter + 1]), Draw::Atof(theArgVec[anArgIter + 2])); - if (aNewStepXY.x() <= 0.0 || aNewStepXY.y() <= 0.0) + if (aNewSizeXY.x() <= 0.0 || aNewSizeXY.y() <= 0.0) { Message::SendFail() << "Syntax error: wrong size '" << theArgVec[anArgIter + 1] << " " << theArgVec[anArgIter + 2] << "'"; @@ -5104,6 +5116,59 @@ static int VGrid(Draw_Interpretor& /*theDI*/, int theArgNb, const char** theArgV } anArgIter += 2; } + else if (anArgIter + 3 < theArgNb && (anArg == "-color")) + { + hasColor = true; + aNewColor = Quantity_Color(Draw::Atof(theArgVec[anArgIter + 1]), + Draw::Atof(theArgVec[anArgIter + 2]), + Draw::Atof(theArgVec[anArgIter + 3]), + Quantity_TOC_RGB); + aGridParams.SetColor(aNewColor); + anArgIter += 3; + } + else if (anArgIter + 3 < theArgNb && (anArg == "-tenthcolor" || anArg == "-accentcolor")) + { + hasTenthColor = true; + aNewTenthColor = Quantity_Color(Draw::Atof(theArgVec[anArgIter + 1]), + Draw::Atof(theArgVec[anArgIter + 2]), + Draw::Atof(theArgVec[anArgIter + 3]), + Quantity_TOC_RGB); + aGridParams.SetAccentColor(aNewTenthColor); + anArgIter += 3; + } + else if (anArgIter + 1 < theArgNb && anArg == "-scale") + { + hasScale = true; + aGridParams.SetScale(Draw::Atof(theArgVec[++anArgIter])); + } + else if (anArgIter + 1 < theArgNb && (anArg == "-linethickness" || anArg == "-thickness")) + { + aGridParams.SetLineThickness(Draw::Atof(theArgVec[++anArgIter])); + } + else if (anArgIter + 1 < theArgNb && anArg == "-background") + { + const int aVal = Draw::Atoi(theArgVec[++anArgIter]); + aGridParams.SetIsBackground(aVal != 0); + } + else if (anArgIter + 1 < theArgNb && anArg == "-drawaxis") + { + const int aVal = Draw::Atoi(theArgVec[++anArgIter]); + aGridParams.SetIsDrawAxis(aVal != 0); + } + else if (anArgIter + 1 < theArgNb && (anArg == "-inf" || anArg == "-infinity")) + { + const int aVal = Draw::Atoi(theArgVec[++anArgIter]); + aGridParams.SetIsInfinity(aVal != 0); + } + else if (anArgIter + 2 < theArgNb && anArg == "-arc") + { + // Angular range for circular grids (radians). Equal start/end = full circle. + hasArc = true; + aNewArcStart = Draw::Atof(theArgVec[anArgIter + 1]); + aNewArcEnd = Draw::Atof(theArgVec[anArgIter + 2]); + aGridParams.SetArcRange(aNewArcStart, aNewArcEnd); + anArgIter += 2; + } else if (anArg == "r" || anArg == "rect" || anArg == "rectangular") { aType = Aspect_GT_Rectangular; @@ -5120,10 +5185,9 @@ static int VGrid(Draw_Interpretor& /*theDI*/, int theArgNb, const char** theArgV { aMode = Aspect_GDM_Points; } - else if (anArgIter + 1 >= theArgNb && anArg == "off") + else if (anArg == "off") { - aViewer->DeactivateGrid(); - return 0; + hasInfOff = true; } else { @@ -5132,6 +5196,119 @@ static int VGrid(Draw_Interpretor& /*theDI*/, int theArgNb, const char** theArgV } } + if (isInfinite && hasInfOff) + { + Message::SendFail("Syntax error: 'off' cannot be combined with '-type inf'"); + return 1; + } + + if (isInfinite || hasInfOff) + { + if (hasInfOff) + { + aView->GridErase(); + } + if (isInfinite) + { + // An infinite grid needs a real Aspect_Grid to back snap selection, so we + // route through ActivateGrid(rect|circ) first and override the display + // with the inf-specific params afterwards. Decide the shape from aType + // (user explicitly asked) or from the presence of circular-only options. + const bool isInfCircular = aType == Aspect_GT_Circular || hasRadius || hasArc; + const double anOrigX = hasOrigin ? aNewOriginXY.x() : 0.0; + const double anOrigY = hasOrigin ? aNewOriginXY.y() : 0.0; + const double aRotAngle = hasRotAngle ? aNewRotAngle : 0.0; + + if (isInfCircular) + { + // Radial step + angular divisions. `-step R N` supplies both; otherwise + // use sensible defaults that keep snap consistent with the visible grid. + double aRadiusStep = 1.0; + int aDivisionCount = 16; + if (hasStep) + { + aRadiusStep = aNewStepXY.x(); + aDivisionCount = int(aNewStepXY.y() > 0 ? aNewStepXY.y() : aDivisionCount); + } + else if (hasScale && aGridParams.Scale() > 0.0) + { + aRadiusStep = 1.0 / aGridParams.Scale(); + } + aViewer->SetCircularGridValues(anOrigX, anOrigY, aRadiusStep, aDivisionCount, aRotAngle); + // Arc range reaches the circular grid base before ActivateGrid's first + // syncViews fires (otherwise syncViews would overwrite our override). + if (hasArc) + { + if (occ::handle aCircGrid = + occ::down_cast(aViewer->Grid(true))) + { + aCircGrid->SetArcRange(aNewArcStart, aNewArcEnd); + } + } + aViewer->ActivateGrid(Aspect_GT_Circular, aMode); + + aGridParams.SetScale(1.0 / aRadiusStep); + aGridParams.SetScaleY(0.0); // unused in circular mode + aGridParams.SetAngularDivisions(aDivisionCount); + } + else + { + // Rectangular infinite grid. Derive step from explicit -step or from the + // Aspect_GridParams scale; fall back to 1 world unit so the default is + // immediately useful (Aspect_GridParams::Scale defaults to 0.01 which + // would give step 100 - too coarse for typical scenes). + if (!hasScale) + { + if (hasStep) + { + aGridParams.SetScale(1.0 / aNewStepXY.x()); + aGridParams.SetScaleY(1.0 / aNewStepXY.y()); + } + else + { + aGridParams.SetScale(1.0); + aGridParams.SetScaleY(1.0); + } + } + const double aInfStepX = 1.0 / aGridParams.Scale(); + const double aInfStepY = 1.0 / aGridParams.EffectiveScaleY(); + aViewer->SetRectangularGridValues(anOrigX, anOrigY, aInfStepX, aInfStepY, aRotAngle); + aViewer->ActivateGrid(Aspect_GT_Rectangular, aMode); + + aGridParams.SetAngularDivisions(0); + } + + // Convert origin to the same world-offset convention used by V3d + // syncViews so the shader's aPlaneOrigin matches snap's aPnt0. + const gp_Ax3 aPlane = aViewer->PrivilegedPlane(); + const gp_XYZ aOriginOffset = + aPlane.XDirection().XYZ() * -anOrigX + aPlane.YDirection().XYZ() * -anOrigY; + aGridParams.SetOrigin(gp_Pnt(aOriginOffset)); + aGridParams.SetDrawMode(aMode); + aGridParams.SetRotationAngle(aRotAngle); + if (hasSize && !isInfCircular) + { + aGridParams.SetSizeX(aNewSizeXY.x()); + aGridParams.SetSizeY(aNewSizeXY.y()); + } + if (hasRadius) + { + aGridParams.SetRadius(aNewRadius); + } + if (hasZOffset) + { + aGridParams.SetZOffset(aNewZOffset); + } + aView->GridDisplay(aGridParams); + } + if (hasInfOff && !isInfinite) + { + // plain 'vgrid off' still deactivates the classical grid + aViewer->DeactivateGrid(); + } + return 0; + } + if (aType == Aspect_GT_Rectangular) { NCollection_Vec2 anOrigXY, aStepXY; @@ -5205,18 +5382,18 @@ static int VGrid(Draw_Interpretor& /*theDI*/, int theArgNb, const char** theArgV aRadiusStep, aDivisionNumber, aRotAngle); - if (hasSize || hasZOffset) + if (hasSize) + { + Message::SendFail("Syntax error: circular size should be specified as radius"); + return 1; + } + if (hasRadius || hasZOffset) { double aRadius = 0.0, aZOffset = 0.0; aViewer->CircularGridGraphicValues(aRadius, aZOffset); - if (hasSize) + if (hasRadius) { - aRadius = aNewSizeXY.x(); - if (aNewSizeXY.y() != 0.0) - { - Message::SendFail("Syntax error: circular size should be specified as radius"); - return 1; - } + aRadius = aNewRadius; } if (hasZOffset) { @@ -5224,6 +5401,37 @@ static int VGrid(Draw_Interpretor& /*theDI*/, int theArgNb, const char** theArgV } aViewer->SetCircularGridGraphicValues(aRadius, aZOffset); } + // Angular range must hit the Aspect_CircularGrid base before ActivateGrid + // fires syncViews - syncViews copies AngleStart/End from the grid, so any + // value set earlier only on aGridParams would get overwritten. + if (hasArc) + { + if (occ::handle aCircGrid = + occ::down_cast(aViewer->Grid(true))) + { + aCircGrid->SetArcRange(aNewArcStart, aNewArcEnd); + } + } + } + // Apply -color / -tenthColor to the active grid so V3d syncViews picks + // them up on the next display. Raw -type inf users still drive these + // through aGridParams directly (set earlier in the parser loop). + if (hasColor || hasTenthColor) + { + if (occ::handle aGrid = aViewer->Grid(true)) + { + Quantity_Color aMainColor, aTenthColor; + aGrid->Colors(aMainColor, aTenthColor); + if (hasColor) + { + aMainColor = aNewColor; + } + if (hasTenthColor) + { + aTenthColor = aNewTenthColor; + } + aGrid->SetColors(aMainColor, aTenthColor); + } } aViewer->ActivateGrid(aType, aMode); return 0; @@ -13951,9 +14159,16 @@ vlayerline x1 y1 x2 y2 [linewidth=0.5] [linetype=0] [transparency=1.0] )" /* [vlayerline] */); addCmd("vgrid", VGrid, /* [vgrid] */ R"( -vgrid [off] [-type {rect|circ}] [-mode {line|point}] [-origin X Y] [-rotAngle Angle] [-zoffset DZ] +vgrid [off] [-type {rect|circ|inf}] [-mode {line|point}] [-origin X Y] [-rotAngle Angle] [-zoffset DZ] [-step X Y] [-size DX DY] - [-step StepRadius NbDivisions] [-radius Radius] + [-step StepRadius NbDivisions] [-radius Radius] [-arc AngleStart AngleEnd] + [-color R G B] [-tenthColor R G B] [-scale N] [-lineThickness T] + [-background {0|1}] [-drawAxis {0|1}] [-inf {0|1}] +'-type inf' activates a shader-rendered infinite (or bounded) grid; combine +with '-size DX DY' for a rectangle, '-radius R' for a disc, '-arc S E' for a +wedge. '-color', '-tenthColor', '-scale', '-lineThickness' drive every grid +since rendering is shader-based. '-background', '-drawAxis', '-inf {0|1}' are +inf-mode only. 'off' deactivates the grid and cannot be combined with '-type inf'. )" /* [vgrid] */); addCmd("vpriviledgedplane", VPriviledgedPlane, /* [vpriviledgedplane] */ R"( diff --git a/src/Visualization/TKOpenGl/OpenGl/OpenGl_ShaderManager.cxx b/src/Visualization/TKOpenGl/OpenGl/OpenGl_ShaderManager.cxx index 942ccd0d04..796cccc4f8 100644 --- a/src/Visualization/TKOpenGl/OpenGl/OpenGl_ShaderManager.cxx +++ b/src/Visualization/TKOpenGl/OpenGl/OpenGl_ShaderManager.cxx @@ -174,6 +174,7 @@ void OpenGl_ShaderManager::clear() myBlitPrograms[1].Init(occ::handle()); myBoundBoxProgram.Nullify(); myBoundBoxVertBuffer.Nullify(); + myGridProgram.Nullify(); for (int aModeIter = 0; aModeIter < Graphic3d_StereoMode_NB; ++aModeIter) { myStereoPrograms[aModeIter].Nullify(); @@ -1476,6 +1477,36 @@ const occ::handle& OpenGl_ShaderManager::GetColoredQuad //================================================================================================= +bool OpenGl_ShaderManager::BindGridProgram() +{ + if (myGridProgram.IsNull()) + { + occ::handle aProgramSrc = getGridProgram(); + TCollection_AsciiString aKey; + if (!Create(aProgramSrc, aKey, myGridProgram)) + { + myContext->PushMessage(GL_DEBUG_SOURCE_APPLICATION, + GL_DEBUG_TYPE_ERROR, + 0, + GL_DEBUG_SEVERITY_HIGH, + "Error: infinite-grid shader failed to compile/link"); + myGridProgram = new OpenGl_ShaderProgram(); // mark as invalid so we don't retry every frame + return false; + } + } + if (!myGridProgram->IsValid()) + { + return false; + } + // Route through bindProgramWithState so OCCT built-in uniforms (occProjectionMatrix, + // occWorldViewMatrix, occModelWorldMatrix, their inverses, viewport, etc.) are uploaded + // to the grid program. The shader's unproject() path depends on these matrices; without + // PushState they stay at zero and every fragment is discarded (the grid never draws). + return bindProgramWithState(myGridProgram, Graphic3d_TypeOfShadingModel_Unlit); +} + +//================================================================================================= + bool OpenGl_ShaderManager::bindProgramWithState(const occ::handle& theProgram, Graphic3d_TypeOfShadingModel theShadingModel) { diff --git a/src/Visualization/TKOpenGl/OpenGl/OpenGl_ShaderManager.hxx b/src/Visualization/TKOpenGl/OpenGl/OpenGl_ShaderManager.hxx index 06403ff05d..43bd7a5b23 100644 --- a/src/Visualization/TKOpenGl/OpenGl/OpenGl_ShaderManager.hxx +++ b/src/Visualization/TKOpenGl/OpenGl/OpenGl_ShaderManager.hxx @@ -254,6 +254,10 @@ public: //! Generates shader program to render correctly colored quad. Standard_EXPORT const occ::handle& GetColoredQuadProgram(); + //! Compile (once) and bind the infinite-grid shader program. + //! Returns FALSE if shader compilation fails or the GAPI is not supported. + Standard_EXPORT bool BindGridProgram(); + //! Resets PBR shading models to corresponding non-PBR ones if PBR is not allowed. static Graphic3d_TypeOfShadingModel PBRShadingModelFallback( Graphic3d_TypeOfShadingModel theShadingModel, @@ -803,6 +807,7 @@ protected: occ::handle myBgCubeMapProgram; //!< program for background cubemap rendering occ::handle myBgSkydomeProgram; //!< program for background cubemap rendering occ::handle myColoredQuadProgram; //!< program for correct quad rendering + occ::handle myGridProgram; //!< shader program for infinite grid occ::handle myStereoPrograms[Graphic3d_StereoMode_NB]; //!< standard stereo programs diff --git a/src/Visualization/TKOpenGl/OpenGl/OpenGl_View.cxx b/src/Visualization/TKOpenGl/OpenGl/OpenGl_View.cxx index 9a2aa9c7e2..8f47bcf78c 100644 --- a/src/Visualization/TKOpenGl/OpenGl/OpenGl_View.cxx +++ b/src/Visualization/TKOpenGl/OpenGl/OpenGl_View.cxx @@ -15,12 +15,15 @@ #include +#include + #include #include #include #include #include #include +#include #include #include #include @@ -28,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -134,6 +138,8 @@ OpenGl_View::OpenGl_View(const occ::handle& theMgr, myTextureParams(new OpenGl_Aspects()), myCubeMapParams(new OpenGl_Aspects()), myColoredQuadParams(new OpenGl_Aspects()), + myGridVao(0), + myToShowGrid(false), myPBREnvState(OpenGl_PBREnvState_NONEXISTENT), myPBREnvRequest(false), // ray-tracing fields initialization @@ -275,6 +281,13 @@ void OpenGl_View::ReleaseGlResources(const occ::handle& theCtx) { myPBREnvironment->Release(theCtx.get()); } + + if (myGridVao != 0 && !theCtx.IsNull() && theCtx->core30 != nullptr) + { + theCtx->core30->glDeleteVertexArrays(1, &myGridVao); + } + myGridVao = 0; + ReleaseXR(); } @@ -2580,6 +2593,12 @@ void OpenGl_View::render(Graphic3d_Camera::Projection theProjection, myWorkspace->SetEnvironmentTexture(occ::handle()); + // Render shader-based infinite grid on top of opaque scene, before trihedron. + if (!theToDrawImmediate) + { + renderGrid(); + } + // =============================== // Step 4: Trihedron // =============================== @@ -3545,3 +3564,363 @@ void OpenGl_View::updatePBREnvironment(const occ::handle& theCtx aGlTextureSet.Nullify(); OpenGl_Element::Destroy(theCtx.get(), aTmpGlAspects); } + +//================================================================================================= + +void OpenGl_View::GridDisplay(const Aspect_GridParams& theParams, const gp_Ax3& thePlane) +{ + // Reference view matrix (for background-mode anchoring) is captured only on + // transitions into background mode or into the showing state. Otherwise live + // param edits (SetArcRange, SetSize, etc.) while the camera is mid-orbit would + // re-snapshot the current view and visually snap the anchored grid. + const bool wasShowing = myToShowGrid; + const bool wasBackground = wasShowing && myGridParams.IsBackground(); + const bool toCapture = theParams.IsBackground() && (!wasShowing || !wasBackground); + + myGridParams = theParams; + myGridPlane = thePlane; + myToShowGrid = true; + + if (toCapture) + { + const occ::handle& aCtx = myWorkspace->GetGlContext(); + if (!aCtx.IsNull()) + { + myGridRefViewMatrix = aCtx->WorldViewState.Current(); + } + } +} + +//================================================================================================= + +void OpenGl_View::GridErase() +{ + myToShowGrid = false; +} + +//================================================================================================= + +void OpenGl_View::renderGrid() +{ + if (!myToShowGrid || myGridParams.DrawMode() == Aspect_GDM_None) + { + return; + } + + const occ::handle& aContext = myWorkspace->GetGlContext(); + if (aContext.IsNull()) + { + return; + } + if (aContext->core30 == nullptr) + { + // The shader grid requires GL 3.0+ / GLES 3.0+ (VAO + gl_VertexID + gl_FragDepth). + // Warn once per process so the caller knows why the grid isn't drawn; snap + // math still works. The process scope avoids per-view state bloat - a stray + // missed warning on a second view is less costly than a data member. + static bool THE_GRID_GL30_WARNED = false; + if (!THE_GRID_GL30_WARNED) + { + THE_GRID_GL30_WARNED = true; + aContext->PushMessage(GL_DEBUG_SOURCE_APPLICATION, + GL_DEBUG_TYPE_OTHER, + 0, + GL_DEBUG_SEVERITY_MEDIUM, + "Warning: shader-based grid requires GL 3.0 / GLES 3.0 or later; " + "grid will not be rendered on this driver. Snap selection remains " + "functional."); + } + return; + } + + const occ::handle& aCamera = aContext->Camera(); + if (aCamera.IsNull()) + { + return; + } + + if (myGridVao == 0) + { + aContext->core30->glGenVertexArrays(1, &myGridVao); + if (myGridVao == 0) + { + myToShowGrid = false; + return; + } + } + + GLint aPrevVao = 0; + aContext->core11fwd->glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &aPrevVao); + + GLboolean wasDepthTest = aContext->core11fwd->glIsEnabled(GL_DEPTH_TEST); + GLboolean wasBlend = aContext->core11fwd->glIsEnabled(GL_BLEND); + GLboolean wasCullFace = aContext->core11fwd->glIsEnabled(GL_CULL_FACE); + GLboolean wasDepthWrite = GL_TRUE; + aContext->core11fwd->glGetBooleanv(GL_DEPTH_WRITEMASK, &wasDepthWrite); + GLint aPrevDepthFunc = GL_LESS; + aContext->core11fwd->glGetIntegerv(GL_DEPTH_FUNC, &aPrevDepthFunc); + GLint aPrevBlendSrcRgb = GL_ONE, aPrevBlendDstRgb = GL_ZERO; + GLint aPrevBlendSrcA = GL_ONE, aPrevBlendDstA = GL_ZERO; + aContext->core11fwd->glGetIntegerv(GL_BLEND_SRC_RGB, &aPrevBlendSrcRgb); + aContext->core11fwd->glGetIntegerv(GL_BLEND_DST_RGB, &aPrevBlendDstRgb); + aContext->core11fwd->glGetIntegerv(GL_BLEND_SRC_ALPHA, &aPrevBlendSrcA); + aContext->core11fwd->glGetIntegerv(GL_BLEND_DST_ALPHA, &aPrevBlendDstA); + const bool hasDepthClamp = aContext->arbDepthClamp; + const bool wasDepthClamp = + hasDepthClamp && aContext->core11fwd->glIsEnabled(GL_DEPTH_CLAMP) == GL_TRUE; + const occ::handle aPrevProgram = aContext->ActiveProgram(); + + // Capture the user-set camera ZRange BEFORE any grid-specific adjustment. + // ZFitAll below mutates the camera; the restore block at the end of this + // method targets these original values, otherwise vconvert and other APIs + // observing ZRange between frames see values inflated by the grid bounds. + const double aZNearKeep = aCamera->ZNear(); + const double aZFarKeep = aCamera->ZFar(); + const Graphic3d_Camera::Projection aProjKeep = aCamera->ProjectionType(); + + Bnd_Box aBnd = MinMaxValues(true); + const gp_Pnt aPlaneLoc = myGridPlane.Location(); + if (myGridParams.IsBackground() || aBnd.IsVoid() || aBnd.IsOut(aPlaneLoc)) + { + aBnd.Add(aPlaneLoc); + // ZFitAll asserts ZFar > ZNear on return (Graphic3d_Camera.cxx:1636), + // so no post-hoc SetZRange nudge is needed. + aCamera->ZFitAll(1.0, aBnd, aBnd); + } + if (myGridParams.IsBackground()) + { + aCamera->SetProjectionType(Graphic3d_Camera::Projection_Orthographic); + } + + aContext->ProjectionState.Push(); + aContext->ProjectionState.SetCurrent(aCamera->ProjectionMatrixF()); + aContext->ApplyProjectionMatrix(); + + const NCollection_Mat4 aWorldViewCurrent = aContext->WorldViewState.Current(); + aContext->WorldViewState.Push(); + if (myGridParams.IsBackground()) + { + // In background mode the grid lives in an unchanging reference frame. + // Derive the camera-motion delta from the captured reference view matrix so the + // grid stays fixed in world coords during pan/rotate, without exposing + // PanningVector / RotationPoint on Graphic3d_Camera. + NCollection_Mat4 aRefInv; + if (!myGridRefViewMatrix.Inverted(aRefInv)) + { + aRefInv.InitIdentity(); + } + NCollection_Mat4 aDelta = aWorldViewCurrent * aRefInv; + aContext->WorldViewState.SetCurrent(aDelta); + } + aContext->ApplyWorldViewMatrix(); + + aContext->core11fwd->glEnable(GL_DEPTH_TEST); + aContext->core11fwd->glDepthFunc(GL_LESS); + aContext->core11fwd->glDepthMask(GL_TRUE); + aContext->core11fwd->glEnable(GL_BLEND); + aContext->core11fwd->glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + // The clip-space full-screen quad winds CW; if back-face culling is on + // (leftover from prior scene render with solid interiors), every triangle + // is culled and the grid silently vanishes. Disable culling for the draw + // call and restore at the end. + aContext->core11fwd->glDisable(GL_CULL_FACE); + if (hasDepthClamp && !wasDepthClamp) + { + aContext->core11fwd->glEnable(GL_DEPTH_CLAMP); + } + + aContext->core30->glBindVertexArray(myGridVao); + + double aScaleX = myGridParams.Scale(); + double aScaleY = myGridParams.EffectiveScaleY(); + if (myGridParams.IsInfinity()) + { + const double aCamScale = aCamera->Scale(); + aScaleX = 10.0 / std::pow(10.0, std::floor(std::log10(std::max(aCamScale, 1.0))) + 1.0); + aScaleY = aScaleX; + } + + if (aContext->ShaderManager()->BindGridProgram()) + { + const occ::handle& aProg = aContext->ActiveProgram(); + + aProg->SetUniform(aContext, "uScaleX", GLfloat(aScaleX)); + aProg->SetUniform(aContext, "uScaleY", GLfloat(aScaleY)); + aProg->SetUniform(aContext, "uThickness", GLfloat(myGridParams.LineThickness())); + aProg->SetUniform(aContext, + "uColor", + NCollection_Vec3((float)myGridParams.Color().Red(), + (float)myGridParams.Color().Green(), + (float)myGridParams.Color().Blue())); + aProg->SetUniform(aContext, + "uAccentColor", + NCollection_Vec3((float)myGridParams.AccentColor().Red(), + (float)myGridParams.AccentColor().Green(), + (float)myGridParams.AccentColor().Blue())); + aProg->SetUniform(aContext, "uAccentScaleX", GLfloat(myGridParams.AccentScaleX())); + aProg->SetUniform(aContext, "uAccentScaleY", GLfloat(myGridParams.AccentScaleY())); + aProg->SetUniform(aContext, "uAccentAngularScale", GLfloat(myGridParams.AccentAngularScale())); + aProg->SetUniform(aContext, "uIsDrawAxis", myGridParams.IsDrawAxis() ? 1 : 0); + aProg->SetUniform(aContext, "uIsBackground", myGridParams.IsBackground() ? 1 : 0); + aProg->SetUniform(aContext, "uGridType", myGridParams.IsCircular() ? 1 : 0); + // Angular spokes per radian: N spokes in 180 deg = N / pi spokes per radian. + const double aAngularScale = + myGridParams.IsCircular() ? double(myGridParams.AngularDivisions()) / M_PI : 0.0; + aProg->SetUniform(aContext, "uAngularScale", GLfloat(aAngularScale)); + aProg->SetUniform(aContext, "uDrawMode", myGridParams.DrawMode() == Aspect_GDM_Points ? 1 : 0); + + // Bounded work area (HalfSizeX, HalfSizeY, Radius). 0 = unbounded along that axis. + const float aHalfX = myGridParams.SizeX() > 0.0 ? float(myGridParams.SizeX() * 0.5) : 0.0f; + const float aHalfY = myGridParams.SizeY() > 0.0 ? float(myGridParams.SizeY() * 0.5) : 0.0f; + const float aRadius = myGridParams.Radius() > 0.0 ? float(myGridParams.Radius()) : 0.0f; + aProg->SetUniform(aContext, "uBounds", NCollection_Vec3(aHalfX, aHalfY, aRadius)); + aProg->SetUniform( + aContext, + "uArcRange", + NCollection_Vec2(float(myGridParams.AngleStart()), float(myGridParams.AngleEnd()))); + aProg->SetUniform(aContext, "uArcBounded", myGridParams.IsArc() ? 1 : 0); + + // In-plane rotation: rotate the plane's X/Y basis around the plane normal + // so the grid lines follow the requested RotationAngle. Sign matches + // V3d_View::SetGrid's Trsf2 so snap (V3d_View::Compute) and the drawn + // grid use the same basis: aGridX = cos*planeX - sin*planeY, + // aGridY = sin*planeX + cos*planeY. + const double aCosA = std::cos(myGridParams.RotationAngle()); + const double aSinA = std::sin(myGridParams.RotationAngle()); + const gp_Dir aRawX = myGridPlane.XDirection(); + const gp_Dir aRawY = myGridPlane.YDirection(); + const gp_XYZ aXRotated = aRawX.XYZ() * aCosA - aRawY.XYZ() * aSinA; + const gp_XYZ aYRotated = aRawX.XYZ() * aSinA + aRawY.XYZ() * aCosA; + + // ZOffset pushes the displayed plane along its normal to avoid z-fighting + // with coplanar geometry. Snap math uses the unshifted plane, so selection + // still lands on the true plane. + const gp_Dir aNDir = myGridPlane.Direction(); + const double aZOffset = myGridParams.ZOffset(); + const gp_Pnt aOriginLocal = myGridParams.Origin(); + const gp_Pnt aPlaneOrigin(aPlaneLoc.X() + aOriginLocal.X() + aNDir.X() * aZOffset, + aPlaneLoc.Y() + aOriginLocal.Y() + aNDir.Y() * aZOffset, + aPlaneLoc.Z() + aOriginLocal.Z() + aNDir.Z() * aZOffset); + aProg->SetUniform(aContext, + "uPlaneOrigin", + NCollection_Vec3((float)aPlaneOrigin.X(), + (float)aPlaneOrigin.Y(), + (float)aPlaneOrigin.Z())); + aProg->SetUniform( + aContext, + "uPlaneX", + NCollection_Vec3((float)aXRotated.X(), (float)aXRotated.Y(), (float)aXRotated.Z())); + aProg->SetUniform( + aContext, + "uPlaneY", + NCollection_Vec3((float)aYRotated.X(), (float)aYRotated.Y(), (float)aYRotated.Z())); + + // Build a stable per-frame rectangular-grid reference point in plane-local + // coordinates. This keeps shader fract() arguments bounded at shallow angles + // without re-running extra unproject/intersection work for every fragment. + int aHasStableRef = 0; + NCollection_Vec2 aStableRefLocal(0.0f, 0.0f); + if (!myGridParams.IsCircular()) + { + const int* aViewport = aContext->Viewport(); + if (aViewport != nullptr) + { + const float aWinX = float(aViewport[0]) + float(aViewport[2]) * 0.5f; + const float aWinY = float(aViewport[1]) + float(aViewport[3]) * 0.5f; + + float aNearX = 0.0f, aNearY = 0.0f, aNearZ = 0.0f; + float aFarX = 0.0f, aFarY = 0.0f, aFarZ = 0.0f; + const bool isNearOk = + Graphic3d_TransformUtils::UnProject(aWinX, + aWinY, + 0.0f, + aContext->WorldViewState.Current(), + aContext->ProjectionState.Current(), + aViewport, + aNearX, + aNearY, + aNearZ); + const bool isFarOk = + Graphic3d_TransformUtils::UnProject(aWinX, + aWinY, + 1.0f, + aContext->WorldViewState.Current(), + aContext->ProjectionState.Current(), + aViewport, + aFarX, + aFarY, + aFarZ); + if (isNearOk && isFarOk) + { + const NCollection_Vec3 aNearP(aNearX, aNearY, aNearZ); + const NCollection_Vec3 aFarP(aFarX, aFarY, aFarZ); + const NCollection_Vec3 aDir = aFarP - aNearP; + + const NCollection_Vec3 aPlaneN((float)aNDir.X(), + (float)aNDir.Y(), + (float)aNDir.Z()); + const float aDenom = aPlaneN.Dot(aDir); + if (std::abs(aDenom) > 1.0e-6f) + { + const NCollection_Vec3 aPlaneOriginV((float)aPlaneOrigin.X(), + (float)aPlaneOrigin.Y(), + (float)aPlaneOrigin.Z()); + const float aT = aPlaneN.Dot(aPlaneOriginV - aNearP) / aDenom; + const NCollection_Vec3 aHit = aNearP + aDir * aT; + const NCollection_Vec3 aLocal3 = aHit - aPlaneOriginV; + const NCollection_Vec3 aPlaneX((float)aXRotated.X(), + (float)aXRotated.Y(), + (float)aXRotated.Z()); + const NCollection_Vec3 aPlaneY((float)aYRotated.X(), + (float)aYRotated.Y(), + (float)aYRotated.Z()); + aStableRefLocal.SetValues(aLocal3.Dot(aPlaneX), aLocal3.Dot(aPlaneY)); + aHasStableRef = 1; + } + } + } + } + aProg->SetUniform(aContext, "uStableRefLocal", aStableRefLocal); + aProg->SetUniform(aContext, "uHasStableRef", aHasStableRef); + aProg->SetUniform( + aContext, + "uPlaneN", + NCollection_Vec3((float)aNDir.X(), (float)aNDir.Y(), (float)aNDir.Z())); + + aContext->core11fwd->glDrawArrays(GL_TRIANGLES, 0, 6); + } + + aContext->BindProgram(aPrevProgram); + aContext->core30->glBindVertexArray((GLuint)aPrevVao); + + aCamera->SetZRange(aZNearKeep, aZFarKeep); + aCamera->SetProjectionType(aProjKeep); + + aContext->WorldViewState.Pop(); + aContext->ProjectionState.Pop(); + aContext->ApplyWorldViewMatrix(); + aContext->ApplyProjectionMatrix(); + + if (wasDepthTest == GL_FALSE) + { + aContext->core11fwd->glDisable(GL_DEPTH_TEST); + } + aContext->core11fwd->glDepthFunc(aPrevDepthFunc); + aContext->core11fwd->glDepthMask(wasDepthWrite); + if (wasBlend == GL_FALSE) + { + aContext->core11fwd->glDisable(GL_BLEND); + } + aContext->core15fwd->glBlendFuncSeparate(aPrevBlendSrcRgb, + aPrevBlendDstRgb, + aPrevBlendSrcA, + aPrevBlendDstA); + if (wasCullFace == GL_TRUE) + { + aContext->core11fwd->glEnable(GL_CULL_FACE); + } + if (hasDepthClamp && !wasDepthClamp) + { + aContext->core11fwd->glDisable(GL_DEPTH_CLAMP); + } +} diff --git a/src/Visualization/TKOpenGl/OpenGl/OpenGl_View.hxx b/src/Visualization/TKOpenGl/OpenGl/OpenGl_View.hxx index 36b4c5d87f..0176b0c881 100644 --- a/src/Visualization/TKOpenGl/OpenGl/OpenGl_View.hxx +++ b/src/Visualization/TKOpenGl/OpenGl/OpenGl_View.hxx @@ -220,9 +220,15 @@ public: //! Enables or disables IBL (Image Based Lighting) from background cubemap. //! Has no effect if PBR is not used. //! @param[in] theToEnableIBL enable or disable IBL from background cubemap - //! @param[in] theToUpdate redraw the view Standard_EXPORT void SetImageBasedLighting(bool theToEnableIBL) override; + //! Display a shader-rendered infinite grid on the given plane. + Standard_EXPORT void GridDisplay(const Aspect_GridParams& theParams, + const gp_Ax3& thePlane) override; + + //! Erase the shader-rendered infinite grid. + Standard_EXPORT void GridErase() override; + //! Returns number of mipmap levels used in specular IBL map. //! 0 if PBR environment is not created. Standard_EXPORT unsigned int SpecIBLMapLevels() const; @@ -424,6 +430,10 @@ protected: //! @name Rendering of GL graphics (with prepared drawing buffer). //! Renders frame statistics. void renderFrameStats(); + //! Render the shader-based infinite grid. + //! No-op unless GridDisplay() has been called and the GAPI supports the grid shader. + void renderGrid(); + private: //! Adds the structure to display lists of the view. Standard_EXPORT void displayStructure(const occ::handle& theStructure, @@ -529,6 +539,11 @@ protected: //! @name Background parameters OpenGl_Aspects* myTextureParams; //!< Stores texture and its parameters for textured background OpenGl_Aspects* myCubeMapParams; //!< Stores cubemap and its parameters for cubemap background OpenGl_Aspects* myColoredQuadParams; //!< Stores parameters for gradient (corner mode) background + Aspect_GridParams myGridParams; //!< parameters of shader infinite grid + gp_Ax3 myGridPlane; //!< grid plane in world coordinates + NCollection_Mat4 myGridRefViewMatrix; //!< worldview captured at GridDisplay() for pan/rotate compensation + unsigned int myGridVao; //!< dedicated VAO for textureless grid draw + bool myToShowGrid; //!< flag indicating the grid is active OpenGl_BackgroundArray* myBackgrounds[Graphic3d_TypeOfBackground_NB]; //!< Array of primitive arrays of different background types // clang-format on occ::handle myTextureEnv; diff --git a/src/Visualization/TKService/Aspect/Aspect_CircularGrid.cxx b/src/Visualization/TKService/Aspect/Aspect_CircularGrid.cxx index ffc88b533a..30d25e4f52 100644 --- a/src/Visualization/TKService/Aspect/Aspect_CircularGrid.cxx +++ b/src/Visualization/TKService/Aspect/Aspect_CircularGrid.cxx @@ -26,7 +26,14 @@ Aspect_CircularGrid::Aspect_CircularGrid(const double aRadiusStep, const double aRotationAngle) : Aspect_Grid(anXOrigin, anYOrigin, aRotationAngle), myRadiusStep(aRadiusStep), - myDivisionNumber(aDivisionNumber) + myDivisionNumber(aDivisionNumber), + myAlpha(0.0), + myA1(0.0), + myB1(0.0), + myRadius(0.0), + myZOffset(0.0), + myAngleStart(0.0), + myAngleEnd(0.0) { } @@ -170,6 +177,36 @@ void Aspect_CircularGrid::Init() myB1 = std::sin(myAlpha); } +void Aspect_CircularGrid::SetRadius(const double theRadius) +{ + Standard_NegativeValue_Raise_if(theRadius < 0.0, "invalid grid radius"); + if (myRadius != theRadius) + { + myRadius = theRadius; + UpdateDisplay(); + } +} + +void Aspect_CircularGrid::SetZOffset(const double theOffset) +{ + if (myZOffset != theOffset) + { + myZOffset = theOffset; + UpdateDisplay(); + } +} + +void Aspect_CircularGrid::SetArcRange(const double theStart, const double theEnd) +{ + if (myAngleStart == theStart && myAngleEnd == theEnd) + { + return; + } + myAngleStart = theStart; + myAngleEnd = theEnd; + UpdateDisplay(); +} + //================================================================================================= void Aspect_CircularGrid::DumpJson(Standard_OStream& theOStream, int theDepth) const @@ -183,4 +220,8 @@ void Aspect_CircularGrid::DumpJson(Standard_OStream& theOStream, int theDepth) c OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myAlpha) OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myA1) OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myB1) + OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myRadius) + OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myZOffset) + OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myAngleStart) + OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myAngleEnd) } diff --git a/src/Visualization/TKService/Aspect/Aspect_CircularGrid.hxx b/src/Visualization/TKService/Aspect/Aspect_CircularGrid.hxx index 3c4a9e0aad..ed9db02211 100644 --- a/src/Visualization/TKService/Aspect/Aspect_CircularGrid.hxx +++ b/src/Visualization/TKService/Aspect/Aspect_CircularGrid.hxx @@ -56,6 +56,36 @@ public: //! returns the x step of the grid. Standard_EXPORT int DivisionNumber() const; + //! Set the circular grid radius (plane-local units). 0.0 (default) means + //! unbounded - the shader draws the grid to the horizon. + Standard_EXPORT void SetRadius(const double theRadius); + + //! Return the bounded radius. 0.0 means unbounded. + double Radius() const { return myRadius; } + + //! Set signed offset along the plane normal for display only; snap math + //! stays on the plane. Use a small negative value to avoid z-fighting with + //! coplanar geometry. + Standard_EXPORT void SetZOffset(const double theOffset); + + //! Return the display-time Z-offset along the plane normal. + double ZOffset() const { return myZOffset; } + + //! Restrict the grid to an angular wedge, walking counter-clockwise from + //! @p theStart to @p theEnd (radians, measured from the rotated plane X + //! axis). Setting both values equal (e.g. both 0.0) returns to full-circle + //! rendering - the sentinel used for unbounded. + Standard_EXPORT void SetArcRange(const double theStart, const double theEnd); + + //! Return the arc start angle (radians). Meaningful only when IsArc() is true. + double AngleStart() const { return myAngleStart; } + + //! Return the arc end angle (radians). Meaningful only when IsArc() is true. + double AngleEnd() const { return myAngleEnd; } + + //! Return TRUE when the grid is restricted to an angular wedge. + bool IsArc() const { return myAngleStart != myAngleEnd; } + Standard_EXPORT void Init() override; //! Dumps the content of me into the stream @@ -67,6 +97,10 @@ private: double myAlpha; double myA1; double myB1; + double myRadius; + double myZOffset; + double myAngleStart; + double myAngleEnd; }; #endif // _Aspect_CircularGrid_HeaderFile diff --git a/src/Visualization/TKService/Aspect/Aspect_GridParams.hxx b/src/Visualization/TKService/Aspect/Aspect_GridParams.hxx new file mode 100644 index 0000000000..f6657dc30f --- /dev/null +++ b/src/Visualization/TKService/Aspect/Aspect_GridParams.hxx @@ -0,0 +1,266 @@ +// Copyright (c) 2026 OPEN CASCADE SAS +// +// This file is part of Open CASCADE Technology software library. +// +// This library is free software; you can redistribute it and/or modify it under +// the terms of the GNU Lesser General Public License version 2.1 as published +// by the Free Software Foundation, with special exception defined in the file +// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT +// distribution for complete text of the license and disclaimer of any warranty. +// +// Alternatively, this file may be used under the terms of Open CASCADE +// commercial license or contractual agreement. + +#ifndef _Aspect_GridParams_HeaderFile +#define _Aspect_GridParams_HeaderFile + +#include +#include +#include + +#include +#include +#include + +//! Render-only appearance parameters for a shader-rendered grid. +//! +//! Drives a screen-space quad intersected with a grid plane in the fragment +//! shader (see Graphic3d_ShaderManager::getGridProgram). This is a pure value +//! struct; it does NOT own any snap state. The authoritative grid geometry for +//! snap selection still lives on Aspect_RectangularGrid / Aspect_CircularGrid, +//! and V3d_{Rectangular,Circular}Grid::syncViews produces an Aspect_GridParams +//! from the grid on every display. If you introduce a new field here that has +//! a matching concept on Aspect_Grid, update syncViews too so both layers stay +//! in sync. +class Aspect_GridParams +{ +public: + DEFINE_STANDARD_ALLOC + + //! Construct with sensible defaults: grey lines on the plane origin with + //! axis coloring enabled, 1/100 plane-unit spacing, overlay mode, + //! unbounded in extent and radius. + Aspect_GridParams() + : myColor(Quantity_NOC_GRAY70), + myAccentColor(Quantity_NOC_GRAY50), + myOrigin(0.0, 0.0, 0.0), + myScale(0.01), + myScaleY(0.0), + myAccentScaleX(0.0), + myAccentScaleY(0.0), + myAccentAngularScale(0.0), + myLineThickness(0.01), + myRotationAngle(0.0), + mySizeX(0.0), + mySizeY(0.0), + myRadius(0.0), + myZOffset(0.0), + myAngleStart(0.0), + myAngleEnd(0.0), + myAngularDivisions(0), + myDrawMode(Aspect_GDM_Lines), + myIsBackground(false), + myIsDrawAxis(true), + myIsInfinity(false) + { + } + + //! Return grid line color. + const Quantity_Color& Color() const { return myColor; } + + //! Set grid line color. + void SetColor(const Quantity_Color& theColor) { myColor = theColor; } + + //! Return local offset of the grid origin within the plane. + const gp_Pnt& Origin() const { return myOrigin; } + + //! Set local offset of the grid origin within the plane. + void SetOrigin(const gp_Pnt& theOrigin) { myOrigin = theOrigin; } + + //! Return accent color used by the classical V3d grid emulation path. + const Quantity_Color& AccentColor() const { return myAccentColor; } + + //! Set accent color used by the classical V3d grid emulation path. + void SetAccentColor(const Quantity_Color& theColor) { myAccentColor = theColor; } + + //! Return accent overlay scale along the plane X/radial direction. + //! Zero disables the accent layer on that axis. + double AccentScaleX() const { return myAccentScaleX; } + + //! Set accent overlay scale along the plane X/radial direction. + void SetAccentScaleX(const double theScale) { myAccentScaleX = theScale; } + + //! Return accent overlay scale along the plane Y direction. + //! Zero disables the accent layer on that axis. + double AccentScaleY() const { return myAccentScaleY; } + + //! Set accent overlay scale along the plane Y direction. + void SetAccentScaleY(const double theScale) { myAccentScaleY = theScale; } + + //! Return accent overlay angular scale for circular-grid spokes. + //! Zero disables the angular accent layer. + double AccentAngularScale() const { return myAccentAngularScale; } + + //! Set accent overlay angular scale for circular-grid spokes. + void SetAccentAngularScale(const double theScale) { myAccentAngularScale = theScale; } + + //! Return major-grid scale factor along the plane X direction (cells per plane unit). + double Scale() const { return myScale; } + + //! Set major-grid scale factor along the plane X direction (cells per plane unit). + //! Must be non-negative; zero is a valid "unused" sentinel. + void SetScale(const double theScale) + { + Standard_NegativeValue_Raise_if(theScale < 0.0, "invalid grid scale"); + myScale = theScale; + } + + //! Return explicit Y-direction scale. When 0.0, renderer falls back to Scale() (isotropic). + double ScaleY() const { return myScaleY; } + + //! Set explicit Y-direction scale. Pass 0.0 to mirror Scale() (isotropic, default). + void SetScaleY(const double theScaleY) + { + Standard_NegativeValue_Raise_if(theScaleY < 0.0, "invalid grid Y-scale"); + myScaleY = theScaleY; + } + + //! Effective Y-direction scale actually consumed by the renderer. + double EffectiveScaleY() const { return myScaleY > 0.0 ? myScaleY : myScale; } + + //! Return line thickness in plane units (minimum pixel-space line width is derived from fwidth). + double LineThickness() const { return myLineThickness; } + + //! Set line thickness in plane units. + void SetLineThickness(const double theThickness) + { + Standard_NegativeValue_Raise_if(theThickness < 0.0, "invalid grid line thickness"); + myLineThickness = theThickness; + } + + //! Return in-plane rotation angle (radians) applied to the grid axes around the plane normal. + double RotationAngle() const { return myRotationAngle; } + + //! Set in-plane rotation angle (radians) applied to the grid axes around the plane normal. + void SetRotationAngle(const double theAngle) { myRotationAngle = theAngle; } + + //! Return the angular subdivision count of the half-circle for circular grids. + //! Zero means rectangular grid (default); any positive value switches the + //! renderer to polar rings (Scale -> radial step) and spokes at pi/N rad. + int AngularDivisions() const { return myAngularDivisions; } + + //! Set angular subdivision count (0 = rectangular grid, N>0 = circular with N spokes per 180 + //! deg). + void SetAngularDivisions(const int theDivisions) { myAngularDivisions = theDivisions; } + + //! Return TRUE when the parameters describe a circular (polar) grid. + bool IsCircular() const { return myAngularDivisions > 0; } + + //! Return rectangular bounded extent along plane X; 0.0 means unbounded. + double SizeX() const { return mySizeX; } + + //! Set rectangular bounded extent along plane X; 0.0 means unbounded. + void SetSizeX(const double theSize) + { + Standard_NegativeValue_Raise_if(theSize < 0.0, "invalid grid X-size"); + mySizeX = theSize; + } + + //! Return rectangular bounded extent along plane Y; 0.0 means unbounded. + double SizeY() const { return mySizeY; } + + //! Set rectangular bounded extent along plane Y; 0.0 means unbounded. + void SetSizeY(const double theSize) + { + Standard_NegativeValue_Raise_if(theSize < 0.0, "invalid grid Y-size"); + mySizeY = theSize; + } + + //! Return circular bounded radius; 0.0 means unbounded. + double Radius() const { return myRadius; } + + //! Set circular bounded radius; 0.0 means unbounded. + void SetRadius(const double theRadius) + { + Standard_NegativeValue_Raise_if(theRadius < 0.0, "invalid grid radius"); + myRadius = theRadius; + } + + //! Return signed plane-normal offset applied at render time. + double ZOffset() const { return myZOffset; } + + //! Set signed plane-normal offset applied at render time (display only; + //! snap math stays on the unshifted plane). + void SetZOffset(const double theOffset) { myZOffset = theOffset; } + + //! Return arc start angle (radians). Meaningful only when IsArc() is true. + double AngleStart() const { return myAngleStart; } + + //! Return arc end angle (radians). Meaningful only when IsArc() is true. + double AngleEnd() const { return myAngleEnd; } + + //! Restrict the circular grid to an angular wedge [start, end], walking CCW. + //! Equal start and end (e.g. 0.0 and 0.0) returns to full-circle rendering. + void SetArcRange(const double theStart, const double theEnd) + { + myAngleStart = theStart; + myAngleEnd = theEnd; + } + + //! Return TRUE when the parameters describe a bounded rectangle or disc. + bool IsBounded() const { return mySizeX > 0.0 || mySizeY > 0.0 || myRadius > 0.0; } + + //! Return TRUE when the circular grid is restricted to a sub-arc. + bool IsArc() const { return myAngleStart != myAngleEnd; } + + //! Return draw mode: lines, points at grid intersections, or none. + Aspect_GridDrawMode DrawMode() const { return myDrawMode; } + + //! Set draw mode. Aspect_GDM_None suppresses rendering entirely; Points draws + //! dots at grid-line intersections, Lines (default) draws the full grid. + void SetDrawMode(const Aspect_GridDrawMode theMode) { myDrawMode = theMode; } + + //! Return TRUE if grid is drawn as a view-space background (behind all geometry). + bool IsBackground() const { return myIsBackground; } + + //! Set background-mode rendering on/off. + void SetIsBackground(const bool theIsBackground) { myIsBackground = theIsBackground; } + + //! Return TRUE if axis lines on the grid plane are drawn in red/green/blue. + bool IsDrawAxis() const { return myIsDrawAxis; } + + //! Set axis coloring on/off. + void SetIsDrawAxis(const bool theIsDrawAxis) { myIsDrawAxis = theIsDrawAxis; } + + //! Return TRUE if the scale adapts to camera zoom to keep an apparent grid density. + bool IsInfinity() const { return myIsInfinity; } + + //! Set camera-adaptive scale on/off. When enabled, Scale() is ignored in favour + //! of a derived value based on camera distance. + void SetIsInfinity(const bool theIsInfinity) { myIsInfinity = theIsInfinity; } + +private: + Quantity_Color myColor; + Quantity_Color myAccentColor; + gp_Pnt myOrigin; + double myScale; + double myScaleY; + double myAccentScaleX; + double myAccentScaleY; + double myAccentAngularScale; + double myLineThickness; + double myRotationAngle; + double mySizeX; + double mySizeY; + double myRadius; + double myZOffset; + double myAngleStart; + double myAngleEnd; + int myAngularDivisions; + Aspect_GridDrawMode myDrawMode; + bool myIsBackground; + bool myIsDrawAxis; + bool myIsInfinity; +}; + +#endif // _Aspect_GridParams_HeaderFile diff --git a/src/Visualization/TKService/Aspect/Aspect_RectangularGrid.cxx b/src/Visualization/TKService/Aspect/Aspect_RectangularGrid.cxx index c1d9fa373c..264e2420aa 100644 --- a/src/Visualization/TKService/Aspect/Aspect_RectangularGrid.cxx +++ b/src/Visualization/TKService/Aspect/Aspect_RectangularGrid.cxx @@ -30,7 +30,10 @@ Aspect_RectangularGrid::Aspect_RectangularGrid(const double aXStep, myXStep(aXStep), myYStep(aYStep), myFirstAngle(aFirstAngle), - mySecondAngle(aSecondAngle) + mySecondAngle(aSecondAngle), + mySizeX(0.0), + mySizeY(0.0), + myZOffset(0.0) { Standard_NumericError_Raise_if(!CheckAngle(aFirstAngle, mySecondAngle), "networks are parallel"); @@ -68,6 +71,35 @@ void Aspect_RectangularGrid::SetAngle(const double anAngle1, const double anAngl UpdateDisplay(); } +void Aspect_RectangularGrid::SetSizeX(const double theSize) +{ + Standard_NegativeValue_Raise_if(theSize < 0.0, "invalid grid size X"); + if (mySizeX != theSize) + { + mySizeX = theSize; + UpdateDisplay(); + } +} + +void Aspect_RectangularGrid::SetSizeY(const double theSize) +{ + Standard_NegativeValue_Raise_if(theSize < 0.0, "invalid grid size Y"); + if (mySizeY != theSize) + { + mySizeY = theSize; + UpdateDisplay(); + } +} + +void Aspect_RectangularGrid::SetZOffset(const double theOffset) +{ + if (myZOffset != theOffset) + { + myZOffset = theOffset; + UpdateDisplay(); + } +} + void Aspect_RectangularGrid::SetGridValues(const double theXOrigin, const double theYOrigin, const double theXStep, @@ -186,6 +218,9 @@ void Aspect_RectangularGrid::DumpJson(Standard_OStream& theOStream, int theDepth OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myYStep) OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myFirstAngle) OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, mySecondAngle) + OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, mySizeX) + OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, mySizeY) + OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myZOffset) OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, a1) OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, b1) OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, c1) diff --git a/src/Visualization/TKService/Aspect/Aspect_RectangularGrid.hxx b/src/Visualization/TKService/Aspect/Aspect_RectangularGrid.hxx index ec2d0a0335..b2befab7d3 100644 --- a/src/Visualization/TKService/Aspect/Aspect_RectangularGrid.hxx +++ b/src/Visualization/TKService/Aspect/Aspect_RectangularGrid.hxx @@ -72,6 +72,28 @@ public: //! returns the y Angle of the grid, relatively to the vertical. Standard_EXPORT double SecondAngle() const; + //! Set full extent of the bounded grid along the plane X direction (plane-local units). + //! 0.0 (default) means unbounded - the shader draws the grid to the horizon. + Standard_EXPORT void SetSizeX(const double theSize); + + //! Return the bounded-region extent along plane X. 0.0 means unbounded. + double SizeX() const { return mySizeX; } + + //! Set full extent of the bounded grid along the plane Y direction (plane-local units). + //! 0.0 (default) means unbounded. + Standard_EXPORT void SetSizeY(const double theSize); + + //! Return the bounded-region extent along plane Y. 0.0 means unbounded. + double SizeY() const { return mySizeY; } + + //! Set signed offset (plane-local units) applied along the plane normal for + //! display only - snap math stays on the plane. Use a small negative value + //! to push the grid slightly below coplanar geometry and avoid z-fighting. + Standard_EXPORT void SetZOffset(const double theOffset); + + //! Return the display-time Z-offset along the plane normal. + double ZOffset() const { return myZOffset; } + Standard_EXPORT void Init() override; //! Dumps the content of me into the stream @@ -85,6 +107,9 @@ private: double myYStep; double myFirstAngle; double mySecondAngle; + double mySizeX; + double mySizeY; + double myZOffset; double a1; double b1; double c1; diff --git a/src/Visualization/TKService/Aspect/FILES.cmake b/src/Visualization/TKService/Aspect/FILES.cmake index a5b5b837fe..775278f89f 100644 --- a/src/Visualization/TKService/Aspect/FILES.cmake +++ b/src/Visualization/TKService/Aspect/FILES.cmake @@ -29,6 +29,7 @@ set(OCCT_Aspect_FILES Aspect_Grid.cxx Aspect_Grid.hxx Aspect_GridDrawMode.hxx + Aspect_GridParams.hxx Aspect_GridType.hxx Aspect_NeutralWindow.cxx Aspect_NeutralWindow.hxx diff --git a/src/Visualization/TKService/GTests/Aspect_GridParams_Test.cxx b/src/Visualization/TKService/GTests/Aspect_GridParams_Test.cxx new file mode 100644 index 0000000000..caada60e39 --- /dev/null +++ b/src/Visualization/TKService/GTests/Aspect_GridParams_Test.cxx @@ -0,0 +1,266 @@ +// Copyright (c) 2026 OPEN CASCADE SAS +// +// This file is part of Open CASCADE Technology software library. +// +// This library is free software; you can redistribute it and/or modify it under +// the terms of the GNU Lesser General Public License version 2.1 as published +// by the Free Software Foundation, with special exception defined in the file +// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT +// distribution for complete text of the license and disclaimer of any warranty. +// +// Alternatively, this file may be used under the terms of Open CASCADE +// commercial license or contractual agreement. + +#include +#include +#include +#include +#include + +#include + +TEST(Aspect_GridParamsTest, Defaults_AreReasonable) +{ + Aspect_GridParams aParams; + EXPECT_EQ(Quantity_NOC_GRAY70, aParams.Color().Name()) << "default color is grey"; + EXPECT_EQ(Quantity_NOC_GRAY50, aParams.AccentColor().Name()) + << "default accent color is darker grey"; + EXPECT_DOUBLE_EQ(0.01, aParams.Scale()); + EXPECT_DOUBLE_EQ(0.0, aParams.ScaleY()) << "default ScaleY is 0 (isotropic sentinel)"; + EXPECT_DOUBLE_EQ(0.01, aParams.EffectiveScaleY()) << "isotropic: EffectiveScaleY mirrors Scale"; + EXPECT_DOUBLE_EQ(0.0, aParams.AccentScaleX()); + EXPECT_DOUBLE_EQ(0.0, aParams.AccentScaleY()); + EXPECT_DOUBLE_EQ(0.0, aParams.AccentAngularScale()); + EXPECT_DOUBLE_EQ(0.01, aParams.LineThickness()); + EXPECT_DOUBLE_EQ(0.0, aParams.RotationAngle()); + EXPECT_EQ(0, aParams.AngularDivisions()) << "default grid is rectangular"; + EXPECT_FALSE(aParams.IsCircular()); + EXPECT_EQ(Aspect_GDM_Lines, aParams.DrawMode()); + EXPECT_FALSE(aParams.IsBackground()); + EXPECT_TRUE(aParams.IsDrawAxis()); + EXPECT_FALSE(aParams.IsInfinity()); + EXPECT_NEAR(0.0, aParams.Origin().X(), Precision::Confusion()); + EXPECT_NEAR(0.0, aParams.Origin().Y(), Precision::Confusion()); + EXPECT_NEAR(0.0, aParams.Origin().Z(), Precision::Confusion()); + EXPECT_DOUBLE_EQ(0.0, aParams.SizeX()); + EXPECT_DOUBLE_EQ(0.0, aParams.SizeY()); + EXPECT_DOUBLE_EQ(0.0, aParams.Radius()); + EXPECT_DOUBLE_EQ(0.0, aParams.ZOffset()); + EXPECT_DOUBLE_EQ(0.0, aParams.AngleStart()); + EXPECT_DOUBLE_EQ(0.0, aParams.AngleEnd()); + EXPECT_FALSE(aParams.IsBounded()); + EXPECT_FALSE(aParams.IsArc()); +} + +TEST(Aspect_GridParamsTest, Bounds_RoundTrip) +{ + Aspect_GridParams aParams; + EXPECT_FALSE(aParams.IsBounded()); + + aParams.SetSizeX(10.0); + EXPECT_DOUBLE_EQ(10.0, aParams.SizeX()); + EXPECT_TRUE(aParams.IsBounded()); + + aParams.SetSizeX(0.0); + EXPECT_FALSE(aParams.IsBounded()) + << "clearing SizeX returns to unbounded when other bounds are 0"; + + aParams.SetSizeY(5.0); + EXPECT_TRUE(aParams.IsBounded()); + aParams.SetSizeY(0.0); + + aParams.SetRadius(3.0); + EXPECT_DOUBLE_EQ(3.0, aParams.Radius()); + EXPECT_TRUE(aParams.IsBounded()); + + aParams.SetRadius(0.0); + EXPECT_FALSE(aParams.IsBounded()); + + aParams.SetZOffset(-0.01); + EXPECT_DOUBLE_EQ(-0.01, aParams.ZOffset()); + EXPECT_FALSE(aParams.IsBounded()) << "ZOffset alone is not a bound"; +} + +TEST(Aspect_GridParamsTest, ArcRange_RoundTrip) +{ + Aspect_GridParams aParams; + EXPECT_FALSE(aParams.IsArc()); + + aParams.SetArcRange(0.0, M_PI); + EXPECT_DOUBLE_EQ(0.0, aParams.AngleStart()); + EXPECT_DOUBLE_EQ(M_PI, aParams.AngleEnd()); + EXPECT_TRUE(aParams.IsArc()); + + // Equal start and end reverts to full circle sentinel. + aParams.SetArcRange(1.0, 1.0); + EXPECT_FALSE(aParams.IsArc()); + + // Wraparound: Start > End is allowed; renderer walks CCW through the wrap. + aParams.SetArcRange(2.356, -2.356); + EXPECT_TRUE(aParams.IsArc()); +} + +TEST(Aspect_GridParamsTest, DrawMode_RoundTrip) +{ + Aspect_GridParams aParams; + aParams.SetDrawMode(Aspect_GDM_Points); + EXPECT_EQ(Aspect_GDM_Points, aParams.DrawMode()); + aParams.SetDrawMode(Aspect_GDM_None); + EXPECT_EQ(Aspect_GDM_None, aParams.DrawMode()); + aParams.SetDrawMode(Aspect_GDM_Lines); + EXPECT_EQ(Aspect_GDM_Lines, aParams.DrawMode()); +} + +TEST(Aspect_GridParamsTest, AngularDivisions_ToggleCircular) +{ + Aspect_GridParams aParams; + EXPECT_FALSE(aParams.IsCircular()); + + aParams.SetAngularDivisions(8); + EXPECT_EQ(8, aParams.AngularDivisions()); + EXPECT_TRUE(aParams.IsCircular()); + + aParams.SetAngularDivisions(0); + EXPECT_FALSE(aParams.IsCircular()) << "zero divisions falls back to rectangular"; +} + +TEST(Aspect_GridParamsTest, EffectiveScaleY_FollowsScaleWhenZero) +{ + Aspect_GridParams aParams; + aParams.SetScale(0.25); + EXPECT_DOUBLE_EQ(0.25, aParams.EffectiveScaleY()); + aParams.SetScaleY(0.5); + EXPECT_DOUBLE_EQ(0.5, aParams.EffectiveScaleY()); + aParams.SetScaleY(0.0); + EXPECT_DOUBLE_EQ(0.25, aParams.EffectiveScaleY()) << "clearing ScaleY restores isotropy"; +} + +TEST(Aspect_GridParamsTest, RotationAngle_RoundTrip) +{ + Aspect_GridParams aParams; + aParams.SetRotationAngle(0.5); + EXPECT_DOUBLE_EQ(0.5, aParams.RotationAngle()); + aParams.SetRotationAngle(-1.25); + EXPECT_DOUBLE_EQ(-1.25, aParams.RotationAngle()); +} + +TEST(Aspect_GridParamsTest, Setters_RoundTrip) +{ + Aspect_GridParams aParams; + + const Quantity_Color aBlue(Quantity_NOC_BLUE1); + aParams.SetColor(aBlue); + EXPECT_EQ(aBlue, aParams.Color()); + + const Quantity_Color aRed(Quantity_NOC_RED); + aParams.SetAccentColor(aRed); + EXPECT_EQ(aRed, aParams.AccentColor()); + + const gp_Pnt aOrigin(1.0, 2.0, 3.0); + aParams.SetOrigin(aOrigin); + EXPECT_NEAR(1.0, aParams.Origin().X(), Precision::Confusion()); + EXPECT_NEAR(2.0, aParams.Origin().Y(), Precision::Confusion()); + EXPECT_NEAR(3.0, aParams.Origin().Z(), Precision::Confusion()); + + aParams.SetScale(0.5); + EXPECT_DOUBLE_EQ(0.5, aParams.Scale()); + + aParams.SetScaleY(0.75); + EXPECT_DOUBLE_EQ(0.75, aParams.ScaleY()); + EXPECT_DOUBLE_EQ(0.75, aParams.EffectiveScaleY()); + + aParams.SetAccentScaleX(0.05); + aParams.SetAccentScaleY(0.1); + aParams.SetAccentAngularScale(2.5); + EXPECT_DOUBLE_EQ(0.05, aParams.AccentScaleX()); + EXPECT_DOUBLE_EQ(0.1, aParams.AccentScaleY()); + EXPECT_DOUBLE_EQ(2.5, aParams.AccentAngularScale()); + + aParams.SetLineThickness(0.02); + EXPECT_DOUBLE_EQ(0.02, aParams.LineThickness()); + + aParams.SetRotationAngle(0.123); + EXPECT_DOUBLE_EQ(0.123, aParams.RotationAngle()); + + aParams.SetAngularDivisions(12); + EXPECT_EQ(12, aParams.AngularDivisions()); + EXPECT_TRUE(aParams.IsCircular()); + + aParams.SetIsBackground(true); + EXPECT_TRUE(aParams.IsBackground()); + + aParams.SetIsDrawAxis(false); + EXPECT_FALSE(aParams.IsDrawAxis()); + + aParams.SetIsInfinity(true); + EXPECT_TRUE(aParams.IsInfinity()); +} + +TEST(Aspect_GridParamsTest, Copy_PreservesFields) +{ + Aspect_GridParams aSrc; + aSrc.SetColor(Quantity_Color(0.1, 0.2, 0.3, Quantity_TOC_RGB)); + aSrc.SetOrigin(gp_Pnt(4.0, 5.0, 6.0)); + aSrc.SetAccentColor(Quantity_Color(0.7, 0.6, 0.5, Quantity_TOC_RGB)); + aSrc.SetScale(0.25); + aSrc.SetScaleY(0.125); + aSrc.SetAccentScaleX(0.025); + aSrc.SetAccentScaleY(0.0125); + aSrc.SetAccentAngularScale(3.0); + aSrc.SetLineThickness(0.04); + aSrc.SetRotationAngle(-0.5); + aSrc.SetAngularDivisions(16); + aSrc.SetIsBackground(true); + aSrc.SetIsDrawAxis(false); + aSrc.SetIsInfinity(true); + aSrc.SetSizeX(7.0); + aSrc.SetSizeY(3.0); + aSrc.SetRadius(2.0); + aSrc.SetZOffset(-0.002); + aSrc.SetArcRange(0.25, 2.0); + + const Aspect_GridParams aCopy = aSrc; + EXPECT_NEAR(0.1, aCopy.Color().Red(), Precision::Confusion()); + EXPECT_NEAR(0.2, aCopy.Color().Green(), Precision::Confusion()); + EXPECT_NEAR(0.3, aCopy.Color().Blue(), Precision::Confusion()); + EXPECT_NEAR(4.0, aCopy.Origin().X(), Precision::Confusion()); + EXPECT_NEAR(5.0, aCopy.Origin().Y(), Precision::Confusion()); + EXPECT_NEAR(6.0, aCopy.Origin().Z(), Precision::Confusion()); + EXPECT_NEAR(0.7, aCopy.AccentColor().Red(), Precision::Confusion()); + EXPECT_NEAR(0.6, aCopy.AccentColor().Green(), Precision::Confusion()); + EXPECT_NEAR(0.5, aCopy.AccentColor().Blue(), Precision::Confusion()); + EXPECT_DOUBLE_EQ(0.25, aCopy.Scale()); + EXPECT_DOUBLE_EQ(0.125, aCopy.ScaleY()); + EXPECT_DOUBLE_EQ(0.125, aCopy.EffectiveScaleY()); + EXPECT_DOUBLE_EQ(0.025, aCopy.AccentScaleX()); + EXPECT_DOUBLE_EQ(0.0125, aCopy.AccentScaleY()); + EXPECT_DOUBLE_EQ(3.0, aCopy.AccentAngularScale()); + EXPECT_DOUBLE_EQ(0.04, aCopy.LineThickness()); + EXPECT_DOUBLE_EQ(-0.5, aCopy.RotationAngle()); + EXPECT_EQ(16, aCopy.AngularDivisions()); + EXPECT_TRUE(aCopy.IsCircular()); + EXPECT_TRUE(aCopy.IsBackground()); + EXPECT_FALSE(aCopy.IsDrawAxis()); + EXPECT_TRUE(aCopy.IsInfinity()); + EXPECT_DOUBLE_EQ(7.0, aCopy.SizeX()); + EXPECT_DOUBLE_EQ(3.0, aCopy.SizeY()); + EXPECT_DOUBLE_EQ(2.0, aCopy.Radius()); + EXPECT_DOUBLE_EQ(-0.002, aCopy.ZOffset()); + EXPECT_DOUBLE_EQ(0.25, aCopy.AngleStart()); + EXPECT_DOUBLE_EQ(2.0, aCopy.AngleEnd()); + EXPECT_TRUE(aCopy.IsBounded()); + EXPECT_TRUE(aCopy.IsArc()); +} + +#ifndef No_Exception +TEST(Aspect_GridParamsTest, Setters_RejectNegativeValues) +{ + Aspect_GridParams aParams; + EXPECT_THROW(aParams.SetScale(-1.0), Standard_NegativeValue); + EXPECT_THROW(aParams.SetScaleY(-1.0), Standard_NegativeValue); + EXPECT_THROW(aParams.SetLineThickness(-0.1), Standard_NegativeValue); + EXPECT_THROW(aParams.SetSizeX(-1.0), Standard_NegativeValue); + EXPECT_THROW(aParams.SetSizeY(-1.0), Standard_NegativeValue); + EXPECT_THROW(aParams.SetRadius(-1.0), Standard_NegativeValue); +} +#endif diff --git a/src/Visualization/TKService/GTests/Aspect_Grid_Bounds_Test.cxx b/src/Visualization/TKService/GTests/Aspect_Grid_Bounds_Test.cxx new file mode 100644 index 0000000000..c71ab32fbd --- /dev/null +++ b/src/Visualization/TKService/GTests/Aspect_Grid_Bounds_Test.cxx @@ -0,0 +1,116 @@ +// Copyright (c) 2026 OPEN CASCADE SAS +// +// This file is part of Open CASCADE Technology software library. +// +// This library is free software; you can redistribute it and/or modify it under +// the terms of the GNU Lesser General Public License version 2.1 as published +// by the Free Software Foundation, with special exception defined in the file +// OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT +// distribution for complete text of the license and disclaimer of any warranty. +// +// Alternatively, this file may be used under the terms of Open CASCADE +// commercial license or contractual agreement. + +#include +#include + +#include + +namespace +{ +// Concrete shims: Aspect_RectangularGrid / Aspect_CircularGrid are abstract +// because Display/Erase/IsDisplayed/UpdateDisplay are pure virtual on the +// base. V3d_RectangularGrid supplies them in production, but pulling V3d into +// this test would need a live viewer/window. Since we only exercise the new +// bounds accessors, a minimal stub is enough. +class RectGridStub : public Aspect_RectangularGrid +{ +public: + RectGridStub(const double theXStep, const double theYStep) + : Aspect_RectangularGrid(theXStep, theYStep) + { + } + + void Display() override {} + + void Erase() const override {} + + bool IsDisplayed() const override { return false; } + +protected: + void UpdateDisplay() override {} +}; + +class CircGridStub : public Aspect_CircularGrid +{ +public: + CircGridStub(const double theRadiusStep, const int theDivisions) + : Aspect_CircularGrid(theRadiusStep, theDivisions) + { + } + + void Display() override {} + + void Erase() const override {} + + bool IsDisplayed() const override { return false; } + +protected: + void UpdateDisplay() override {} +}; +} // namespace + +TEST(Aspect_GridBoundsTest, Rectangular_Defaults) +{ + RectGridStub aGrid(1.0, 1.0); + EXPECT_DOUBLE_EQ(0.0, aGrid.SizeX()); + EXPECT_DOUBLE_EQ(0.0, aGrid.SizeY()); + EXPECT_DOUBLE_EQ(0.0, aGrid.ZOffset()); +} + +TEST(Aspect_GridBoundsTest, Rectangular_RoundTrip) +{ + RectGridStub aGrid(1.0, 1.0); + aGrid.SetSizeX(8.0); + aGrid.SetSizeY(4.0); + aGrid.SetZOffset(-0.005); + EXPECT_DOUBLE_EQ(8.0, aGrid.SizeX()); + EXPECT_DOUBLE_EQ(4.0, aGrid.SizeY()); + EXPECT_DOUBLE_EQ(-0.005, aGrid.ZOffset()); +} + +TEST(Aspect_GridBoundsTest, Circular_Defaults) +{ + CircGridStub aGrid(1.0, 8); + EXPECT_DOUBLE_EQ(0.0, aGrid.Radius()); + EXPECT_DOUBLE_EQ(0.0, aGrid.ZOffset()); + EXPECT_DOUBLE_EQ(0.0, aGrid.AngleStart()); + EXPECT_DOUBLE_EQ(0.0, aGrid.AngleEnd()); + EXPECT_FALSE(aGrid.IsArc()); +} + +TEST(Aspect_GridBoundsTest, Circular_RoundTrip) +{ + CircGridStub aGrid(1.0, 8); + aGrid.SetRadius(2.5); + aGrid.SetZOffset(-0.005); + EXPECT_DOUBLE_EQ(2.5, aGrid.Radius()); + EXPECT_DOUBLE_EQ(-0.005, aGrid.ZOffset()); +} + +TEST(Aspect_GridBoundsTest, Circular_ArcRange) +{ + CircGridStub aGrid(1.0, 8); + EXPECT_FALSE(aGrid.IsArc()); + + aGrid.SetArcRange(0.0, M_PI); + EXPECT_TRUE(aGrid.IsArc()); + EXPECT_DOUBLE_EQ(0.0, aGrid.AngleStart()); + EXPECT_DOUBLE_EQ(M_PI, aGrid.AngleEnd()); + + aGrid.SetArcRange(0.0, 0.0); + EXPECT_FALSE(aGrid.IsArc()) << "equal start/end is the full-circle sentinel"; + + aGrid.SetArcRange(2.356, -2.356); + EXPECT_TRUE(aGrid.IsArc()) << "wraparound (Start > End) is valid"; +} diff --git a/src/Visualization/TKService/GTests/FILES.cmake b/src/Visualization/TKService/GTests/FILES.cmake index 9a36c5bfdc..7066b92dd4 100644 --- a/src/Visualization/TKService/GTests/FILES.cmake +++ b/src/Visualization/TKService/GTests/FILES.cmake @@ -2,6 +2,8 @@ set(OCCT_TKService_GTests_FILES_LOCATION "${CMAKE_CURRENT_LIST_DIR}") set(OCCT_TKService_GTests_FILES + Aspect_GridParams_Test.cxx + Aspect_Grid_Bounds_Test.cxx Graphic3d_Aspects_Test.cxx Graphic3d_BndBox_Test.cxx Graphic3d_Flipper_Test.cxx diff --git a/src/Visualization/TKService/Graphic3d/Graphic3d_CView.hxx b/src/Visualization/TKService/Graphic3d/Graphic3d_CView.hxx index c544cd0dfe..4024107bc8 100644 --- a/src/Visualization/TKService/Graphic3d/Graphic3d_CView.hxx +++ b/src/Visualization/TKService/Graphic3d/Graphic3d_CView.hxx @@ -14,9 +14,11 @@ #ifndef _Graphic3d_CView_HeaderFile #define _Graphic3d_CView_HeaderFile +#include #include #include #include +#include #include #include #include @@ -438,6 +440,20 @@ public: //! @param[in] theToEnableIBL enable or disable IBL from background cubemap virtual void SetImageBasedLighting(bool theToEnableIBL) = 0; + //! Display a shader-rendered infinite grid on the given plane. + //! The default implementation is a no-op; drivers with shader support override it. + //! @param[in] theParams appearance parameters + //! @param[in] thePlane grid plane in world coordinates (origin + X/Y directions) + virtual void GridDisplay(const Aspect_GridParams& theParams, const gp_Ax3& thePlane) + { + (void)theParams; + (void)thePlane; + } + + //! Erase the shader-rendered infinite grid. + //! The default implementation is a no-op; drivers with shader support override it. + virtual void GridErase() {} + //! Returns environment texture set for the view. const occ::handle& TextureEnv() const { return myTextureEnvData; } diff --git a/src/Visualization/TKService/Graphic3d/Graphic3d_ShaderManager.cxx b/src/Visualization/TKService/Graphic3d/Graphic3d_ShaderManager.cxx index aad4f49946..81f9e59e4b 100644 --- a/src/Visualization/TKService/Graphic3d/Graphic3d_ShaderManager.cxx +++ b/src/Visualization/TKService/Graphic3d/Graphic3d_ShaderManager.cxx @@ -2148,3 +2148,243 @@ occ::handle Graphic3d_ShaderManager::getColoredQuadProg return aProgSrc; } + +//================================================================================================= + +occ::handle Graphic3d_ShaderManager::getGridProgram() const +{ + occ::handle aProgSrc = new Graphic3d_ShaderProgram(); + + Graphic3d_ShaderObject::ShaderVariableList aUniforms, aStageInOuts; + // Pass only NDC.xy to the fragment. Near/Far world points are computed + // per-pixel because the perspective-divide that unproject() performs is + // nonlinear in NDC, and linearly interpolating the resulting world-space + // points across the full-screen quad produces wrong rays along the + // diagonal seam between the two triangles (visible as empty triangular + // dead zones at oblique camera angles). + aStageInOuts.Append( + Graphic3d_ShaderObject::ShaderVariable("vec2 vNdc", + Graphic3d_TOS_VERTEX | Graphic3d_TOS_FRAGMENT)); + + aUniforms.Append(Graphic3d_ShaderObject::ShaderVariable("float uScaleX", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append(Graphic3d_ShaderObject::ShaderVariable("float uScaleY", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append( + Graphic3d_ShaderObject::ShaderVariable("float uThickness", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append(Graphic3d_ShaderObject::ShaderVariable("vec3 uColor", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append( + Graphic3d_ShaderObject::ShaderVariable("vec3 uAccentColor", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append( + Graphic3d_ShaderObject::ShaderVariable("float uAccentScaleX", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append( + Graphic3d_ShaderObject::ShaderVariable("float uAccentScaleY", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append( + Graphic3d_ShaderObject::ShaderVariable("float uAccentAngularScale", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append( + Graphic3d_ShaderObject::ShaderVariable("int uIsDrawAxis", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append( + Graphic3d_ShaderObject::ShaderVariable("int uIsBackground", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append( + Graphic3d_ShaderObject::ShaderVariable("vec3 uPlaneOrigin", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append(Graphic3d_ShaderObject::ShaderVariable("vec3 uPlaneX", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append(Graphic3d_ShaderObject::ShaderVariable("vec3 uPlaneY", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append(Graphic3d_ShaderObject::ShaderVariable("vec3 uPlaneN", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append(Graphic3d_ShaderObject::ShaderVariable("int uGridType", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append( + Graphic3d_ShaderObject::ShaderVariable("float uAngularScale", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append(Graphic3d_ShaderObject::ShaderVariable("int uDrawMode", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append( + Graphic3d_ShaderObject::ShaderVariable("vec2 uStableRefLocal", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append( + Graphic3d_ShaderObject::ShaderVariable("int uHasStableRef", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append(Graphic3d_ShaderObject::ShaderVariable("vec3 uBounds", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append( + Graphic3d_ShaderObject::ShaderVariable("vec2 uArcRange", Graphic3d_TOS_FRAGMENT)); + aUniforms.Append( + Graphic3d_ShaderObject::ShaderVariable("int uArcBounded", Graphic3d_TOS_FRAGMENT)); + + TCollection_AsciiString aSrcVert = + TCollection_AsciiString() + + EOL "const vec3 gridPlane[6] = vec3[] (" EOL + " vec3( 1.0, 1.0, 0.0), vec3(-1.0, -1.0, 0.0), vec3(-1.0, 1.0, 0.0)," EOL + " vec3(-1.0, -1.0, 0.0), vec3( 1.0, 1.0, 0.0), vec3( 1.0, -1.0, 0.0));" + + EOL "void main()" EOL "{" EOL " vec3 aVertex = gridPlane[gl_VertexID];" EOL + " vNdc = aVertex.xy;" EOL " gl_Position = vec4 (aVertex, 1.0);" EOL "}"; + + TCollection_AsciiString aSrcFrag = + TCollection_AsciiString() + + EOL + "vec4 gridLines2d (vec2 theUV, vec3 theColor, vec2 theScale," EOL + " bool theIsDrawAxis, float theThickness)" EOL "{" EOL + " vec2 aCoord = theUV * theScale;" EOL " vec2 aFwidth = fwidth (aCoord);" EOL + " vec2 aDerivative = max (aFwidth, vec2 (theThickness));" EOL + " vec2 aGrid = abs (fract (aCoord - 0.5) - 0.5) / aDerivative;" EOL + // Per-axis Nyquist fade: when more than ~1.5 grid cells map to a pixel in a given + // direction, that set of lines fades to zero instead of smearing into a bright band. + // Lines mode (uDrawMode!=1): bright when either axis shows a line -> max of per-axis alphas. + // Points mode (uDrawMode==1): bright only at intersections -> product of per-axis alphas. + EOL " vec2 aNyq = vec2 (1.0) - smoothstep (vec2 (1.0), vec2 (2.0), aFwidth);" EOL + " float aAlphaX = (1.0 - min (aGrid.x, 1.0)) * aNyq.x;" EOL + " float aAlphaY = (1.0 - min (aGrid.y, 1.0)) * aNyq.y;" EOL + " float aAlpha = uDrawMode == 1 ? aAlphaX * aAlphaY : max (aAlphaX, aAlphaY);" EOL + " float aMinY = min (aDerivative.y, 1.0);" EOL + " float aMinX = min (aDerivative.x, 1.0);" EOL + " vec4 aColor = vec4 (theColor, aAlpha);" EOL + // Axis colouring in lines mode only; a "point" has no axis direction. + EOL " if (uIsDrawAxis != 0 && theIsDrawAxis && uDrawMode != 1)" EOL " {" EOL + " bool isYAxis = abs (aCoord.x) < aMinX;" EOL + " bool isXAxis = abs (aCoord.y) < aMinY;" EOL + " if (isXAxis && isYAxis) { aColor.xyz = vec3 (0.0, 0.0, 1.0); }" EOL + " else if (isXAxis) { aColor.xyz = vec3 (1.0, 0.0, 0.0); }" EOL + " else if (isYAxis) { aColor.xyz = vec3 (0.0, 1.0, 0.0); }" EOL " }" EOL + " return aColor;" EOL "}" + + EOL "vec4 gridLines1d (float theCoord, float theScale, vec3 theColor, float theThickness)" EOL + "{" EOL " float aCoord = theCoord * theScale;" EOL + " float aFwidth = fwidth (aCoord);" EOL + " float aDerivative = max (aFwidth, theThickness);" EOL + " float aGrid = abs (fract (aCoord - 0.5) - 0.5) / aDerivative;" EOL + " float aNyq = 1.0 - smoothstep (1.0, 2.0, aFwidth);" EOL + " return vec4 (theColor, (1.0 - min (aGrid, 1.0)) * aNyq);" EOL "}" + + EOL "vec4 overlayGrid (vec4 theBase, vec4 theAccent)" EOL "{" EOL + " return theAccent.a >= theBase.a ? theAccent : theBase;" EOL "}" + + EOL "vec3 unproject (float theX, float theY, float theZ)" EOL "{" EOL + " vec4 aUnproj = occModelWorldMatrixInverse * occWorldViewMatrixInverse" EOL + " * occProjectionMatrixInverse * vec4 (theX, theY, theZ, 1.0);" EOL + " return aUnproj.xyz / aUnproj.w;" EOL "}" + + // sin(angle) threshold for "ray parallel to plane" - ~5.7e-5 deg. + EOL "const float GRID_PARALLEL_EPS = 1e-6;" + + EOL "bool intersectPlane (vec3 theNearPoint, vec3 theFarPoint, out vec3 theHit)" EOL "{" EOL + " vec3 aDir = theFarPoint - theNearPoint;" EOL + " float aDenomN = dot (uPlaneN, normalize (aDir));" EOL + " if (abs (aDenomN) < GRID_PARALLEL_EPS)" EOL " {" EOL " theHit = theNearPoint;" EOL + " return false;" EOL " }" EOL " float aDenom = dot (uPlaneN, aDir);" EOL + " float aT = dot (uPlaneN, uPlaneOrigin - theNearPoint) / aDenom;" EOL + " theHit = theNearPoint + aT * aDir;" EOL " return true;" EOL "}" EOL + + EOL "float computeDepth (vec3 thePos)" EOL "{" EOL + " mat4 aMVP = occProjectionMatrix * occWorldViewMatrix * occModelWorldMatrix;" EOL + " vec4 aClip = aMVP * vec4 (thePos, 1.0);" EOL " return aClip.z / aClip.w;" EOL "}" + + // Fade-band width (as a fraction of bound) applied inside the bound edge. + // Narrow smoothstep softens rectangular/disc cuts; angular cut stays hard. + EOL "const float GRID_FADE_BAND = 0.95;" + // Keep gl_FragDepth strictly less than 1.0 so the grid never lands exactly + // on the far plane (some drivers cull there). + EOL "const float GRID_DEPTH_EPS = 1e-5;" EOL "const float GRID_TWO_PI = 6.28318530718;" + + EOL "void main()" EOL "{" + // Unproject per-pixel from vNdc (linearly interpolated NDC.xy) to avoid the + // nonlinearity of the perspective divide when ray endpoints are passed as + // varyings. Each fragment reconstructs its own Near/Far world points. + EOL " vec3 aNearPoint = unproject (vNdc.x, vNdc.y, -1.0);" EOL + " vec3 aFarPoint = unproject (vNdc.x, vNdc.y, 1.0);" EOL " vec3 aHit;" EOL + " if (!intersectPlane (aNearPoint, aFarPoint, aHit)) { discard; }" + // Plane-local 2D coords, origin-centered to tame fp precision at large world offsets. + EOL " vec3 aLocal3 = aHit - uPlaneOrigin;" EOL + " vec2 aLocal = vec2 (dot (aLocal3, uPlaneX), dot (aLocal3, uPlaneY));" + + // Bounded work area. Rectangular, radial and angular clipping can be mixed. + EOL " float aR = length (aLocal);" EOL " float aA = atan (aLocal.y, aLocal.x);" EOL + " float aBoundFade = 1.0;" EOL " if (uBounds.z > 0.0)" EOL " {" EOL + " float aFwBR = fwidth (aR);" EOL " if (aR > uBounds.z + aFwBR) { discard; }" EOL + " aBoundFade *= 1.0 - smoothstep (GRID_FADE_BAND * uBounds.z, uBounds.z + aFwBR, aR);" EOL + " }" EOL " if (uArcBounded != 0)" EOL " {" EOL + " float aSpan = uArcRange.y - uArcRange.x;" EOL + " if (aSpan < 0.0) { aSpan += GRID_TWO_PI; }" EOL " float aDelta = aA - uArcRange.x;" EOL + " if (aDelta < 0.0) { aDelta += GRID_TWO_PI; }" EOL + " if (aDelta > aSpan) { discard; }" EOL " }" EOL " if (uBounds.x > 0.0)" EOL " {" EOL + // Extend the hard discard and smoothstep range by fwidth so the rectangular + // boundary gets sub-pixel AA (one screen-pixel transition) instead of a + // hard binary staircase at the clipping edge. + " float aFwBX = fwidth (abs (aLocal.x));" EOL + " if (abs (aLocal.x) > uBounds.x + aFwBX) { discard; }" EOL + " aBoundFade *= 1.0 - smoothstep (GRID_FADE_BAND * uBounds.x, uBounds.x + aFwBX, abs " + "(aLocal.x));" EOL " }" EOL " if (uBounds.y > 0.0)" EOL " {" EOL + " float aFwBY = fwidth (abs (aLocal.y));" EOL + " if (abs (aLocal.y) > uBounds.y + aFwBY) { discard; }" EOL + " aBoundFade *= 1.0 - smoothstep (GRID_FADE_BAND * uBounds.y, uBounds.y + aFwBY, abs " + "(aLocal.y));" EOL " }" + + // Grid coordinates depend on uGridType: 0=rectangular (X/Y), 1=circular (radius/angle). + // For rectangular grid, rebase UVs around a CPU-provided stable reference + // (same for all fragments in the frame) to keep fract() arguments small. + EOL " vec2 aStableLocal = aLocal;" EOL " if (uGridType == 0)" EOL " {" EOL + " if (uHasStableRef != 0)" EOL " {" EOL + " vec2 aScaleSafe = max (abs (vec2 (uScaleX, uScaleY)), vec2 (1e-9));" EOL + " vec2 aShift = floor (uStableRefLocal * aScaleSafe) / aScaleSafe;" EOL + " aStableLocal = aLocal - aShift;" EOL " }" EOL " }" EOL " vec2 aGridUv;" EOL + " vec2 aScale;" EOL " vec2 aAxisUv;" EOL " if (uGridType == 1)" EOL " {" EOL + " aGridUv = vec2 (aR, aA);" EOL " aScale = vec2 (uScaleX, uAngularScale);" EOL + " aAxisUv = aLocal;" EOL " }" EOL " else" EOL " {" EOL " aGridUv = aStableLocal;" EOL + " aScale = vec2 (uScaleX, uScaleY);" EOL " aAxisUv = aLocal;" EOL " }" EOL + " vec4 aColor = gridLines2d (aGridUv, uColor, aScale, uGridType == 0, uThickness);" EOL + " if (uDrawMode != 1)" EOL " {" EOL " if (uGridType == 0)" EOL " {" EOL + " if (uAccentScaleX > 0.0)" EOL " {" EOL " float aAccX = aLocal.x;" EOL + " if (uHasStableRef != 0)" EOL " {" EOL + " float aScaleAccX = max (abs (uAccentScaleX), 1e-9);" EOL + " float aShiftAccX = floor (uStableRefLocal.x * aScaleAccX) / aScaleAccX;" EOL + " aAccX -= aShiftAccX;" EOL " }" EOL + " aColor = overlayGrid (aColor, gridLines1d (aAccX, uAccentScaleX, uAccentColor, " + "uThickness));" EOL " }" EOL " if (uAccentScaleY > 0.0)" EOL " {" EOL + " float aAccY = aLocal.y;" EOL " if (uHasStableRef != 0)" EOL " {" EOL + " float aScaleAccY = max (abs (uAccentScaleY), 1e-9);" EOL + " float aShiftAccY = floor (uStableRefLocal.y * aScaleAccY) / aScaleAccY;" EOL + " aAccY -= aShiftAccY;" EOL " }" EOL + " aColor = overlayGrid (aColor, gridLines1d (aAccY, uAccentScaleY, uAccentColor, " + "uThickness));" EOL " }" EOL " }" EOL " else" EOL " {" EOL + " if (uAccentScaleX > 0.0)" EOL " {" EOL + " aColor = overlayGrid (aColor, gridLines1d (aR, uAccentScaleX, uAccentColor, " + "uThickness));" EOL " }" EOL " if (uAccentAngularScale > 0.0)" EOL " {" EOL + " aColor = overlayGrid (aColor, gridLines1d (aA, uAccentAngularScale, uAccentColor, " + "uThickness));" EOL " }" EOL " }" EOL + // For circular grid, paint X/Y axis lines explicitly using plane-local coords. + " if (uIsDrawAxis != 0 && uGridType == 1)" EOL " {" EOL + " vec2 aDerivAxis = max (fwidth (aAxisUv), vec2 (uThickness));" EOL + " if (abs (aAxisUv.x) < aDerivAxis.x && abs (aAxisUv.y) < aDerivAxis.y)" EOL " {" EOL + " aColor = vec4 (0.0, 0.0, 1.0, 1.0);" EOL " }" EOL + " else if (abs (aAxisUv.y) < aDerivAxis.y)" EOL " {" EOL + " aColor = vec4 (1.0, 0.0, 0.0, 1.0);" EOL " }" EOL + " else if (abs (aAxisUv.x) < aDerivAxis.x)" EOL " {" EOL + " aColor = vec4 (0.0, 1.0, 0.0, 1.0);" EOL " }" EOL " }" EOL " }" + + EOL " float aDepth = computeDepth (aHit);" EOL " float aFar = gl_DepthRange.far;" EOL + " float aNear = gl_DepthRange.near;" EOL + " aDepth = ((aFar - aNear) * aDepth + aNear + aFar) * 0.5;" + // No aT-sign discard: when the grid plane passes through / close to the + // near clip (the "grid going into camera view" case), aT can be <= 0 for + // fragments whose Near point is on the opposite side of the plane. The ray + // still intersects the plane; we just clamp depth via gl_FragDepth's [0,1] + // range so those fragments read as "in front of everything", which reads + // naturally as the ground/plane reaching under the camera. + EOL " if (aColor.a == 0.0) { discard; }" + + EOL " float aMaxDepth = 1.0 - GRID_DEPTH_EPS;" EOL + " gl_FragDepth = uIsBackground != 0 ? aMaxDepth : min (aDepth, aMaxDepth);" EOL + " aColor.a *= aBoundFade;" EOL " occFragColor = aColor;" EOL "}"; + + // Requires gl_VertexID (GL 3.0/ES 3.0+), fwidth, gl_FragDepth. + if (myGapi == Aspect_GraphicsLibrary_OpenGL) + { + aProgSrc->SetHeader(IsGapiGreaterEqual(3, 2) ? "#version 150" : "#version 130"); + } + else if (myGapi == Aspect_GraphicsLibrary_OpenGLES && IsGapiGreaterEqual(3, 0)) + { + aProgSrc->SetHeader("#version 300 es"); + } + + aProgSrc->AttachShader(Graphic3d_ShaderObject::CreateFromSource(aSrcVert, + Graphic3d_TOS_VERTEX, + aUniforms, + aStageInOuts)); + aProgSrc->AttachShader(Graphic3d_ShaderObject::CreateFromSource(aSrcFrag, + Graphic3d_TOS_FRAGMENT, + aUniforms, + aStageInOuts)); + + return aProgSrc; +} diff --git a/src/Visualization/TKService/Graphic3d/Graphic3d_ShaderManager.hxx b/src/Visualization/TKService/Graphic3d/Graphic3d_ShaderManager.hxx index fb2c5f1f90..f074916e09 100644 --- a/src/Visualization/TKService/Graphic3d/Graphic3d_ShaderManager.hxx +++ b/src/Visualization/TKService/Graphic3d/Graphic3d_ShaderManager.hxx @@ -156,6 +156,11 @@ protected: //! Generates shader program to render correctly colored quad. Standard_EXPORT occ::handle getColoredQuadProgram() const; + //! Generates shader program to render an infinite grid on an arbitrary plane. + //! Uses a screen-space triangle unprojected to world rays and ray-plane intersection + //! driven by uniforms uPlaneOrigin / uPlaneX / uPlaneY / uPlaneN. + Standard_EXPORT occ::handle getGridProgram() const; + //! Prepare GLSL source for IBL generation used in PBR pipeline. Standard_EXPORT occ::handle getPBREnvBakingProgram(int theIndex) const; diff --git a/src/Visualization/TKV3d/V3d/V3d_CircularGrid.cxx b/src/Visualization/TKV3d/V3d/V3d_CircularGrid.cxx index 7c1ac38ce7..a1aea8ce9e 100644 --- a/src/Visualization/TKV3d/V3d/V3d_CircularGrid.cxx +++ b/src/Visualization/TKV3d/V3d/V3d_CircularGrid.cxx @@ -13,374 +13,169 @@ #include -#include -#include -#include -#include -#include -#include +#include #include #include -#include -#include +#include #include +#include +#include IMPLEMENT_STANDARD_RTTIEXT(V3d_CircularGrid, Aspect_CircularGrid) namespace { constexpr double THE_DEFAULT_GRID_STEP = 10.0; -constexpr int THE_DIVISION = 8; +constexpr int THE_DEFAULT_DIVISION = 8; constexpr double THE_MYFACTOR = 50.0; } // namespace -//! Dummy implementation of Graphic3d_Structure overriding ::Compute() method for handling Device -//! Lost. -class V3d_CircularGrid::CircularGridStructure : public Graphic3d_Structure -{ -public: - //! Main constructor. - CircularGridStructure(const occ::handle& theManager, - V3d_CircularGrid* theGrid) - : Graphic3d_Structure(theManager), - myGrid(theGrid) - { - } - - //! Override method initiating recomputing in V3d_CircularGrid. - void Compute() override - { - GraphicClear(false); - myGrid->myGroup = NewGroup(); - myGrid->myCurAreDefined = false; - myGrid->UpdateDisplay(); - } - -private: - V3d_CircularGrid* myGrid; -}; - -/*----------------------------------------------------------------------*/ +//================================================================================================= V3d_CircularGrid::V3d_CircularGrid(const V3d_ViewerPointer& aViewer, const Quantity_Color& aColor, const Quantity_Color& aTenthColor) - : Aspect_CircularGrid(1., 8), + : Aspect_CircularGrid(1., THE_DEFAULT_DIVISION), myViewer(aViewer), - myCurAreDefined(false), - myToComputePrs(false), - myCurDrawMode(Aspect_GDM_Lines), - myCurXo(0.0), - myCurYo(0.0), - myCurAngle(0.0), - myCurStep(0.0), - myCurDivi(0), - myRadius(0.5 * aViewer->DefaultViewSize()), - myOffSet(THE_DEFAULT_GRID_STEP / THE_MYFACTOR) + myIsDisplayed(false) { myColor = aColor; myTenthColor = aTenthColor; - myStructure = new CircularGridStructure(aViewer->StructureManager(), this); - myGroup = myStructure->NewGroup(); - myStructure->SetInfiniteState(true); - SetRadiusStep(THE_DEFAULT_GRID_STEP); + Aspect_CircularGrid::SetRadius(0.5 * aViewer->DefaultViewSize()); + Aspect_CircularGrid::SetZOffset(THE_DEFAULT_GRID_STEP / THE_MYFACTOR); } +//================================================================================================= + V3d_CircularGrid::~V3d_CircularGrid() { - myGroup.Nullify(); - if (!myStructure.IsNull()) + if (myIsDisplayed) { - myStructure->Erase(); + syncViews(false); } } +//================================================================================================= + void V3d_CircularGrid::SetColors(const Quantity_Color& aColor, const Quantity_Color& aTenthColor) { if (myColor != aColor || myTenthColor != aTenthColor) { - myColor = aColor; - myTenthColor = aTenthColor; - myCurAreDefined = false; + myColor = aColor; + myTenthColor = aTenthColor; UpdateDisplay(); } } +//================================================================================================= + void V3d_CircularGrid::Display() { - myStructure->SetDisplayPriority(Graphic3d_DisplayPriority_AlmostBottom); - myStructure->Display(); - UpdateDisplay(); + myIsDisplayed = true; + syncViews(true); } +//================================================================================================= + void V3d_CircularGrid::Erase() const { - myStructure->Erase(); + myIsDisplayed = false; + syncViews(false); } +//================================================================================================= + bool V3d_CircularGrid::IsDisplayed() const { - return myStructure->IsDisplayed(); + return myIsDisplayed; } +//================================================================================================= + void V3d_CircularGrid::UpdateDisplay() { - gp_Ax3 ThePlane = myViewer->PrivilegedPlane(); - - double xl, yl, zl; - double xdx, xdy, xdz; - double ydx, ydy, ydz; - double dx, dy, dz; - ThePlane.Location().Coord(xl, yl, zl); - ThePlane.XDirection().Coord(xdx, xdy, xdz); - ThePlane.YDirection().Coord(ydx, ydy, ydz); - ThePlane.Direction().Coord(dx, dy, dz); - - bool MakeTransform = !myCurAreDefined; - if (!MakeTransform) + if (myIsDisplayed) { - MakeTransform = (RotationAngle() != myCurAngle || XOrigin() != myCurXo || YOrigin() != myCurYo); - if (!MakeTransform) - { - double curxl, curyl, curzl; - double curxdx, curxdy, curxdz; - double curydx, curydy, curydz; - double curdx, curdy, curdz; - myCurViewPlane.Location().Coord(curxl, curyl, curzl); - myCurViewPlane.XDirection().Coord(curxdx, curxdy, curxdz); - myCurViewPlane.YDirection().Coord(curydx, curydy, curydz); - myCurViewPlane.Direction().Coord(curdx, curdy, curdz); - if (xl != curxl || yl != curyl || zl != curzl || xdx != curxdx || xdy != curxdy - || xdz != curxdz || ydx != curydx || ydy != curydy || ydz != curydz || dx != curdx - || dy != curdy || dz != curdz) - MakeTransform = true; - } + syncViews(true); } - - if (MakeTransform) - { - const double CosAlpha = std::cos(RotationAngle()); - const double SinAlpha = std::sin(RotationAngle()); - - gp_Trsf aTrsf; - // Translation - // Transformation of change of marker - aTrsf.SetValues(xdx, ydx, dx, xl, xdy, ydy, dy, yl, xdz, ydz, dz, zl); - - // Translation of the origin - // Rotation Alpha around axis -Z - gp_Trsf aTrsf2; - aTrsf2.SetValues(CosAlpha, - SinAlpha, - 0.0, - -XOrigin(), - -SinAlpha, - CosAlpha, - 0.0, - -YOrigin(), - 0.0, - 0.0, - 1.0, - 0.0); - aTrsf.Multiply(aTrsf2); - myStructure->SetTransformation(new TopLoc_Datum3D(aTrsf)); - - myCurAngle = RotationAngle(); - myCurXo = XOrigin(), myCurYo = YOrigin(); - myCurViewPlane = ThePlane; - } - - switch (myDrawMode) - { - case Aspect_GDM_Points: - DefinePoints(); - myCurDrawMode = Aspect_GDM_Points; - break; - case Aspect_GDM_Lines: - DefineLines(); - myCurDrawMode = Aspect_GDM_Lines; - break; - case Aspect_GDM_None: - myCurDrawMode = Aspect_GDM_None; - break; - } - myCurAreDefined = true; } -void V3d_CircularGrid::DefineLines() -{ - const double aStep = RadiusStep(); - const double aDivision = DivisionNumber(); - const bool toUpdate = !myCurAreDefined || myCurDrawMode != Aspect_GDM_Lines - || aDivision != myCurDivi || aStep != myCurStep; - if (!toUpdate && !myToComputePrs) - { - return; - } - else if (!myStructure->IsDisplayed()) - { - myToComputePrs = true; - return; - } - - myToComputePrs = false; - myGroup->Clear(); - - const int Division = (int)((aDivision >= THE_DIVISION ? aDivision : THE_DIVISION)); - - int nbpnts = 2 * Division; - // diametres - double alpha = M_PI / aDivision; - - myGroup->SetGroupPrimitivesAspect( - new Graphic3d_AspectLine3d(myTenthColor, Aspect_TOL_SOLID, 1.0)); - occ::handle aPrims1 = new Graphic3d_ArrayOfSegments(2 * nbpnts); - const gp_Pnt p0(0., 0., -myOffSet); - for (int i = 1; i <= nbpnts; i++) - { - aPrims1->AddVertex(p0); - aPrims1->AddVertex(std::cos(alpha * i) * myRadius, std::sin(alpha * i) * myRadius, -myOffSet); - } - myGroup->AddPrimitiveArray(aPrims1, false); - - // circles - nbpnts = 2 * Division + 1; - alpha = M_PI / Division; - int nblines = 0; - NCollection_Sequence aSeqLines, aSeqTenth; - for (double r = aStep; r <= myRadius; r += aStep, nblines++) - { - const bool isTenth = (Modulus(nblines, 10) == 0); - for (int i = 0; i < nbpnts; i++) - { - const gp_Pnt pt(std::cos(alpha * i) * r, std::sin(alpha * i) * r, -myOffSet); - (isTenth ? aSeqTenth : aSeqLines).Append(pt); - } - } - if (aSeqTenth.Length()) - { - myGroup->SetGroupPrimitivesAspect( - new Graphic3d_AspectLine3d(myTenthColor, Aspect_TOL_SOLID, 1.0)); - int n, np; - const int nbl = aSeqTenth.Length() / nbpnts; - occ::handle aPrims2 = - new Graphic3d_ArrayOfPolylines(aSeqTenth.Length(), nbl); - for (np = 1, n = 0; n < nbl; n++) - { - aPrims2->AddBound(nbpnts); - for (int i = 0; i < nbpnts; i++, np++) - aPrims2->AddVertex(aSeqTenth(np)); - } - myGroup->AddPrimitiveArray(aPrims2, false); - } - if (aSeqLines.Length()) - { - myGroup->SetPrimitivesAspect(new Graphic3d_AspectLine3d(myColor, Aspect_TOL_SOLID, 1.0)); - int n, np; - const int nbl = aSeqLines.Length() / nbpnts; - occ::handle aPrims3 = - new Graphic3d_ArrayOfPolylines(aSeqLines.Length(), nbl); - for (np = 1, n = 0; n < nbl; n++) - { - aPrims3->AddBound(nbpnts); - for (int i = 0; i < nbpnts; i++, np++) - aPrims3->AddVertex(aSeqLines(np)); - } - myGroup->AddPrimitiveArray(aPrims3, false); - } - - myGroup->SetMinMaxValues(-myRadius, -myRadius, -myOffSet, myRadius, myRadius, -myOffSet); - myCurStep = aStep, myCurDivi = (int)aDivision; - - // update bounding box - myStructure->CalculateBoundBox(); - myViewer->StructureManager()->Update(myStructure->GetZLayer()); -} - -void V3d_CircularGrid::DefinePoints() -{ - const double aStep = RadiusStep(); - const double aDivision = DivisionNumber(); - const bool toUpdate = !myCurAreDefined || myCurDrawMode != Aspect_GDM_Points - || aDivision != myCurDivi || aStep != myCurStep; - if (!toUpdate && !myToComputePrs) - { - return; - } - else if (!myStructure->IsDisplayed()) - { - myToComputePrs = true; - return; - } - - myToComputePrs = false; - myGroup->Clear(); - - occ::handle MarkerAttrib = new Graphic3d_AspectMarker3d(); - MarkerAttrib->SetColor(myColor); - MarkerAttrib->SetType(Aspect_TOM_POINT); - MarkerAttrib->SetScale(3.); - - const int nbpnts = int(2 * aDivision); - double r, alpha = M_PI / aDivision; - - // diameters - NCollection_Sequence aSeqPnts; - aSeqPnts.Append(gp_Pnt(0.0, 0.0, -myOffSet)); - for (r = aStep; r <= myRadius; r += aStep) - { - for (int i = 0; i < nbpnts; i++) - aSeqPnts.Append(gp_Pnt(std::cos(alpha * i) * r, std::sin(alpha * i) * r, -myOffSet)); - } - myGroup->SetGroupPrimitivesAspect(MarkerAttrib); - if (aSeqPnts.Length()) - { - double X, Y, Z; - const int nbv = aSeqPnts.Length(); - occ::handle Cercle = new Graphic3d_ArrayOfPoints(nbv); - for (int i = 1; i <= nbv; i++) - { - aSeqPnts(i).Coord(X, Y, Z); - Cercle->AddVertex(X, Y, Z); - } - myGroup->AddPrimitiveArray(Cercle, false); - } - myGroup->SetMinMaxValues(-myRadius, -myRadius, -myOffSet, myRadius, myRadius, -myOffSet); - - myCurStep = aStep, myCurDivi = (int)aDivision; - - // update bounding box - myStructure->CalculateBoundBox(); - myViewer->StructureManager()->Update(myStructure->GetZLayer()); -} +//================================================================================================= void V3d_CircularGrid::GraphicValues(double& theRadius, double& theOffSet) const { - theRadius = myRadius; - theOffSet = myOffSet; + theRadius = Radius(); + theOffSet = ZOffset(); } +//================================================================================================= + void V3d_CircularGrid::SetGraphicValues(const double theRadius, const double theOffSet) { - if (!myCurAreDefined) + // Base-class setters each trigger UpdateDisplay only on real change. + SetRadius(theRadius); + SetZOffset(theOffSet); +} + +//================================================================================================= + +void V3d_CircularGrid::syncViews(const bool theDoDisplay) const +{ + if (myViewer == nullptr) { - myRadius = theRadius; - myOffSet = theOffSet; + return; } - if (myRadius != theRadius) + + const double aRadiusStep = RadiusStep() > 0.0 ? RadiusStep() : THE_DEFAULT_GRID_STEP; + const int aDivisions = DivisionNumber() > 0 ? DivisionNumber() : THE_DEFAULT_DIVISION; + + const gp_Ax3 aPlane = myViewer->PrivilegedPlane(); + + // Same convention as V3d_RectangularGrid::syncViews: origin is a world-space + // offset aligned with the plane basis so the shader's aPlaneOrigin matches + // V3d_View::Compute's aPnt0 used for snap selection. + const gp_XYZ aOriginOffset = + aPlane.XDirection().XYZ() * -XOrigin() + aPlane.YDirection().XYZ() * -YOrigin(); + + Aspect_GridParams aParams; + aParams.SetColor(myColor); + aParams.SetAccentColor(myTenthColor); + aParams.SetOrigin(gp_Pnt(aOriginOffset)); + aParams.SetScale(1.0 / aRadiusStep); + aParams.SetScaleY(0.0); // unused in circular mode + aParams.SetAccentScaleX(0.1 / aRadiusStep); + // Angular accent defaults OFF: classical OCCT circular grids have no + // "tenth spoke" concept. Spokes come from DivisionNumber and are already + // drawn by the base layer at full count. Setting this to a positive value + // (e.g. aDivisions / (k * M_PI) for k > 1) would highlight every k-th spoke. + aParams.SetAccentAngularScale(0.0); + aParams.SetRotationAngle(RotationAngle()); + aParams.SetAngularDivisions(aDivisions); + aParams.SetDrawMode(DrawMode()); + aParams.SetRadius(Radius()); + aParams.SetZOffset(ZOffset()); + aParams.SetArcRange(AngleStart(), AngleEnd()); + aParams.SetIsBackground(false); + aParams.SetIsDrawAxis(false); + aParams.SetIsInfinity(false); + + for (const occ::handle& aView : myViewer->DefinedViews()) { - myRadius = theRadius; - myCurAreDefined = false; + if (aView.IsNull()) + { + continue; + } + if (theDoDisplay) + { + aView->GridDisplay(aParams, aPlane); + } + else + { + aView->GridErase(); + } } - if (myOffSet != theOffSet) - { - myOffSet = theOffSet; - myCurAreDefined = false; - } - if (!myCurAreDefined) - UpdateDisplay(); } //================================================================================================= @@ -390,19 +185,6 @@ void V3d_CircularGrid::DumpJson(Standard_OStream& theOStream, int theDepth) cons OCCT_DUMP_TRANSIENT_CLASS_BEGIN(theOStream) OCCT_DUMP_BASE_CLASS(theOStream, theDepth, Aspect_CircularGrid) - OCCT_DUMP_FIELD_VALUES_DUMPED(theOStream, theDepth, myStructure.get()) - OCCT_DUMP_FIELD_VALUES_DUMPED(theOStream, theDepth, myGroup.get()) - OCCT_DUMP_FIELD_VALUES_DUMPED(theOStream, theDepth, &myCurViewPlane) OCCT_DUMP_FIELD_VALUE_POINTER(theOStream, myViewer) - OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myCurAreDefined) - OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myToComputePrs) - OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myCurDrawMode) - - OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myCurXo) - OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myCurYo) - OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myCurAngle) - OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myCurStep) - OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myCurDivi) - OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myRadius) - OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myOffSet) + OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myIsDisplayed) } diff --git a/src/Visualization/TKV3d/V3d/V3d_CircularGrid.hxx b/src/Visualization/TKV3d/V3d/V3d_CircularGrid.hxx index e8b838a674..4b924b4190 100644 --- a/src/Visualization/TKV3d/V3d/V3d_CircularGrid.hxx +++ b/src/Visualization/TKV3d/V3d/V3d_CircularGrid.hxx @@ -19,15 +19,12 @@ #include -#include -#include -#include -#include -#include #include -class Graphic3d_Structure; -class Graphic3d_Group; +#include +//! Circular grid bound to a V3d_Viewer. Snap math (Compute/Hit) is inherited +//! from Aspect_CircularGrid; rendering goes through the shader-based infinite +//! grid (OpenGl_View::renderGrid) with a polar branch. class V3d_CircularGrid : public Aspect_CircularGrid { DEFINE_STANDARD_RTTIEXT(V3d_CircularGrid, Aspect_CircularGrid) @@ -58,29 +55,13 @@ protected: Standard_EXPORT void UpdateDisplay() override; private: - Standard_EXPORT void DefineLines(); - - Standard_EXPORT void DefinePoints(); + //! Broadcast current parameters to every view owned by the viewer. + //! When theDoDisplay is false, erases the shader grid from each view instead. + void syncViews(const bool theDoDisplay) const; private: - //! Custom Graphic3d_Structure implementation. - class CircularGridStructure; - -private: - occ::handle myStructure; - occ::handle myGroup; - gp_Ax3 myCurViewPlane; - V3d_ViewerPointer myViewer; - bool myCurAreDefined; - bool myToComputePrs; - Aspect_GridDrawMode myCurDrawMode; - double myCurXo; - double myCurYo; - double myCurAngle; - double myCurStep; - int myCurDivi; - double myRadius; - double myOffSet; + V3d_ViewerPointer myViewer; + mutable bool myIsDisplayed; }; #endif // _V3d_CircularGrid_HeaderFile diff --git a/src/Visualization/TKV3d/V3d/V3d_RectangularGrid.cxx b/src/Visualization/TKV3d/V3d/V3d_RectangularGrid.cxx index 8021bc314c..be6c007ddd 100644 --- a/src/Visualization/TKV3d/V3d/V3d_RectangularGrid.cxx +++ b/src/Visualization/TKV3d/V3d/V3d_RectangularGrid.cxx @@ -13,16 +13,13 @@ #include -#include -#include -#include -#include -#include +#include #include #include -#include -#include +#include #include +#include +#include IMPLEMENT_STANDARD_RTTIEXT(V3d_RectangularGrid, Aspect_RectangularGrid) @@ -32,360 +29,158 @@ constexpr double THE_DEFAULT_GRID_STEP = 10.0; constexpr double THE_MYFACTOR = 50.0; } // namespace -//! Dummy implementation of Graphic3d_Structure overriding ::Compute() method for handling Device -//! Lost. -class V3d_RectangularGrid::RectangularGridStructure : public Graphic3d_Structure -{ -public: - //! Main constructor. - RectangularGridStructure(const occ::handle& theManager, - V3d_RectangularGrid* theGrid) - : Graphic3d_Structure(theManager), - myGrid(theGrid) - { - } - - //! Override method initiating recomputing in V3d_RectangularGrid. - void Compute() override - { - GraphicClear(false); - myGrid->myGroup = NewGroup(); - myGrid->myCurAreDefined = false; - myGrid->UpdateDisplay(); - } - -private: - V3d_RectangularGrid* myGrid; -}; - -/*----------------------------------------------------------------------*/ +//================================================================================================= V3d_RectangularGrid::V3d_RectangularGrid(const V3d_ViewerPointer& aViewer, const Quantity_Color& aColor, const Quantity_Color& aTenthColor) : Aspect_RectangularGrid(1., 1.), myViewer(aViewer), - myCurAreDefined(false), - myToComputePrs(true), - myCurDrawMode(Aspect_GDM_Lines), - myCurXo(0.0), - myCurYo(0.0), - myCurAngle(0.0), - myCurXStep(0.0), - myCurYStep(0.0), - myXSize(0.5 * aViewer->DefaultViewSize()), - myYSize(0.5 * aViewer->DefaultViewSize()), - myOffSet(THE_DEFAULT_GRID_STEP / THE_MYFACTOR) + myIsDisplayed(false) { myColor = aColor; myTenthColor = aTenthColor; - myStructure = new RectangularGridStructure(aViewer->StructureManager(), this); - myGroup = myStructure->NewGroup(); - myStructure->SetInfiniteState(true); - SetXStep(THE_DEFAULT_GRID_STEP); SetYStep(THE_DEFAULT_GRID_STEP); + // Keep rectangular grid unbounded by default; explicit -size / viewer API + // requests set non-zero SizeX/SizeY and activate clipping in the shader. + Aspect_RectangularGrid::SetZOffset(THE_DEFAULT_GRID_STEP / THE_MYFACTOR); } +//================================================================================================= + V3d_RectangularGrid::~V3d_RectangularGrid() { - myGroup.Nullify(); - if (!myStructure.IsNull()) + if (myIsDisplayed) { - myStructure->Erase(); + syncViews(false); } } +//================================================================================================= + void V3d_RectangularGrid::SetColors(const Quantity_Color& aColor, const Quantity_Color& aTenthColor) { if (myColor != aColor || myTenthColor != aTenthColor) { - myColor = aColor; - myTenthColor = aTenthColor; - myCurAreDefined = false; + myColor = aColor; + myTenthColor = aTenthColor; UpdateDisplay(); } } +//================================================================================================= + void V3d_RectangularGrid::Display() { - myStructure->SetDisplayPriority(Graphic3d_DisplayPriority_AlmostBottom); - myStructure->Display(); - UpdateDisplay(); + myIsDisplayed = true; + syncViews(true); } +//================================================================================================= + void V3d_RectangularGrid::Erase() const { - myStructure->Erase(); + myIsDisplayed = false; + syncViews(false); } +//================================================================================================= + bool V3d_RectangularGrid::IsDisplayed() const { - return myStructure->IsDisplayed(); + return myIsDisplayed; } +//================================================================================================= + void V3d_RectangularGrid::UpdateDisplay() { - gp_Ax3 ThePlane = myViewer->PrivilegedPlane(); - - bool MakeTransform = false; - double xl, yl, zl; - double xdx, xdy, xdz; - double ydx, ydy, ydz; - double dx, dy, dz; - ThePlane.Location().Coord(xl, yl, zl); - ThePlane.XDirection().Coord(xdx, xdy, xdz); - ThePlane.YDirection().Coord(ydx, ydy, ydz); - ThePlane.Direction().Coord(dx, dy, dz); - if (!myCurAreDefined) - MakeTransform = true; - else + if (myIsDisplayed) { - if (RotationAngle() != myCurAngle || XOrigin() != myCurXo || YOrigin() != myCurYo) - MakeTransform = true; - if (!MakeTransform) - { - double curxl, curyl, curzl; - double curxdx, curxdy, curxdz; - double curydx, curydy, curydz; - double curdx, curdy, curdz; - myCurViewPlane.Location().Coord(curxl, curyl, curzl); - myCurViewPlane.XDirection().Coord(curxdx, curxdy, curxdz); - myCurViewPlane.YDirection().Coord(curydx, curydy, curydz); - myCurViewPlane.Direction().Coord(curdx, curdy, curdz); - if (xl != curxl || yl != curyl || zl != curzl || xdx != curxdx || xdy != curxdy - || xdz != curxdz || ydx != curydx || ydy != curydy || ydz != curydz || dx != curdx - || dy != curdy || dz != curdz) - MakeTransform = true; - } + syncViews(true); } - - if (MakeTransform) - { - const double CosAlpha = std::cos(RotationAngle()); - const double SinAlpha = std::sin(RotationAngle()); - - gp_Trsf aTrsf; - // Translation - // Transformation of change of marker - aTrsf.SetValues(xdx, ydx, dx, xl, xdy, ydy, dy, yl, xdz, ydz, dz, zl); - - // Translation of the origin - // Rotation Alpha around axis -Z - gp_Trsf aTrsf2; - aTrsf2.SetValues(CosAlpha, - SinAlpha, - 0.0, - -XOrigin(), - -SinAlpha, - CosAlpha, - 0.0, - -YOrigin(), - 0.0, - 0.0, - 1.0, - 0.0); - aTrsf.Multiply(aTrsf2); - myStructure->SetTransformation(new TopLoc_Datum3D(aTrsf)); - - myCurAngle = RotationAngle(); - myCurXo = XOrigin(), myCurYo = YOrigin(); - myCurViewPlane = ThePlane; - } - - switch (myDrawMode) - { - case Aspect_GDM_Points: - DefinePoints(); - myCurDrawMode = Aspect_GDM_Points; - break; - case Aspect_GDM_Lines: - DefineLines(); - myCurDrawMode = Aspect_GDM_Lines; - break; - case Aspect_GDM_None: - myCurDrawMode = Aspect_GDM_None; - break; - } - myCurAreDefined = true; } -void V3d_RectangularGrid::DefineLines() -{ - const double aXStep = XStep(); - const double aYStep = YStep(); - const bool toUpdate = !myCurAreDefined || myCurDrawMode != Aspect_GDM_Lines - || aXStep != myCurXStep || aYStep != myCurYStep; - if (!toUpdate && !myToComputePrs) - { - return; - } - else if (!myStructure->IsDisplayed()) - { - myToComputePrs = true; - return; - } - - myToComputePrs = false; - myGroup->Clear(); - - int nblines; - double xl, yl, zl = myOffSet; - - NCollection_Sequence aSeqLines, aSeqTenth; - - // verticals - aSeqTenth.Append(gp_Pnt(0., -myYSize, -zl)); - aSeqTenth.Append(gp_Pnt(0., myYSize, -zl)); - for (nblines = 1, xl = aXStep; xl < myXSize; xl += aXStep, nblines++) - { - NCollection_Sequence& aSeq = (Modulus(nblines, 10) != 0) ? aSeqLines : aSeqTenth; - aSeq.Append(gp_Pnt(xl, -myYSize, -zl)); - aSeq.Append(gp_Pnt(xl, myYSize, -zl)); - aSeq.Append(gp_Pnt(-xl, -myYSize, -zl)); - aSeq.Append(gp_Pnt(-xl, myYSize, -zl)); - } - - // horizontals - aSeqTenth.Append(gp_Pnt(-myXSize, 0., -zl)); - aSeqTenth.Append(gp_Pnt(myXSize, 0., -zl)); - for (nblines = 1, yl = aYStep; yl < myYSize; yl += aYStep, nblines++) - { - NCollection_Sequence& aSeq = (Modulus(nblines, 10) != 0) ? aSeqLines : aSeqTenth; - aSeq.Append(gp_Pnt(-myXSize, yl, -zl)); - aSeq.Append(gp_Pnt(myXSize, yl, -zl)); - aSeq.Append(gp_Pnt(-myXSize, -yl, -zl)); - aSeq.Append(gp_Pnt(myXSize, -yl, -zl)); - } - - if (aSeqLines.Length()) - { - occ::handle aLineAspect = - new Graphic3d_AspectLine3d(myColor, Aspect_TOL_SOLID, 1.0); - myGroup->SetPrimitivesAspect(aLineAspect); - const int nbv = aSeqLines.Length(); - occ::handle aPrims = new Graphic3d_ArrayOfSegments(nbv); - int n = 1; - while (n <= nbv) - aPrims->AddVertex(aSeqLines(n++)); - myGroup->AddPrimitiveArray(aPrims, false); - } - if (aSeqTenth.Length()) - { - occ::handle aLineAspect = - new Graphic3d_AspectLine3d(myTenthColor, Aspect_TOL_SOLID, 1.0); - myGroup->SetPrimitivesAspect(aLineAspect); - const int nbv = aSeqTenth.Length(); - occ::handle aPrims = new Graphic3d_ArrayOfSegments(nbv); - int n = 1; - while (n <= nbv) - aPrims->AddVertex(aSeqTenth(n++)); - myGroup->AddPrimitiveArray(aPrims, false); - } - - myGroup->SetMinMaxValues(-myXSize, -myYSize, -myOffSet, myXSize, myYSize, -myOffSet); - myCurXStep = aXStep, myCurYStep = aYStep; - - // update bounding box - myStructure->CalculateBoundBox(); - myViewer->StructureManager()->Update(myStructure->GetZLayer()); -} - -void V3d_RectangularGrid::DefinePoints() -{ - const double aXStep = XStep(); - const double aYStep = YStep(); - const bool toUpdate = !myCurAreDefined || myCurDrawMode != Aspect_GDM_Points - || aXStep != myCurXStep || aYStep != myCurYStep; - if (!toUpdate && !myToComputePrs) - { - return; - } - else if (!myStructure->IsDisplayed()) - { - myToComputePrs = true; - return; - } - - myToComputePrs = false; - myGroup->Clear(); - - // horizontals - double xl, yl; - NCollection_Sequence aSeqPnts; - for (xl = 0.0; xl <= myXSize; xl += aXStep) - { - aSeqPnts.Append(gp_Pnt(xl, 0.0, -myOffSet)); - aSeqPnts.Append(gp_Pnt(-xl, 0.0, -myOffSet)); - for (yl = aYStep; yl <= myYSize; yl += aYStep) - { - aSeqPnts.Append(gp_Pnt(xl, yl, -myOffSet)); - aSeqPnts.Append(gp_Pnt(xl, -yl, -myOffSet)); - aSeqPnts.Append(gp_Pnt(-xl, yl, -myOffSet)); - aSeqPnts.Append(gp_Pnt(-xl, -yl, -myOffSet)); - } - } - if (aSeqPnts.Length()) - { - int i; - double X, Y, Z; - const int nbv = aSeqPnts.Length(); - occ::handle Vertical = new Graphic3d_ArrayOfPoints(nbv); - for (i = 1; i <= nbv; i++) - { - aSeqPnts(i).Coord(X, Y, Z); - Vertical->AddVertex(X, Y, Z); - } - - occ::handle aMarkerAspect = - new Graphic3d_AspectMarker3d(Aspect_TOM_POINT, myColor, 3.0); - myGroup->SetGroupPrimitivesAspect(aMarkerAspect); - myGroup->AddPrimitiveArray(Vertical, false); - } - - myGroup->SetMinMaxValues(-myXSize, -myYSize, -myOffSet, myXSize, myYSize, -myOffSet); - myCurXStep = aXStep, myCurYStep = aYStep; - - // update bounding box - myStructure->CalculateBoundBox(); - myViewer->StructureManager()->Update(myStructure->GetZLayer()); -} +//================================================================================================= void V3d_RectangularGrid::GraphicValues(double& theXSize, double& theYSize, double& theOffSet) const { - theXSize = myXSize; - theYSize = myYSize; - theOffSet = myOffSet; + theXSize = SizeX(); + theYSize = SizeY(); + theOffSet = ZOffset(); } +//================================================================================================= + void V3d_RectangularGrid::SetGraphicValues(const double theXSize, const double theYSize, const double theOffSet) { - if (!myCurAreDefined) + // The Aspect_RectangularGrid setters each trigger UpdateDisplay() only when the + // value actually changes, so the final UpdateDisplay fires at most once. + SetSizeX(theXSize); + SetSizeY(theYSize); + SetZOffset(theOffSet); +} + +//================================================================================================= + +void V3d_RectangularGrid::syncViews(const bool theDoDisplay) const +{ + if (myViewer == nullptr) { - myXSize = theXSize; - myYSize = theYSize; - myOffSet = theOffSet; + return; } - if (myXSize != theXSize) + + // XStep/YStep carry world-unit spacing; the shader consumes cells-per-unit (1/step). + // Guard zero/negative step that could come from Aspect_RectangularGrid defaults. + const double aXStep = XStep() > 0.0 ? XStep() : THE_DEFAULT_GRID_STEP; + const double aYStep = YStep() > 0.0 ? YStep() : THE_DEFAULT_GRID_STEP; + + const gp_Ax3 aPlane = myViewer->PrivilegedPlane(); + + // Pass the grid origin as a world-space offset from the plane origin, so the + // shader's aPlaneOrigin = planeLoc + Origin matches V3d_View::Compute's + // aPnt0 = planeLoc - XOrigin * planeX - YOrigin * planeY. Any other + // convention leaves selection snapping to a world point that doesn't line up + // with the drawn grid for tilted privileged planes. + const gp_XYZ aOriginOffset = + aPlane.XDirection().XYZ() * -XOrigin() + aPlane.YDirection().XYZ() * -YOrigin(); + + Aspect_GridParams aParams; + aParams.SetColor(myColor); + aParams.SetAccentColor(myTenthColor); + aParams.SetOrigin(gp_Pnt(aOriginOffset)); + aParams.SetScale(1.0 / aXStep); + aParams.SetScaleY(1.0 / aYStep); + aParams.SetAccentScaleX(0.1 / aXStep); + aParams.SetAccentScaleY(0.1 / aYStep); + aParams.SetRotationAngle(RotationAngle()); + aParams.SetDrawMode(DrawMode()); + aParams.SetSizeX(SizeX()); + aParams.SetSizeY(SizeY()); + aParams.SetZOffset(ZOffset()); + aParams.SetIsBackground(false); + aParams.SetIsDrawAxis(false); + aParams.SetIsInfinity(false); + + for (const occ::handle& aView : myViewer->DefinedViews()) { - myXSize = theXSize; - myCurAreDefined = false; + if (aView.IsNull()) + { + continue; + } + if (theDoDisplay) + { + aView->GridDisplay(aParams, aPlane); + } + else + { + aView->GridErase(); + } } - if (myYSize != theYSize) - { - myYSize = theYSize; - myCurAreDefined = false; - } - if (myOffSet != theOffSet) - { - myOffSet = theOffSet; - myCurAreDefined = false; - } - if (!myCurAreDefined) - UpdateDisplay(); } //================================================================================================= @@ -395,20 +190,6 @@ void V3d_RectangularGrid::DumpJson(Standard_OStream& theOStream, int theDepth) c OCCT_DUMP_TRANSIENT_CLASS_BEGIN(theOStream) OCCT_DUMP_BASE_CLASS(theOStream, theDepth, Aspect_RectangularGrid) - OCCT_DUMP_FIELD_VALUES_DUMPED(theOStream, theDepth, myStructure.get()) - OCCT_DUMP_FIELD_VALUES_DUMPED(theOStream, theDepth, myGroup.get()) - OCCT_DUMP_FIELD_VALUES_DUMPED(theOStream, theDepth, &myCurViewPlane) OCCT_DUMP_FIELD_VALUE_POINTER(theOStream, myViewer) - OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myCurAreDefined) - OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myToComputePrs) - OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myCurDrawMode) - - OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myCurXo) - OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myCurYo) - OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myCurAngle) - OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myCurXStep) - OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myCurYStep) - OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myXSize) - OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myYSize) - OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myOffSet) + OCCT_DUMP_FIELD_VALUE_NUMERICAL(theOStream, myIsDisplayed) } diff --git a/src/Visualization/TKV3d/V3d/V3d_RectangularGrid.hxx b/src/Visualization/TKV3d/V3d/V3d_RectangularGrid.hxx index e9cfc527f4..58d919f5b4 100644 --- a/src/Visualization/TKV3d/V3d/V3d_RectangularGrid.hxx +++ b/src/Visualization/TKV3d/V3d/V3d_RectangularGrid.hxx @@ -19,14 +19,12 @@ #include -#include -#include -#include -#include #include -class Graphic3d_Structure; -class Graphic3d_Group; +#include +//! Rectangular grid bound to a V3d_Viewer. Snap math (Compute/Hit) is inherited +//! from Aspect_RectangularGrid; rendering goes through the shader-based infinite +//! grid (OpenGl_View::renderGrid) instead of CPU-generated line segments. class V3d_RectangularGrid : public Aspect_RectangularGrid { DEFINE_STANDARD_RTTIEXT(V3d_RectangularGrid, Aspect_RectangularGrid) @@ -59,30 +57,13 @@ protected: Standard_EXPORT void UpdateDisplay() override; private: - Standard_EXPORT void DefineLines(); - - Standard_EXPORT void DefinePoints(); + //! Broadcast current parameters to every view owned by the viewer. + //! When theDoDisplay is false, erases the shader grid from each view instead. + void syncViews(const bool theDoDisplay) const; private: - //! Custom Graphic3d_Structure implementation. - class RectangularGridStructure; - -private: - occ::handle myStructure; - occ::handle myGroup; - gp_Ax3 myCurViewPlane; - V3d_ViewerPointer myViewer; - bool myCurAreDefined; - bool myToComputePrs; - Aspect_GridDrawMode myCurDrawMode; - double myCurXo; - double myCurYo; - double myCurAngle; - double myCurXStep; - double myCurYStep; - double myXSize; - double myYSize; - double myOffSet; + V3d_ViewerPointer myViewer; + mutable bool myIsDisplayed; }; #endif // _V3d_RectangularGrid_HeaderFile diff --git a/src/Visualization/TKV3d/V3d/V3d_View.cxx b/src/Visualization/TKV3d/V3d/V3d_View.cxx index 9ef12c2d64..856d7fc345 100644 --- a/src/Visualization/TKV3d/V3d/V3d_View.cxx +++ b/src/Visualization/TKV3d/V3d/V3d_View.cxx @@ -3402,6 +3402,27 @@ void V3d_View::SetGridActivity(const bool AFlag) //================================================================================================= +void V3d_View::GridDisplay(const Aspect_GridParams& theParams) +{ + GridDisplay(theParams, MyViewer->PrivilegedPlane()); +} + +//================================================================================================= + +void V3d_View::GridDisplay(const Aspect_GridParams& theParams, const gp_Ax3& thePlane) +{ + myView->GridDisplay(theParams, thePlane); +} + +//================================================================================================= + +void V3d_View::GridErase() +{ + myView->GridErase(); +} + +//================================================================================================= + void toPolarCoords(const double theX, const double theY, double& theR, double& thePhi) { theR = std::sqrt(theX * theX + theY * theY); diff --git a/src/Visualization/TKV3d/V3d/V3d_View.hxx b/src/Visualization/TKV3d/V3d/V3d_View.hxx index e6be78fbef..1c65675d34 100644 --- a/src/Visualization/TKV3d/V3d/V3d_View.hxx +++ b/src/Visualization/TKV3d/V3d/V3d_View.hxx @@ -17,6 +17,7 @@ #ifndef _V3d_View_HeaderFile #define _V3d_View_HeaderFile +#include #include #include #include @@ -914,6 +915,22 @@ public: //! grid in Standard_EXPORT void SetGridActivity(const bool aFlag); + //! Display a shader-rendered grid on the viewer's privileged plane. + //! @param[in] theParams render-only appearance (color, scale, bounds, arc, + //! draw-mode, background/inf flags); snap geometry still comes + //! from the classical Aspect_*Grid on the viewer. + Standard_EXPORT void GridDisplay(const Aspect_GridParams& theParams); + + //! Display a shader-rendered grid on an explicit plane (overrides the + //! viewer's privileged plane for this view only). + //! @param[in] theParams appearance parameters; see the single-argument overload. + //! @param[in] thePlane world-space grid plane (origin + axes). + Standard_EXPORT void GridDisplay(const Aspect_GridParams& theParams, const gp_Ax3& thePlane); + + //! Erase the shader-rendered grid from this view. Does not touch the + //! viewer's classical grid activation used by snap. + Standard_EXPORT void GridErase(); + //! Dumps the full contents of the View into the image file. This is an alias for ToPixMap() with //! Image_AlienPixMap. //! @param theFile destination image file (image format is determined by file extension like .png, diff --git a/tests/opengl/data/raytrace/bug29395 b/tests/opengl/data/raytrace/bug29395 index 10a21be211..7b5ef00bb6 100644 --- a/tests/opengl/data/raytrace/bug29395 +++ b/tests/opengl/data/raytrace/bug29395 @@ -12,6 +12,7 @@ vraytrace 0 vgrid -type rectangular vraytrace 1 checkcolor 198 197 0.5 0.5 0.5 +vdump $imagedir/${casename}_rectangular.png vclose # Circular Grid @@ -21,3 +22,5 @@ vraytrace 0 vgrid -type circular vraytrace 1 checkcolor 198 197 0.5 0.5 0.5 +vdump $imagedir/${casename}_circular.png +vclose diff --git a/tests/v3d/grid/bounded_circ b/tests/v3d/grid/bounded_circ new file mode 100644 index 0000000000..8c055b835c --- /dev/null +++ b/tests/v3d/grid/bounded_circ @@ -0,0 +1,33 @@ +puts "==================================================================" +puts "0030979: Bounded circular shader grid (-radius, -arc support)" +puts "==================================================================" + +pload MODELING VISUALIZATION + +vclear +vinit View1 w=400 h=400 +vaxo + +box b 1 1 0.1 +vdisplay b -dispMode 1 +vfit +vzoom 0.3 + +# Full disc within radius 2.5 +vgrid -type circ -step 0.25 16 -radius 2.5 +vdump $imagedir/${casename}_disc.png + +# Half circle (0..pi, walking CCW). +vgrid -type circ -step 0.25 16 -radius 2.5 -arc 0 3.14159 +vdump $imagedir/${casename}_halfcircle.png + +# Quarter (upper-right). +vgrid -type circ -step 0.25 16 -radius 2.5 -arc 0 1.5707 +vdump $imagedir/${casename}_quarter.png + +# Wraparound: 3pi/4 .. -3pi/4 covers the back pi/2 region by going CCW +# through +pi and wrapping to -pi (270-degree arc around the far side). +vgrid -type circ -step 0.25 16 -radius 2.5 -arc 2.356 -2.356 +vdump $imagedir/${casename}_wraparound.png + +vgrid off diff --git a/tests/v3d/grid/bounded_rect b/tests/v3d/grid/bounded_rect new file mode 100644 index 0000000000..755bc73b9f --- /dev/null +++ b/tests/v3d/grid/bounded_rect @@ -0,0 +1,30 @@ +puts "==================================================================" +puts "0030979: Bounded rectangular shader grid (-size X Y clips the area)" +puts "==================================================================" + +pload MODELING VISUALIZATION + +vclear +vinit View1 w=400 h=400 +vaxo + +box b 3 3 0.1 +vdisplay b -dispMode 1 +vfit +vzoom 0.5 + +# Full square bound: 4x4 world-unit patch centered at plane origin. +vgrid -type rect -step 0.5 0.5 -size 4 4 +vdump $imagedir/${casename}_full_square.png + +# Rectangular bound with different extents in X/Y. +# NOTE: vgrid -size expects strictly positive X and Y values. +vgrid -type rect -step 0.5 0.5 -size 4 8 +vdump $imagedir/${casename}_strip_x.png + +# Same width but with a slight z-offset so the grid sinks below the box's +# top face - used to avoid z-fighting in dense-coplanar scenes. +vgrid -type rect -step 0.5 0.5 -size 4 4 -zoffset -0.01 +vdump $imagedir/${casename}_zoffset.png + +vgrid off diff --git a/tests/v3d/grid/circ_shader b/tests/v3d/grid/circ_shader new file mode 100644 index 0000000000..ec124c886e --- /dev/null +++ b/tests/v3d/grid/circ_shader @@ -0,0 +1,28 @@ +puts "==================================================================" +puts "0030979: V3d_CircularGrid now renders through the polar shader path" +puts "==================================================================" + +pload MODELING VISUALIZATION + +vclear +vinit View1 +vaxo + +box b 1 2 3 +vdisplay b -dispMode 1 +vfit +vzoom 0.2 + +# Classical circular grid: 8 divisions (22.5 deg spokes), radius step 0.5 +vgrid -type circ -step 0.5 8 +vdump $imagedir/${casename}_circ.png + +# Finer angular resolution, bigger radial spacing +vgrid -type circ -step 1.0 24 +vdump $imagedir/${casename}_circ_fine.png + +# Rotated +vgrid -type circ -step 0.5 12 -rotAngle 0.4 +vdump $imagedir/${casename}_circ_rotated.png + +vgrid off diff --git a/tests/v3d/grid/inf_options b/tests/v3d/grid/inf_options new file mode 100644 index 0000000000..92dbf775ec --- /dev/null +++ b/tests/v3d/grid/inf_options @@ -0,0 +1,31 @@ +puts "==================================================================" +puts "0030979: vgrid -type inf keeps step, mode, rotation and disc/wedge" +puts "==================================================================" + +pload MODELING VISUALIZATION + +vclear +vinit View1 w=400 h=400 +vaxo + +box b 2 2 0.2 +vdisplay b -dispMode 1 +vfit +vzoom 0.25 + +# Step-driven anisotropic spacing and rotation must survive the final GridDisplay +# override; before the fix the display snapped back to default line mode / zero +# rotation whenever -type inf was used. +vgrid -type inf -step 0.5 1.0 -rotAngle 0.4 +vdump $imagedir/${casename}_rotated.png + +# Points mode used to be lost in the same override path. +vgrid -type inf -step 0.5 1.0 -mode points +vdump $imagedir/${casename}_points.png + +# Radius/arc now clip the infinite rectangular grid to a disc sector instead of +# being silently ignored. +vgrid -type inf -step 0.5 0.5 -radius 2.0 -arc 0 1.5707 +vdump $imagedir/${casename}_sector.png + +vgrid off diff --git a/tests/v3d/grid/inf_pan b/tests/v3d/grid/inf_pan new file mode 100644 index 0000000000..7579e913d1 --- /dev/null +++ b/tests/v3d/grid/inf_pan @@ -0,0 +1,21 @@ +puts "======================================================" +puts "0030979: shader infinite grid stays fixed during panning" +puts "======================================================" + +pload MODELING VISUALIZATION + +vclear +vinit View1 +vaxo +box b 1 2 3 +vdisplay b -dispMode 1 +vfit +vzoom 0.2 + +vgrid -type inf -background 1 +vdump $imagedir/${casename}_before.png + +vpan 80 40 +vdump $imagedir/${casename}_after.png + +vgrid off diff --git a/tests/v3d/grid/inf_plane b/tests/v3d/grid/inf_plane new file mode 100644 index 0000000000..8ea66ba754 --- /dev/null +++ b/tests/v3d/grid/inf_plane @@ -0,0 +1,20 @@ +puts "===========================================================" +puts "0030979: shader infinite grid follows non-default privileged" +puts "plane (YZ plane: normal +X)." +puts "===========================================================" + +pload MODELING VISUALIZATION + +vclear +vinit View1 +vaxo +box b 1 2 3 +vdisplay b -dispMode 1 +vfit +vzoom 0.2 + +vpriviledgedplane 0 0 0 1 0 0 0 1 0 +vgrid -type inf -color 0.2 0.8 0.3 +vdump $imagedir/${casename}_yz.png + +vgrid off diff --git a/tests/v3d/grid/inf_rotate b/tests/v3d/grid/inf_rotate new file mode 100644 index 0000000000..a8cb70435e --- /dev/null +++ b/tests/v3d/grid/inf_rotate @@ -0,0 +1,21 @@ +puts "=======================================================" +puts "0030979: shader infinite grid stays fixed during rotation" +puts "=======================================================" + +pload MODELING VISUALIZATION + +vclear +vinit View1 +vaxo +box b 1 2 3 +vdisplay b -dispMode 1 +vfit +vzoom 0.2 + +vgrid -type inf -background 1 +vdump $imagedir/${casename}_before.png + +vrotate 0 0 0.5 +vdump $imagedir/${casename}_after.png + +vgrid off diff --git a/tests/v3d/grid/ortho b/tests/v3d/grid/ortho new file mode 100644 index 0000000000..7ef8e506b9 --- /dev/null +++ b/tests/v3d/grid/ortho @@ -0,0 +1,26 @@ +puts "==================================================" +puts "0030979: Visualization - shader-based infinite grid" +puts "Orthographic projection" +puts "==================================================" + +pload MODELING VISUALIZATION + +vclear +vinit View1 +vaxo + +box b 1 2 3 +vdisplay b -dispMode 1 + +vcamera -ortho +vfit +vzoom 0.1 + +vgrid -type inf +vdump $imagedir/${casename}_0.png + +vgrid off +vgrid -type inf -background 1 -drawAxis 0 -color 0 0 1 -origin 1 1 -inf 1 -lineThickness 0.05 +vdump $imagedir/${casename}_1.png + +vgrid off diff --git a/tests/v3d/grid/persp b/tests/v3d/grid/persp new file mode 100644 index 0000000000..d8b229c723 --- /dev/null +++ b/tests/v3d/grid/persp @@ -0,0 +1,26 @@ +puts "==================================================" +puts "0030979: Visualization - shader-based infinite grid" +puts "Perspective projection" +puts "==================================================" + +pload MODELING VISUALIZATION + +vclear +vinit View1 +vaxo + +box b 1 2 3 +vdisplay b -dispMode 1 + +vcamera -persp +vfit +vzoom 0.1 + +vgrid -type inf +vdump $imagedir/${casename}_0.png + +vgrid off +vgrid -type inf -background 1 -drawAxis 0 -color 0 0 1 -origin 1 1 -inf 1 -lineThickness 0.05 +vdump $imagedir/${casename}_1.png + +vgrid off diff --git a/tests/v3d/grid/rect_points b/tests/v3d/grid/rect_points new file mode 100644 index 0000000000..e9ef4a2f50 --- /dev/null +++ b/tests/v3d/grid/rect_points @@ -0,0 +1,29 @@ +puts "==================================================================" +puts "0030979: Aspect_GDM_Points renders dots at rectangular grid intersections" +puts "==================================================================" + +pload MODELING VISUALIZATION + +vclear +vinit View1 +vaxo + +box b 1 2 3 +vdisplay b -dispMode 1 +vfit +vzoom 0.2 + +# Lines mode (default): full grid with horizontal and vertical lines. +vgrid -type rect -step 0.5 0.5 -mode lines +vdump $imagedir/${casename}_lines.png + +# Points mode: only the intersections light up (uDrawMode=1 in shader, +# alpha = aAlphaX * aAlphaY instead of max(aAlphaX, aAlphaY)). +vgrid -type rect -step 0.5 0.5 -mode points +vdump $imagedir/${casename}_points.png + +# Circular grid with points (dots at ring x spoke intersections). +vgrid -type circ -step 0.5 8 -mode points +vdump $imagedir/${casename}_circ_points.png + +vgrid off diff --git a/tests/v3d/grid/rect_shader b/tests/v3d/grid/rect_shader new file mode 100644 index 0000000000..d3f84adb4e --- /dev/null +++ b/tests/v3d/grid/rect_shader @@ -0,0 +1,29 @@ +puts "==================================================================" +puts "0030979: classical vgrid -type rect now renders through the shader" +puts "==================================================================" + +pload MODELING VISUALIZATION + +vclear +vinit View1 +vaxo + +box b 1 2 3 +vdisplay b -dispMode 1 +vfit +vzoom 0.2 + +# Classical rectangular grid, routed through the infinite-grid shader: +# XStep/YStep map to Scale/ScaleY, RotationAngle rotates in-plane basis. +vgrid -type rect -step 0.5 0.5 -rotAngle 0 +vdump $imagedir/${casename}_rect.png + +# Non-isotropic step (different X and Y spacing) +vgrid -type rect -step 0.5 1.0 +vdump $imagedir/${casename}_rect_aniso.png + +# In-plane rotation +vgrid -type rect -step 0.5 0.5 -rotAngle 0.5 +vdump $imagedir/${casename}_rect_rotated.png + +vgrid off diff --git a/tests/v3d/grids.list b/tests/v3d/grids.list index 4b19c0167b..f03134caa9 100755 --- a/tests/v3d/grids.list +++ b/tests/v3d/grids.list @@ -14,3 +14,4 @@ 014 trihedron 015 trsf 016 viewcube +017 grid