/* * This program source code file is part of KICAD, a free EDA CAD application. * * Copyright (C) 2012 Torsten Hueter, torstenhtr gmx.de * Copyright (C) 2012-2021 Kicad Developers, see AUTHORS.txt for contributors. * Copyright (C) 2017-2018 CERN * * @author Maciej Suminski * * CairoGal - Graphics Abstraction Layer for Cairo * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, you may find one here: * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html * or you may search the http://www.gnu.org website for the version 2 license, * or you may write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ #include #include #include #include #include #include #include // for KiROUND #include #include #include #include #include using namespace KIGFX; CAIRO_GAL_BASE::CAIRO_GAL_BASE( GAL_DISPLAY_OPTIONS& aDisplayOptions ) : GAL( aDisplayOptions ) { // Initialise grouping m_isGrouping = false; m_isElementAdded = false; m_groupCounter = 0; m_currentGroup = nullptr; m_lineWidth = 1.0; m_lineWidthInPixels = 1.0; m_lineWidthIsOdd = true; // Initialise Cairo state cairo_matrix_init_identity( &m_cairoWorldScreenMatrix ); m_currentContext = nullptr; m_context = nullptr; m_surface = nullptr; // Grid color settings are different in Cairo and OpenGL SetGridColor( COLOR4D( 0.1, 0.1, 0.1, 0.8 ) ); SetAxesColor( COLOR4D( BLUE ) ); // Avoid uninitialized variables: cairo_matrix_init_identity( &m_currentXform ); cairo_matrix_init_identity( &m_currentWorld2Screen ); } CAIRO_GAL_BASE::~CAIRO_GAL_BASE() { ClearCache(); if( m_surface ) cairo_surface_destroy( m_surface ); if( m_context ) cairo_destroy( m_context ); for( _cairo_surface* imageSurface : m_imageSurfaces ) cairo_surface_destroy( imageSurface ); } void CAIRO_GAL_BASE::beginDrawing() { resetContext(); } void CAIRO_GAL_BASE::endDrawing() { // Force remaining objects to be drawn Flush(); } void CAIRO_GAL_BASE::updateWorldScreenMatrix() { cairo_matrix_multiply( &m_currentWorld2Screen, &m_currentXform, &m_cairoWorldScreenMatrix ); } const VECTOR2D CAIRO_GAL_BASE::xform( double x, double y ) { VECTOR2D rv; rv.x = m_currentWorld2Screen.xx * x + m_currentWorld2Screen.xy * y + m_currentWorld2Screen.x0; rv.y = m_currentWorld2Screen.yx * x + m_currentWorld2Screen.yy * y + m_currentWorld2Screen.y0; return rv; } const VECTOR2D CAIRO_GAL_BASE::xform( const VECTOR2D& aP ) { return xform( aP.x, aP.y ); } const double CAIRO_GAL_BASE::angle_xform( const double aAngle ) { // calculate rotation angle due to the rotation transform // and if flipped on X axis. double world_rotation = -std::atan2( m_currentWorld2Screen.xy, m_currentWorld2Screen.xx ); // When flipped on X axis, the rotation angle is M_PI - initial angle: if( IsFlippedX() ) world_rotation = M_PI - world_rotation; return std::fmod( aAngle + world_rotation, 2.0 * M_PI ); } void CAIRO_GAL_BASE::arc_angles_xform_and_normalize( double& aStartAngle, double& aEndAngle ) { // 360 deg arcs have a specific calculation. bool is_360deg_arc = std::abs( aEndAngle - aStartAngle ) >= 2 * M_PI; double startAngle = aStartAngle; double endAngle = aEndAngle; // When the view is flipped, the coordinates are flipped by the matrix transform // However, arc angles need to be "flipped": the flipped angle is M_PI - initial angle. if( IsFlippedX() ) { startAngle = M_PI - startAngle; endAngle = M_PI - endAngle; } // Normalize arc angles SWAP( startAngle, >, endAngle ); // now rotate arc according to the rotation transform matrix // Remark: // We call angle_xform() to calculate angles according to the flip/rotation // transform and normalize between -2M_PI and +2M_PI. // Therefore, if aStartAngle = aEndAngle + 2*n*M_PI, the transform gives // aEndAngle = aStartAngle // So, if this is the case, force the aEndAngle value to draw a circle. aStartAngle = angle_xform( startAngle ); if( is_360deg_arc ) // arc is a full circle aEndAngle = aStartAngle + 2 * M_PI; else aEndAngle = angle_xform( endAngle ); } const double CAIRO_GAL_BASE::xform( double x ) { double dx = m_currentWorld2Screen.xx * x; double dy = m_currentWorld2Screen.yx * x; return sqrt( dx * dx + dy * dy ); } static double roundp( double x ) { return floor( x + 0.5 ) + 0.5; } const VECTOR2D CAIRO_GAL_BASE::roundp( const VECTOR2D& v ) { if( m_lineWidthIsOdd && m_isStrokeEnabled ) return VECTOR2D( ::roundp( v.x ), ::roundp( v.y ) ); else return VECTOR2D( floor( v.x + 0.5 ), floor( v.y + 0.5 ) ); } void CAIRO_GAL_BASE::DrawLine( const VECTOR2D& aStartPoint, const VECTOR2D& aEndPoint ) { syncLineWidth(); VECTOR2D p0 = roundp( xform( aStartPoint ) ); VECTOR2D p1 = roundp( xform( aEndPoint ) ); cairo_move_to( m_currentContext, p0.x, p0.y ); cairo_line_to( m_currentContext, p1.x, p1.y ); flushPath(); m_isElementAdded = true; } void CAIRO_GAL_BASE::syncLineWidth( bool aForceWidth, double aWidth ) { double w = floor( xform( aForceWidth ? aWidth : m_lineWidth ) + 0.5 ); if( w <= 1.0 ) { w = 1.0; cairo_set_line_join( m_currentContext, CAIRO_LINE_JOIN_MITER ); cairo_set_line_cap( m_currentContext, CAIRO_LINE_CAP_BUTT ); cairo_set_line_width( m_currentContext, 1.0 ); m_lineWidthIsOdd = true; } else { cairo_set_line_join( m_currentContext, CAIRO_LINE_JOIN_ROUND ); cairo_set_line_cap( m_currentContext, CAIRO_LINE_CAP_ROUND ); cairo_set_line_width( m_currentContext, w ); m_lineWidthIsOdd = ( (int) w % 2 ) == 1; } m_lineWidthInPixels = w; } void CAIRO_GAL_BASE::DrawSegment( const VECTOR2D& aStartPoint, const VECTOR2D& aEndPoint, double aWidth ) { if( m_isFillEnabled ) { syncLineWidth( true, aWidth ); VECTOR2D p0 = roundp( xform( aStartPoint ) ); VECTOR2D p1 = roundp( xform( aEndPoint ) ); cairo_move_to( m_currentContext, p0.x, p0.y ); cairo_line_to( m_currentContext, p1.x, p1.y ); cairo_set_source_rgba( m_currentContext, m_fillColor.r, m_fillColor.g, m_fillColor.b, m_fillColor.a ); cairo_stroke( m_currentContext ); } else { aWidth /= 2.0; SetLineWidth( 1.0 ); syncLineWidth(); // Outline mode for tracks VECTOR2D startEndVector = aEndPoint - aStartPoint; double lineAngle = atan2( startEndVector.y, startEndVector.x ); double sa = sin( lineAngle + M_PI / 2.0 ); double ca = cos( lineAngle + M_PI / 2.0 ); auto pa0 = xform( aStartPoint + VECTOR2D( aWidth * ca, aWidth * sa ) ); auto pa1 = xform( aStartPoint - VECTOR2D( aWidth * ca, aWidth * sa ) ); auto pb0 = xform( aEndPoint + VECTOR2D( aWidth * ca, aWidth * sa ) ); auto pb1 = xform( aEndPoint - VECTOR2D( aWidth * ca, aWidth * sa ) ); auto pa = xform( aStartPoint ); auto pb = xform( aEndPoint ); auto rb = ( pa0 - pa ).EuclideanNorm(); cairo_set_source_rgba( m_currentContext, m_strokeColor.r, m_strokeColor.g, m_strokeColor.b, m_strokeColor.a ); cairo_move_to( m_currentContext, pa0.x, pa0.y ); cairo_line_to( m_currentContext, pb0.x, pb0.y ); cairo_move_to( m_currentContext, pa1.x, pa1.y ); cairo_line_to( m_currentContext, pb1.x, pb1.y ); cairo_arc( m_currentContext, pb.x, pb.y, rb, lineAngle - M_PI / 2.0, lineAngle + M_PI / 2.0 ); cairo_arc( m_currentContext, pa.x, pa.y, rb, lineAngle + M_PI / 2.0, lineAngle + 3.0 * M_PI / 2.0 ); flushPath(); } m_isElementAdded = true; } void CAIRO_GAL_BASE::DrawCircle( const VECTOR2D& aCenterPoint, double aRadius ) { syncLineWidth(); VECTOR2D c = roundp( xform( aCenterPoint ) ); double r = ::roundp( xform( aRadius ) ); cairo_set_line_width( m_currentContext, std::min( 2.0 * r, m_lineWidthInPixels ) ); cairo_new_sub_path( m_currentContext ); cairo_arc( m_currentContext, c.x, c.y, r, 0.0, 2 * M_PI ); cairo_close_path( m_currentContext ); flushPath(); m_isElementAdded = true; } void CAIRO_GAL_BASE::DrawArc( const VECTOR2D& aCenterPoint, double aRadius, double aStartAngle, double aEndAngle ) { syncLineWidth(); // calculate start and end arc angles according to the rotation transform matrix // and normalize: arc_angles_xform_and_normalize( aStartAngle, aEndAngle ); double r = xform( aRadius ); // N.B. This is backwards. We set this because we want to adjust the center // point that changes both endpoints. In the worst case, this is twice as far. // We cannot adjust radius or center based on the other because this causes the // whole arc to change position/size m_lineWidthIsOdd = !( static_cast( aRadius ) % 2 ); auto mid = roundp( xform( aCenterPoint ) ); cairo_set_line_width( m_currentContext, m_lineWidthInPixels ); cairo_new_sub_path( m_currentContext ); if( m_isFillEnabled ) cairo_move_to( m_currentContext, mid.x, mid.y ); cairo_arc( m_currentContext, mid.x, mid.y, r, aStartAngle, aEndAngle ); if( m_isFillEnabled ) cairo_close_path( m_currentContext ); flushPath(); m_isElementAdded = true; } void CAIRO_GAL_BASE::DrawArcSegment( const VECTOR2D& aCenterPoint, double aRadius, double aStartAngle, double aEndAngle, double aWidth, double aMaxError ) { // Note: aMaxError is not used because Cairo can draw true arcs if( m_isFillEnabled ) { m_lineWidth = aWidth; m_isStrokeEnabled = true; m_isFillEnabled = false; DrawArc( aCenterPoint, aRadius, aStartAngle, aEndAngle ); m_isFillEnabled = true; m_isStrokeEnabled = false; return; } syncLineWidth(); // calculate start and end arc angles according to the rotation transform matrix // and normalize: double startAngleS = aStartAngle; double endAngleS = aEndAngle; arc_angles_xform_and_normalize( startAngleS, endAngleS ); double r = xform( aRadius ); // N.B. This is backwards. We set this because we want to adjust the center // point that changes both endpoints. In the worst case, this is twice as far. // We cannot adjust radius or center based on the other because this causes the // whole arc to change position/size m_lineWidthIsOdd = !( static_cast( aRadius ) % 2 ); VECTOR2D mid = roundp( xform( aCenterPoint ) ); double width = xform( aWidth / 2.0 ); VECTOR2D startPointS = VECTOR2D( r, 0.0 ).Rotate( startAngleS ); VECTOR2D endPointS = VECTOR2D( r, 0.0 ).Rotate( endAngleS ); cairo_save( m_currentContext ); cairo_set_source_rgba( m_currentContext, m_strokeColor.r, m_strokeColor.g, m_strokeColor.b, m_strokeColor.a ); cairo_translate( m_currentContext, mid.x, mid.y ); cairo_new_sub_path( m_currentContext ); cairo_arc( m_currentContext, 0, 0, r - width, startAngleS, endAngleS ); cairo_new_sub_path( m_currentContext ); cairo_arc( m_currentContext, 0, 0, r + width, startAngleS, endAngleS ); cairo_new_sub_path( m_currentContext ); cairo_arc_negative( m_currentContext, startPointS.x, startPointS.y, width, startAngleS, startAngleS + M_PI ); cairo_new_sub_path( m_currentContext ); cairo_arc( m_currentContext, endPointS.x, endPointS.y, width, endAngleS, endAngleS + M_PI ); cairo_restore( m_currentContext ); flushPath(); m_isElementAdded = true; } void CAIRO_GAL_BASE::DrawRectangle( const VECTOR2D& aStartPoint, const VECTOR2D& aEndPoint ) { // Calculate the diagonal points syncLineWidth(); const VECTOR2D p0 = roundp( xform( aStartPoint ) ); const VECTOR2D p1 = roundp( xform( VECTOR2D( aEndPoint.x, aStartPoint.y ) ) ); const VECTOR2D p2 = roundp( xform( aEndPoint ) ); const VECTOR2D p3 = roundp( xform( VECTOR2D( aStartPoint.x, aEndPoint.y ) ) ); // The path is composed from 4 segments cairo_move_to( m_currentContext, p0.x, p0.y ); cairo_line_to( m_currentContext, p1.x, p1.y ); cairo_line_to( m_currentContext, p2.x, p2.y ); cairo_line_to( m_currentContext, p3.x, p3.y ); cairo_close_path( m_currentContext ); flushPath(); m_isElementAdded = true; } void CAIRO_GAL_BASE::DrawPolygon( const SHAPE_POLY_SET& aPolySet, bool aStrokeTriangulation ) { for( int i = 0; i < aPolySet.OutlineCount(); ++i ) drawPoly( aPolySet.COutline( i ) ); } void CAIRO_GAL_BASE::DrawPolygon( const SHAPE_LINE_CHAIN& aPolygon ) { drawPoly( aPolygon ); } void CAIRO_GAL_BASE::DrawCurve( const VECTOR2D& aStartPoint, const VECTOR2D& aControlPointA, const VECTOR2D& aControlPointB, const VECTOR2D& aEndPoint, double aFilterValue ) { // Note: aFilterValue is not used because the cubic Bezier curve is // supported by Cairo. syncLineWidth(); const VECTOR2D sp = roundp( xform( aStartPoint ) ); const VECTOR2D cpa = roundp( xform( aControlPointA ) ); const VECTOR2D cpb = roundp( xform( aControlPointB ) ); const VECTOR2D ep = roundp( xform( aEndPoint ) ); cairo_move_to( m_currentContext, sp.x, sp.y ); cairo_curve_to( m_currentContext, cpa.x, cpa.y, cpb.x, cpb.y, ep.x, ep.y ); cairo_line_to( m_currentContext, ep.x, ep.y ); flushPath(); m_isElementAdded = true; } void CAIRO_GAL_BASE::DrawBitmap( const BITMAP_BASE& aBitmap ) { cairo_save( m_currentContext ); // We have to calculate the pixel size in users units to draw the image. // m_worldUnitLength is a factor used for converting IU to inches double scale = 1.0 / ( aBitmap.GetPPI() * m_worldUnitLength ); // The position of the bitmap is the bitmap center. // move the draw origin to the top left bitmap corner: int w = aBitmap.GetSizePixels().x; int h = aBitmap.GetSizePixels().y; cairo_set_matrix( m_currentContext, &m_currentWorld2Screen ); cairo_scale( m_currentContext, scale, scale ); cairo_translate( m_currentContext, -w / 2.0, -h / 2.0 ); cairo_new_path( m_currentContext ); cairo_surface_t* image = cairo_image_surface_create( CAIRO_FORMAT_ARGB32, w, h ); cairo_surface_flush( image ); unsigned char* pix_buffer = cairo_image_surface_get_data( image ); // The pixel buffer of the initial bitmap: const wxImage& bm_pix_buffer = *aBitmap.GetImageData(); uint32_t mask_color = ( bm_pix_buffer.GetMaskRed() << 16 ) + ( bm_pix_buffer.GetMaskGreen() << 8 ) + ( bm_pix_buffer.GetMaskBlue() ); // Copy the source bitmap to the cairo bitmap buffer. // In cairo bitmap buffer, a ARGB32 bitmap is an ARGB pixel packed into a uint_32 // 24 low bits only are used for color, top 8 are transparency. for( int row = 0; row < h; row++ ) { for( int col = 0; col < w; col++ ) { // Build the RGB24 pixel: uint32_t pixel = bm_pix_buffer.GetRed( col, row ) << 16; pixel += bm_pix_buffer.GetGreen( col, row ) << 8; pixel += bm_pix_buffer.GetBlue( col, row ); if( bm_pix_buffer.HasAlpha() ) pixel += bm_pix_buffer.GetAlpha( col, row ) << 24; else if( bm_pix_buffer.HasMask() && pixel == mask_color ) pixel += ( wxALPHA_TRANSPARENT << 24 ); else pixel += ( wxALPHA_OPAQUE << 24 ); // Write the pixel to the cairo image buffer: uint32_t* pix_ptr = (uint32_t*) pix_buffer; *pix_ptr = pixel; pix_buffer += 4; } } cairo_surface_mark_dirty( image ); cairo_set_source_surface( m_currentContext, image, 0, 0 ); cairo_paint( m_currentContext ); // store the image handle so it can be destroyed later m_imageSurfaces.push_back( image ); m_isElementAdded = true; cairo_restore( m_currentContext ); } void CAIRO_GAL_BASE::ResizeScreen( int aWidth, int aHeight ) { m_screenSize = VECTOR2I( aWidth, aHeight ); } void CAIRO_GAL_BASE::Flush() { storePath(); } void CAIRO_GAL_BASE::ClearScreen() { cairo_set_source_rgb( m_currentContext, m_clearColor.r, m_clearColor.g, m_clearColor.b ); cairo_rectangle( m_currentContext, 0.0, 0.0, m_screenSize.x, m_screenSize.y ); cairo_fill( m_currentContext ); } void CAIRO_GAL_BASE::SetIsFill( bool aIsFillEnabled ) { storePath(); m_isFillEnabled = aIsFillEnabled; if( m_isGrouping ) { GROUP_ELEMENT groupElement; groupElement.m_Command = CMD_SET_FILL; groupElement.m_Argument.BoolArg = aIsFillEnabled; m_currentGroup->push_back( groupElement ); } } void CAIRO_GAL_BASE::SetIsStroke( bool aIsStrokeEnabled ) { storePath(); m_isStrokeEnabled = aIsStrokeEnabled; if( m_isGrouping ) { GROUP_ELEMENT groupElement; groupElement.m_Command = CMD_SET_STROKE; groupElement.m_Argument.BoolArg = aIsStrokeEnabled; m_currentGroup->push_back( groupElement ); } } void CAIRO_GAL_BASE::SetStrokeColor( const COLOR4D& aColor ) { storePath(); m_strokeColor = aColor; if( m_isGrouping ) { GROUP_ELEMENT groupElement; groupElement.m_Command = CMD_SET_STROKECOLOR; groupElement.m_Argument.DblArg[0] = m_strokeColor.r; groupElement.m_Argument.DblArg[1] = m_strokeColor.g; groupElement.m_Argument.DblArg[2] = m_strokeColor.b; groupElement.m_Argument.DblArg[3] = m_strokeColor.a; m_currentGroup->push_back( groupElement ); } } void CAIRO_GAL_BASE::SetFillColor( const COLOR4D& aColor ) { storePath(); m_fillColor = aColor; if( m_isGrouping ) { GROUP_ELEMENT groupElement; groupElement.m_Command = CMD_SET_FILLCOLOR; groupElement.m_Argument.DblArg[0] = m_fillColor.r; groupElement.m_Argument.DblArg[1] = m_fillColor.g; groupElement.m_Argument.DblArg[2] = m_fillColor.b; groupElement.m_Argument.DblArg[3] = m_fillColor.a; m_currentGroup->push_back( groupElement ); } } void CAIRO_GAL_BASE::SetLineWidth( float aLineWidth ) { storePath(); GAL::SetLineWidth( aLineWidth ); if( m_isGrouping ) { GROUP_ELEMENT groupElement; groupElement.m_Command = CMD_SET_LINE_WIDTH; groupElement.m_Argument.DblArg[0] = aLineWidth; m_currentGroup->push_back( groupElement ); } else { m_lineWidth = aLineWidth; } } void CAIRO_GAL_BASE::SetLayerDepth( double aLayerDepth ) { super::SetLayerDepth( aLayerDepth ); storePath(); } void CAIRO_GAL_BASE::Transform( const MATRIX3x3D& aTransformation ) { cairo_matrix_t cairoTransformation, newXform; cairo_matrix_init( &cairoTransformation, aTransformation.m_data[0][0], aTransformation.m_data[1][0], aTransformation.m_data[0][1], aTransformation.m_data[1][1], aTransformation.m_data[0][2], aTransformation.m_data[1][2] ); cairo_matrix_multiply( &newXform, &m_currentXform, &cairoTransformation ); m_currentXform = newXform; updateWorldScreenMatrix(); } void CAIRO_GAL_BASE::Rotate( double aAngle ) { storePath(); if( m_isGrouping ) { GROUP_ELEMENT groupElement; groupElement.m_Command = CMD_ROTATE; groupElement.m_Argument.DblArg[0] = aAngle; m_currentGroup->push_back( groupElement ); } else { cairo_matrix_rotate( &m_currentXform, aAngle ); updateWorldScreenMatrix(); } } void CAIRO_GAL_BASE::Translate( const VECTOR2D& aTranslation ) { storePath(); if( m_isGrouping ) { GROUP_ELEMENT groupElement; groupElement.m_Command = CMD_TRANSLATE; groupElement.m_Argument.DblArg[0] = aTranslation.x; groupElement.m_Argument.DblArg[1] = aTranslation.y; m_currentGroup->push_back( groupElement ); } else { cairo_matrix_translate( &m_currentXform, aTranslation.x, aTranslation.y ); updateWorldScreenMatrix(); } } void CAIRO_GAL_BASE::Scale( const VECTOR2D& aScale ) { storePath(); if( m_isGrouping ) { GROUP_ELEMENT groupElement; groupElement.m_Command = CMD_SCALE; groupElement.m_Argument.DblArg[0] = aScale.x; groupElement.m_Argument.DblArg[1] = aScale.y; m_currentGroup->push_back( groupElement ); } else { cairo_matrix_scale( &m_currentXform, aScale.x, aScale.y ); updateWorldScreenMatrix(); } } void CAIRO_GAL_BASE::Save() { storePath(); if( m_isGrouping ) { GROUP_ELEMENT groupElement; groupElement.m_Command = CMD_SAVE; m_currentGroup->push_back( groupElement ); } else { m_xformStack.push_back( m_currentXform ); updateWorldScreenMatrix(); } } void CAIRO_GAL_BASE::Restore() { storePath(); if( m_isGrouping ) { GROUP_ELEMENT groupElement; groupElement.m_Command = CMD_RESTORE; m_currentGroup->push_back( groupElement ); } else { if( !m_xformStack.empty() ) { m_currentXform = m_xformStack.back(); m_xformStack.pop_back(); updateWorldScreenMatrix(); } } } int CAIRO_GAL_BASE::BeginGroup() { // If the grouping is started: the actual path is stored in the group, when // a attribute was changed or when grouping stops with the end group method. storePath(); GROUP group; int groupNumber = getNewGroupNumber(); m_groups.insert( std::make_pair( groupNumber, group ) ); m_currentGroup = &m_groups[groupNumber]; m_isGrouping = true; return groupNumber; } void CAIRO_GAL_BASE::EndGroup() { storePath(); m_isGrouping = false; } void CAIRO_GAL_BASE::DrawGroup( int aGroupNumber ) { // This method implements a small Virtual Machine - all stored commands // are executed; nested calling is also possible storePath(); for( auto it = m_groups[aGroupNumber].begin(); it != m_groups[aGroupNumber].end(); ++it ) { switch( it->m_Command ) { case CMD_SET_FILL: m_isFillEnabled = it->m_Argument.BoolArg; break; case CMD_SET_STROKE: m_isStrokeEnabled = it->m_Argument.BoolArg; break; case CMD_SET_FILLCOLOR: m_fillColor = COLOR4D( it->m_Argument.DblArg[0], it->m_Argument.DblArg[1], it->m_Argument.DblArg[2], it->m_Argument.DblArg[3] ); break; case CMD_SET_STROKECOLOR: m_strokeColor = COLOR4D( it->m_Argument.DblArg[0], it->m_Argument.DblArg[1], it->m_Argument.DblArg[2], it->m_Argument.DblArg[3] ); break; case CMD_SET_LINE_WIDTH: { // Make lines appear at least 1 pixel wide, no matter of zoom double x = 1.0, y = 1.0; cairo_device_to_user_distance( m_currentContext, &x, &y ); double minWidth = std::min( fabs( x ), fabs( y ) ); cairo_set_line_width( m_currentContext, std::max( it->m_Argument.DblArg[0], minWidth ) ); break; } case CMD_STROKE_PATH: cairo_set_source_rgba( m_currentContext, m_strokeColor.r, m_strokeColor.g, m_strokeColor.b, m_strokeColor.a ); cairo_append_path( m_currentContext, it->m_CairoPath ); cairo_stroke( m_currentContext ); break; case CMD_FILL_PATH: cairo_set_source_rgba( m_currentContext, m_fillColor.r, m_fillColor.g, m_fillColor.b, m_strokeColor.a ); cairo_append_path( m_currentContext, it->m_CairoPath ); cairo_fill( m_currentContext ); break; /* case CMD_TRANSFORM: cairo_matrix_t matrix; cairo_matrix_init( &matrix, it->argument.DblArg[0], it->argument.DblArg[1], it->argument.DblArg[2], it->argument.DblArg[3], it->argument.DblArg[4], it->argument.DblArg[5] ); cairo_transform( m_currentContext, &matrix ); break; */ case CMD_ROTATE: cairo_rotate( m_currentContext, it->m_Argument.DblArg[0] ); break; case CMD_TRANSLATE: cairo_translate( m_currentContext, it->m_Argument.DblArg[0], it->m_Argument.DblArg[1] ); break; case CMD_SCALE: cairo_scale( m_currentContext, it->m_Argument.DblArg[0], it->m_Argument.DblArg[1] ); break; case CMD_SAVE: cairo_save( m_currentContext ); break; case CMD_RESTORE: cairo_restore( m_currentContext ); break; case CMD_CALL_GROUP: DrawGroup( it->m_Argument.IntArg ); break; } } } void CAIRO_GAL_BASE::ChangeGroupColor( int aGroupNumber, const COLOR4D& aNewColor ) { storePath(); for( auto it = m_groups[aGroupNumber].begin(); it != m_groups[aGroupNumber].end(); ++it ) { if( it->m_Command == CMD_SET_FILLCOLOR || it->m_Command == CMD_SET_STROKECOLOR ) { it->m_Argument.DblArg[0] = aNewColor.r; it->m_Argument.DblArg[1] = aNewColor.g; it->m_Argument.DblArg[2] = aNewColor.b; it->m_Argument.DblArg[3] = aNewColor.a; } } } void CAIRO_GAL_BASE::ChangeGroupDepth( int aGroupNumber, int aDepth ) { // Cairo does not have any possibilities to change the depth coordinate of stored items, // it depends only on the order of drawing } void CAIRO_GAL_BASE::DeleteGroup( int aGroupNumber ) { storePath(); // Delete the Cairo paths std::deque::iterator it, end; for( it = m_groups[aGroupNumber].begin(), end = m_groups[aGroupNumber].end(); it != end; ++it ) { if( it->m_Command == CMD_FILL_PATH || it->m_Command == CMD_STROKE_PATH ) cairo_path_destroy( it->m_CairoPath ); } // Delete the group m_groups.erase( aGroupNumber ); } void CAIRO_GAL_BASE::ClearCache() { for( auto it = m_groups.begin(); it != m_groups.end(); ) DeleteGroup( ( it++ )->first ); } void CAIRO_GAL_BASE::SetNegativeDrawMode( bool aSetting ) { cairo_set_operator( m_currentContext, aSetting ? CAIRO_OPERATOR_CLEAR : CAIRO_OPERATOR_OVER ); } void CAIRO_GAL::StartDiffLayer() { SetTarget( TARGET_TEMP ); ClearTarget( TARGET_TEMP ); } void CAIRO_GAL::EndDiffLayer() { m_compositor->DrawBuffer( m_tempBuffer, m_mainBuffer, CAIRO_OPERATOR_ADD ); } void CAIRO_GAL::StartNegativesLayer() { SetTarget( TARGET_TEMP ); ClearTarget( TARGET_TEMP ); } void CAIRO_GAL::EndNegativesLayer() { m_compositor->DrawBuffer( m_tempBuffer, m_mainBuffer, CAIRO_OPERATOR_OVER ); } void CAIRO_GAL_BASE::DrawCursor( const VECTOR2D& aCursorPosition ) { m_cursorPosition = aCursorPosition; } void CAIRO_GAL_BASE::EnableDepthTest( bool aEnabled ) { } void CAIRO_GAL_BASE::resetContext() { for( _cairo_surface* imageSurface : m_imageSurfaces ) cairo_surface_destroy( imageSurface ); m_imageSurfaces.clear(); ClearScreen(); // Compute the world <-> screen transformations ComputeWorldScreenMatrix(); cairo_matrix_init( &m_cairoWorldScreenMatrix, m_worldScreenMatrix.m_data[0][0], m_worldScreenMatrix.m_data[1][0], m_worldScreenMatrix.m_data[0][1], m_worldScreenMatrix.m_data[1][1], m_worldScreenMatrix.m_data[0][2], m_worldScreenMatrix.m_data[1][2] ); // we work in screen-space coordinates and do the transforms outside. cairo_identity_matrix( m_context ); cairo_matrix_init_identity( &m_currentXform ); // Start drawing with a new path cairo_new_path( m_context ); m_isElementAdded = true; updateWorldScreenMatrix(); m_lineWidth = 0; } void CAIRO_GAL_BASE::drawAxes( const VECTOR2D& aStartPoint, const VECTOR2D& aEndPoint ) { syncLineWidth(); VECTOR2D p0 = roundp( xform( aStartPoint ) ); VECTOR2D p1 = roundp( xform( aEndPoint ) ); VECTOR2D org = roundp( xform( VECTOR2D( 0.0, 0.0 ) ) ); // Axis origin = 0,0 coord cairo_set_source_rgba( m_currentContext, m_axesColor.r, m_axesColor.g, m_axesColor.b, m_axesColor.a ); cairo_move_to( m_currentContext, p0.x, org.y ); cairo_line_to( m_currentContext, p1.x, org.y ); cairo_move_to( m_currentContext, org.x, p0.y ); cairo_line_to( m_currentContext, org.x, p1.y ); cairo_stroke( m_currentContext ); } void CAIRO_GAL_BASE::drawGridLine( const VECTOR2D& aStartPoint, const VECTOR2D& aEndPoint ) { syncLineWidth(); VECTOR2D p0 = roundp( xform( aStartPoint ) ); VECTOR2D p1 = roundp( xform( aEndPoint ) ); cairo_set_source_rgba( m_currentContext, m_gridColor.r, m_gridColor.g, m_gridColor.b, m_gridColor.a ); cairo_move_to( m_currentContext, p0.x, p0.y ); cairo_line_to( m_currentContext, p1.x, p1.y ); cairo_stroke( m_currentContext ); } void CAIRO_GAL_BASE::drawGridCross( const VECTOR2D& aPoint ) { syncLineWidth(); VECTOR2D offset( 0, 0 ); double size = 2.0 * m_lineWidthInPixels + 0.5; VECTOR2D p0 = roundp( xform( aPoint ) ) - VECTOR2D( size, 0 ) + offset; VECTOR2D p1 = roundp( xform( aPoint ) ) + VECTOR2D( size, 0 ) + offset; VECTOR2D p2 = roundp( xform( aPoint ) ) - VECTOR2D( 0, size ) + offset; VECTOR2D p3 = roundp( xform( aPoint ) ) + VECTOR2D( 0, size ) + offset; cairo_set_source_rgba( m_currentContext, m_gridColor.r, m_gridColor.g, m_gridColor.b, m_gridColor.a ); cairo_move_to( m_currentContext, p0.x, p0.y ); cairo_line_to( m_currentContext, p1.x, p1.y ); cairo_move_to( m_currentContext, p2.x, p2.y ); cairo_line_to( m_currentContext, p3.x, p3.y ); cairo_stroke( m_currentContext ); } void CAIRO_GAL_BASE::drawGridPoint( const VECTOR2D& aPoint, double aWidth, double aHeight ) { VECTOR2D p = roundp( xform( aPoint ) ); double sw = std::max( 1.0, aWidth ); double sh = std::max( 1.0, aHeight ); cairo_set_source_rgba( m_currentContext, m_gridColor.r, m_gridColor.g, m_gridColor.b, m_gridColor.a ); cairo_rectangle( m_currentContext, p.x - std::floor( sw / 2 ) - 0.5, p.y - std::floor( sh / 2 ) - 0.5, sw, sh ); cairo_fill( m_currentContext ); } void CAIRO_GAL_BASE::flushPath() { if( m_isFillEnabled ) { cairo_set_source_rgba( m_currentContext, m_fillColor.r, m_fillColor.g, m_fillColor.b, m_fillColor.a ); if( m_isStrokeEnabled ) cairo_fill_preserve( m_currentContext ); else cairo_fill( m_currentContext ); } if( m_isStrokeEnabled ) { cairo_set_source_rgba( m_currentContext, m_strokeColor.r, m_strokeColor.g, m_strokeColor.b, m_strokeColor.a ); cairo_stroke( m_currentContext ); } } void CAIRO_GAL_BASE::storePath() { if( m_isElementAdded ) { m_isElementAdded = false; if( !m_isGrouping ) { if( m_isFillEnabled ) { cairo_set_source_rgba( m_currentContext, m_fillColor.r, m_fillColor.g, m_fillColor.b, m_fillColor.a ); cairo_fill_preserve( m_currentContext ); } if( m_isStrokeEnabled ) { cairo_set_source_rgba( m_currentContext, m_strokeColor.r, m_strokeColor.g, m_strokeColor.b, m_strokeColor.a ); cairo_stroke_preserve( m_currentContext ); } } else { // Copy the actual path, append it to the global path list // then check, if the path needs to be stroked/filled and // add this command to the group list; if( m_isStrokeEnabled ) { GROUP_ELEMENT groupElement; groupElement.m_CairoPath = cairo_copy_path( m_currentContext ); groupElement.m_Command = CMD_STROKE_PATH; m_currentGroup->push_back( groupElement ); } if( m_isFillEnabled ) { GROUP_ELEMENT groupElement; groupElement.m_CairoPath = cairo_copy_path( m_currentContext ); groupElement.m_Command = CMD_FILL_PATH; m_currentGroup->push_back( groupElement ); } } cairo_new_path( m_currentContext ); } } void CAIRO_GAL_BASE::blitCursor( wxMemoryDC& clientDC ) { if( !IsCursorEnabled() ) return; VECTOR2D p = ToScreen( m_cursorPosition ); const COLOR4D cColor = getCursorColor(); const int cursorSize = m_fullscreenCursor ? 8000 : 80; wxColour color( cColor.r * cColor.a * 255, cColor.g * cColor.a * 255, cColor.b * cColor.a * 255, 255 ); clientDC.SetPen( wxPen( color ) ); clientDC.DrawLine( p.x - cursorSize / 2, p.y, p.x + cursorSize / 2, p.y ); clientDC.DrawLine( p.x, p.y - cursorSize / 2, p.x, p.y + cursorSize / 2 ); } void CAIRO_GAL_BASE::drawPoly( const std::deque& aPointList ) { wxCHECK( aPointList.size() > 1, /* void */ ); // Iterate over the point list and draw the segments std::deque::const_iterator it = aPointList.begin(); syncLineWidth(); const VECTOR2D p = roundp( xform( it->x, it->y ) ); cairo_move_to( m_currentContext, p.x, p.y ); for( ++it; it != aPointList.end(); ++it ) { const VECTOR2D p2 = roundp( xform( it->x, it->y ) ); cairo_line_to( m_currentContext, p2.x, p2.y ); } flushPath(); m_isElementAdded = true; } void CAIRO_GAL_BASE::drawPoly( const VECTOR2D aPointList[], int aListSize ) { wxCHECK( aListSize > 1, /* void */ ); // Iterate over the point list and draw the segments const VECTOR2D* ptr = aPointList; syncLineWidth(); const VECTOR2D p = roundp( xform( ptr->x, ptr->y ) ); cairo_move_to( m_currentContext, p.x, p.y ); for( int i = 1; i < aListSize; ++i ) { ++ptr; const VECTOR2D p2 = roundp( xform( ptr->x, ptr->y ) ); cairo_line_to( m_currentContext, p2.x, p2.y ); } flushPath(); m_isElementAdded = true; } void CAIRO_GAL_BASE::drawPoly( const SHAPE_LINE_CHAIN& aLineChain ) { wxCHECK( aLineChain.PointCount() > 1, /* void */ ); syncLineWidth(); auto numPoints = aLineChain.PointCount(); if( aLineChain.IsClosed() ) numPoints += 1; const VECTOR2I start = aLineChain.CPoint( 0 ); const VECTOR2D p = roundp( xform( start.x, start.y ) ); cairo_move_to( m_currentContext, p.x, p.y ); for( int i = 1; i < numPoints; ++i ) { const VECTOR2I& pw = aLineChain.CPoint( i ); const VECTOR2D ps = roundp( xform( pw.x, pw.y ) ); cairo_line_to( m_currentContext, ps.x, ps.y ); } flushPath(); m_isElementAdded = true; } unsigned int CAIRO_GAL_BASE::getNewGroupNumber() { wxASSERT_MSG( m_groups.size() < std::numeric_limits::max(), wxT( "There are no free slots to store a group" ) ); while( m_groups.find( m_groupCounter ) != m_groups.end() ) m_groupCounter++; return m_groupCounter++; } CAIRO_GAL::CAIRO_GAL( GAL_DISPLAY_OPTIONS& aDisplayOptions, wxWindow* aParent, wxEvtHandler* aMouseListener, wxEvtHandler* aPaintListener, const wxString& aName ) : CAIRO_GAL_BASE( aDisplayOptions ), wxWindow( aParent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxEXPAND, aName ) { // Initialise compositing state m_mainBuffer = 0; m_overlayBuffer = 0; m_tempBuffer = 0; m_savedBuffer = 0; m_validCompositor = false; SetTarget( TARGET_NONCACHED ); m_bitmapBuffer = nullptr; m_wxOutput = nullptr; m_parentWindow = aParent; m_mouseListener = aMouseListener; m_paintListener = aPaintListener; // Connect the native cursor handler Connect( wxEVT_SET_CURSOR, wxSetCursorEventHandler( CAIRO_GAL::onSetNativeCursor ), nullptr, this ); // Connecting the event handlers Connect( wxEVT_PAINT, wxPaintEventHandler( CAIRO_GAL::onPaint ) ); // Mouse events are skipped to the parent Connect( wxEVT_MOTION, wxMouseEventHandler( CAIRO_GAL::skipMouseEvent ) ); Connect( wxEVT_LEFT_DOWN, wxMouseEventHandler( CAIRO_GAL::skipMouseEvent ) ); Connect( wxEVT_LEFT_UP, wxMouseEventHandler( CAIRO_GAL::skipMouseEvent ) ); Connect( wxEVT_LEFT_DCLICK, wxMouseEventHandler( CAIRO_GAL::skipMouseEvent ) ); Connect( wxEVT_MIDDLE_DOWN, wxMouseEventHandler( CAIRO_GAL::skipMouseEvent ) ); Connect( wxEVT_MIDDLE_UP, wxMouseEventHandler( CAIRO_GAL::skipMouseEvent ) ); Connect( wxEVT_MIDDLE_DCLICK, wxMouseEventHandler( CAIRO_GAL::skipMouseEvent ) ); Connect( wxEVT_RIGHT_DOWN, wxMouseEventHandler( CAIRO_GAL::skipMouseEvent ) ); Connect( wxEVT_RIGHT_UP, wxMouseEventHandler( CAIRO_GAL::skipMouseEvent ) ); Connect( wxEVT_RIGHT_DCLICK, wxMouseEventHandler( CAIRO_GAL::skipMouseEvent ) ); Connect( wxEVT_MOUSEWHEEL, wxMouseEventHandler( CAIRO_GAL::skipMouseEvent ) ); #if defined _WIN32 || defined _WIN64 Connect( wxEVT_ENTER_WINDOW, wxMouseEventHandler( CAIRO_GAL::skipMouseEvent ) ); #endif SetSize( aParent->GetClientSize() ); m_screenSize = VECTOR2I( aParent->GetClientSize() ); // Allocate memory for pixel storage allocateBitmaps(); m_isInitialized = false; } CAIRO_GAL::~CAIRO_GAL() { deleteBitmaps(); } void CAIRO_GAL::beginDrawing() { initSurface(); CAIRO_GAL_BASE::beginDrawing(); if( !m_validCompositor ) setCompositor(); m_compositor->SetMainContext( m_context ); m_compositor->SetBuffer( m_mainBuffer ); } void CAIRO_GAL::endDrawing() { CAIRO_GAL_BASE::endDrawing(); // Merge buffers on the screen m_compositor->DrawBuffer( m_mainBuffer ); m_compositor->DrawBuffer( m_overlayBuffer ); // Now translate the raw context data from the format stored // by cairo into a format understood by wxImage. pixman_image_t* dstImg = pixman_image_create_bits( wxPlatformInfo::Get().GetEndianness() == wxENDIAN_LITTLE ? PIXMAN_b8g8r8 : PIXMAN_r8g8b8, m_screenSize.x, m_screenSize.y, (uint32_t*) m_wxOutput, m_wxBufferWidth * 3 ); pixman_image_t* srcImg = pixman_image_create_bits( PIXMAN_a8r8g8b8, m_screenSize.x, m_screenSize.y, (uint32_t*) m_bitmapBuffer, m_wxBufferWidth * 4 ); pixman_image_composite( PIXMAN_OP_SRC, srcImg, nullptr, dstImg, 0, 0, 0, 0, 0, 0, m_screenSize.x, m_screenSize.y ); // Free allocated memory pixman_image_unref( srcImg ); pixman_image_unref( dstImg ); wxImage img( m_wxBufferWidth, m_screenSize.y, m_wxOutput, true ); wxBitmap bmp( img ); wxMemoryDC mdc( bmp ); wxClientDC clientDC( this ); // Now it is the time to blit the mouse cursor blitCursor( mdc ); clientDC.Blit( 0, 0, m_screenSize.x, m_screenSize.y, &mdc, 0, 0, wxCOPY ); deinitSurface(); } void CAIRO_GAL::PostPaint( wxPaintEvent& aEvent ) { // posts an event to m_paint_listener to ask for redraw the canvas. if( m_paintListener ) wxPostEvent( m_paintListener, aEvent ); } void CAIRO_GAL::ResizeScreen( int aWidth, int aHeight ) { CAIRO_GAL_BASE::ResizeScreen( aWidth, aHeight ); // Recreate the bitmaps deleteBitmaps(); allocateBitmaps(); if( m_validCompositor ) m_compositor->Resize( aWidth, aHeight ); m_validCompositor = false; SetSize( wxSize( aWidth, aHeight ) ); } bool CAIRO_GAL::Show( bool aShow ) { bool s = wxWindow::Show( aShow ); if( aShow ) wxWindow::Raise(); return s; } int CAIRO_GAL::BeginGroup() { initSurface(); return CAIRO_GAL_BASE::BeginGroup(); } void CAIRO_GAL::EndGroup() { CAIRO_GAL_BASE::EndGroup(); deinitSurface(); } void CAIRO_GAL::SetTarget( RENDER_TARGET aTarget ) { // If the compositor is not set, that means that there is a recaching process going on // and we do not need the compositor now if( !m_validCompositor ) return; // Cairo grouping prevents display of overlapping items on the same layer in the lighter color if( m_isInitialized ) storePath(); switch( aTarget ) { default: case TARGET_CACHED: case TARGET_NONCACHED: m_compositor->SetBuffer( m_mainBuffer ); break; case TARGET_OVERLAY: m_compositor->SetBuffer( m_overlayBuffer ); break; case TARGET_TEMP: m_compositor->SetBuffer( m_tempBuffer ); break; } m_currentTarget = aTarget; } RENDER_TARGET CAIRO_GAL::GetTarget() const { return m_currentTarget; } void CAIRO_GAL::ClearTarget( RENDER_TARGET aTarget ) { // Save the current state unsigned int currentBuffer = m_compositor->GetBuffer(); switch( aTarget ) { // Cached and noncached items are rendered to the same buffer default: case TARGET_CACHED: case TARGET_NONCACHED: m_compositor->SetBuffer( m_mainBuffer ); break; case TARGET_OVERLAY: m_compositor->SetBuffer( m_overlayBuffer ); break; case TARGET_TEMP: m_compositor->SetBuffer( m_tempBuffer ); break; } m_compositor->ClearBuffer( COLOR4D::BLACK ); // Restore the previous state m_compositor->SetBuffer( currentBuffer ); } void CAIRO_GAL::initSurface() { if( m_isInitialized ) return; m_surface = cairo_image_surface_create_for_data( m_bitmapBuffer, GAL_FORMAT, m_wxBufferWidth, m_screenSize.y, m_stride ); m_context = cairo_create( m_surface ); #ifdef DEBUG cairo_status_t status = cairo_status( m_context ); wxASSERT_MSG( status == CAIRO_STATUS_SUCCESS, wxT( "Cairo context creation error" ) ); #endif /* DEBUG */ m_currentContext = m_context; m_isInitialized = true; } void CAIRO_GAL::deinitSurface() { if( !m_isInitialized ) return; cairo_destroy( m_context ); m_context = nullptr; cairo_surface_destroy( m_surface ); m_surface = nullptr; m_isInitialized = false; } void CAIRO_GAL::allocateBitmaps() { m_wxBufferWidth = m_screenSize.x; while( ( ( m_wxBufferWidth * 3 ) % 4 ) != 0 ) m_wxBufferWidth++; // Create buffer, use the system independent Cairo context backend m_stride = cairo_format_stride_for_width( GAL_FORMAT, m_wxBufferWidth ); m_bufferSize = m_stride * m_screenSize.y; wxASSERT( m_bitmapBuffer == nullptr ); m_bitmapBuffer = new unsigned char[m_bufferSize * 4]; wxASSERT( m_wxOutput == nullptr ); m_wxOutput = new unsigned char[m_wxBufferWidth * 3 * m_screenSize.y]; } void CAIRO_GAL::deleteBitmaps() { delete[] m_bitmapBuffer; m_bitmapBuffer = nullptr; delete[] m_wxOutput; m_wxOutput = nullptr; } void CAIRO_GAL::setCompositor() { // Recreate the compositor with the new Cairo context m_compositor.reset( new CAIRO_COMPOSITOR( &m_currentContext ) ); m_compositor->Resize( m_screenSize.x, m_screenSize.y ); m_compositor->SetAntialiasingMode( m_options.cairo_antialiasing_mode ); // Prepare buffers m_mainBuffer = m_compositor->CreateBuffer(); m_overlayBuffer = m_compositor->CreateBuffer(); m_tempBuffer = m_compositor->CreateBuffer(); m_validCompositor = true; } void CAIRO_GAL::onPaint( wxPaintEvent& aEvent ) { PostPaint( aEvent ); } void CAIRO_GAL::skipMouseEvent( wxMouseEvent& aEvent ) { // Post the mouse event to the event listener registered in constructor, if any if( m_mouseListener ) wxPostEvent( m_mouseListener, aEvent ); } bool CAIRO_GAL::updatedGalDisplayOptions( const GAL_DISPLAY_OPTIONS& aOptions ) { bool refresh = false; if( m_validCompositor && aOptions.cairo_antialiasing_mode != m_compositor->GetAntialiasingMode() ) { m_compositor->SetAntialiasingMode( m_options.cairo_antialiasing_mode ); m_validCompositor = false; deinitSurface(); refresh = true; } if( super::updatedGalDisplayOptions( aOptions ) ) { Refresh(); refresh = true; } return refresh; } bool CAIRO_GAL::SetNativeCursorStyle( KICURSOR aCursor ) { // Store the current cursor type and get the wxCursor for it if( !GAL::SetNativeCursorStyle( aCursor ) ) return false; m_currentwxCursor = CURSOR_STORE::GetCursor( m_currentNativeCursor ); // Update the cursor in the wx control wxWindow::SetCursor( m_currentwxCursor ); return true; } void CAIRO_GAL::onSetNativeCursor( wxSetCursorEvent& aEvent ) { aEvent.SetCursor( m_currentwxCursor ); } void CAIRO_GAL_BASE::DrawGrid() { SetTarget( TARGET_NONCACHED ); // Draw the grid // For the drawing the start points, end points and increments have // to be calculated in world coordinates VECTOR2D worldStartPoint = m_screenWorldMatrix * VECTOR2D( 0.0, 0.0 ); VECTOR2D worldEndPoint = m_screenWorldMatrix * VECTOR2D( m_screenSize ); // Compute the line marker or point radius of the grid // Note: generic grids can't handle sub-pixel lines without // either losing fine/course distinction or having some dots // fail to render float marker = std::fmax( 1.0f, m_gridLineWidth ) / m_worldScale; float doubleMarker = 2.0f * marker; // Draw axes if desired if( m_axesEnabled ) { SetLineWidth( marker ); drawAxes( worldStartPoint, worldEndPoint ); } if( !m_gridVisibility || m_gridSize.x == 0 || m_gridSize.y == 0 ) return; VECTOR2D gridScreenSize( m_gridSize ); double gridThreshold = KiROUND( computeMinGridSpacing() / m_worldScale ); if( m_gridStyle == GRID_STYLE::SMALL_CROSS ) gridThreshold *= 2.0; // If we cannot display the grid density, scale down by a tick size and // try again. Eventually, we get some representation of the grid while( std::min( gridScreenSize.x, gridScreenSize.y ) <= gridThreshold ) { gridScreenSize = gridScreenSize * static_cast( m_gridTick ); } // Compute grid starting and ending indexes to draw grid points on the // visible screen area // Note: later any point coordinate will be offsetted by m_gridOrigin int gridStartX = KiROUND( ( worldStartPoint.x - m_gridOrigin.x ) / gridScreenSize.x ); int gridEndX = KiROUND( ( worldEndPoint.x - m_gridOrigin.x ) / gridScreenSize.x ); int gridStartY = KiROUND( ( worldStartPoint.y - m_gridOrigin.y ) / gridScreenSize.y ); int gridEndY = KiROUND( ( worldEndPoint.y - m_gridOrigin.y ) / gridScreenSize.y ); // Ensure start coordinate > end coordinate SWAP( gridStartX, >, gridEndX ); SWAP( gridStartY, >, gridEndY ); // Ensure the grid fills the screen --gridStartX; ++gridEndX; --gridStartY; ++gridEndY; // Draw the grid behind all other layers SetLayerDepth( m_depthRange.y * 0.75 ); if( m_gridStyle == GRID_STYLE::LINES ) { // Now draw the grid, every coarse grid line gets the double width // Vertical lines for( int j = gridStartY; j <= gridEndY; j++ ) { const double y = j * gridScreenSize.y + m_gridOrigin.y; if( m_axesEnabled && y == 0.0 ) continue; SetLineWidth( ( j % m_gridTick ) ? marker : doubleMarker ); drawGridLine( VECTOR2D( gridStartX * gridScreenSize.x + m_gridOrigin.x, y ), VECTOR2D( gridEndX * gridScreenSize.x + m_gridOrigin.x, y ) ); } // Horizontal lines for( int i = gridStartX; i <= gridEndX; i++ ) { const double x = i * gridScreenSize.x + m_gridOrigin.x; if( m_axesEnabled && x == 0.0 ) continue; SetLineWidth( ( i % m_gridTick ) ? marker : doubleMarker ); drawGridLine( VECTOR2D( x, gridStartY * gridScreenSize.y + m_gridOrigin.y ), VECTOR2D( x, gridEndY * gridScreenSize.y + m_gridOrigin.y ) ); } } else // Dots or Crosses grid { m_lineWidthIsOdd = true; m_isStrokeEnabled = true; for( int j = gridStartY; j <= gridEndY; j++ ) { bool tickY = ( j % m_gridTick == 0 ); for( int i = gridStartX; i <= gridEndX; i++ ) { bool tickX = ( i % m_gridTick == 0 ); VECTOR2D pos{ i * gridScreenSize.x + m_gridOrigin.x, j * gridScreenSize.y + m_gridOrigin.y }; if( m_gridStyle == GRID_STYLE::SMALL_CROSS ) { SetLineWidth( ( tickX && tickY ) ? doubleMarker : marker ); drawGridCross( pos ); } else if( m_gridStyle == GRID_STYLE::DOTS ) { double doubleGridLineWidth = m_gridLineWidth * 2.0f; drawGridPoint( pos, ( tickX ) ? doubleGridLineWidth : m_gridLineWidth, ( tickY ) ? doubleGridLineWidth : m_gridLineWidth ); } } } } }