From 7ca8f72b7cf8058be3a463f5b00621c33783b1ac Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Thu, 6 May 2021 06:08:32 -0400 Subject: [PATCH] fix dashed lines with square line caps (#9561) * fix "line-dasharray" with "line-cap": "square" fix #9531 * remove leftover Co-authored-by: Vladimir Agafonkin --- src/data/bucket/line_bucket.js | 42 +++++++++++------- src/render/draw_line.js | 5 +-- src/render/line_atlas.js | 22 ++++----- test/ignores.json | 1 - .../line-dasharray/case/square/expected.png | Bin 9458 -> 3581 bytes 5 files changed, 40 insertions(+), 30 deletions(-) diff --git a/src/data/bucket/line_bucket.js b/src/data/bucket/line_bucket.js index 0c620c747dc..13ce96de051 100644 --- a/src/data/bucket/line_bucket.js +++ b/src/data/bucket/line_bucket.js @@ -206,12 +206,12 @@ class LineBucket implements Bucket { hasFeatureDashes = true; } else { - const round = capPropertyValue.value === 'round'; + const constCap = capPropertyValue.value; const constDash = dashPropertyValue.value; if (!constDash) continue; - lineAtlas.addDash(constDash.from, round); - lineAtlas.addDash(constDash.to, round); - if (constDash.other) lineAtlas.addDash(constDash.other, round); + lineAtlas.addDash(constDash.from, constCap); + lineAtlas.addDash(constDash.to, constCap); + if (constDash.other) lineAtlas.addDash(constDash.other, constCap); } } @@ -228,7 +228,7 @@ class LineBucket implements Bucket { if (dashPropertyValue.kind === 'constant' && capPropertyValue.kind === 'constant') continue; - let minDashArray, midDashArray, maxDashArray, minRound, midRound, maxRound; + let minDashArray, midDashArray, maxDashArray, minCap, midCap, maxCap; if (dashPropertyValue.kind === 'constant') { const constDash = dashPropertyValue.value; @@ -244,21 +244,21 @@ class LineBucket implements Bucket { } if (capPropertyValue.kind === 'constant') { - minRound = midRound = maxRound = capPropertyValue.value === 'round'; + minCap = midCap = maxCap = capPropertyValue.value; } else { - minRound = capPropertyValue.evaluate({zoom: zoom - 1}, feature) === 'round'; - midRound = capPropertyValue.evaluate({zoom}, feature) === 'round'; - maxRound = capPropertyValue.evaluate({zoom: zoom + 1}, feature) === 'round'; + minCap = capPropertyValue.evaluate({zoom: zoom - 1}, feature); + midCap = capPropertyValue.evaluate({zoom}, feature); + maxCap = capPropertyValue.evaluate({zoom: zoom + 1}, feature); } - lineAtlas.addDash(minDashArray, minRound); - lineAtlas.addDash(midDashArray, midRound); - lineAtlas.addDash(maxDashArray, maxRound); + lineAtlas.addDash(minDashArray, minCap); + lineAtlas.addDash(midDashArray, midCap); + lineAtlas.addDash(maxDashArray, maxCap); - const min = lineAtlas.getKey(minDashArray, minRound); - const mid = lineAtlas.getKey(midDashArray, midRound); - const max = lineAtlas.getKey(maxDashArray, maxRound); + const min = lineAtlas.getKey(minDashArray, minCap); + const mid = lineAtlas.getKey(midDashArray, midCap); + const max = lineAtlas.getKey(maxDashArray, maxCap); // save positions for paint array feature.patterns[layer.id] = {min, mid, max}; @@ -540,7 +540,17 @@ class LineBucket implements Bucket { } else if (currentJoin === 'square') { const offset = prevVertex ? 1 : -1; // closing or starting square cap - this.addCurrentVertex(currentVertex, joinNormal, offset, offset, segment); + + if (!prevVertex) { + this.addCurrentVertex(currentVertex, joinNormal, offset, offset, segment); + } + + // make the cap it's own quad to avoid the cap affecting the line distance + this.addCurrentVertex(currentVertex, joinNormal, 0, 0, segment); + + if (prevVertex) { + this.addCurrentVertex(currentVertex, joinNormal, offset, offset, segment); + } } else if (currentJoin === 'round') { diff --git a/src/render/draw_line.js b/src/render/draw_line.js index a8a95a52256..aeed831936c 100644 --- a/src/render/draw_line.js +++ b/src/render/draw_line.js @@ -70,9 +70,8 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay if (!image && constantDash && constantCap && tile.lineAtlas) { const atlas = tile.lineAtlas; - const round = constantCap === 'round'; - const posTo = atlas.getDash(constantDash.to, round); - const posFrom = atlas.getDash(constantDash.from, round); + const posTo = atlas.getDash(constantDash.to, constantCap); + const posFrom = atlas.getDash(constantDash.from, constantCap); if (posTo && posFrom) programConfiguration.setConstantPatternPositions(posTo, posFrom); } diff --git a/src/render/line_atlas.js b/src/render/line_atlas.js index f8d0fb620e1..46a5672a236 100644 --- a/src/render/line_atlas.js +++ b/src/render/line_atlas.js @@ -34,12 +34,12 @@ class LineAtlas { * Get a dash line pattern. * * @param {Array} dasharray - * @param {boolean} round whether to add circle caps in between dash segments + * @param {string} lineCap the type of line caps to be added to dashes * @returns {Object} position of dash texture in { y, height, width } * @private */ - getDash(dasharray: Array, round: boolean) { - const key = this.getKey(dasharray, round); + getDash(dasharray: Array, lineCap: string) { + const key = this.getKey(dasharray, lineCap); return this.positions[key]; } @@ -49,8 +49,8 @@ class LineAtlas { this.image.resize({width, height}); } - getKey(dasharray: Array, round: boolean): string { - return dasharray.join(',') + String(round); + getKey(dasharray: Array, lineCap: string): string { + return dasharray.join(',') + lineCap; } getDashRanges(dasharray: Array, lineAtlasWidth: number, stretch: number) { @@ -111,7 +111,7 @@ class LineAtlas { } } - addRegularDash(ranges: Object) { + addRegularDash(ranges: Object, capLength: number) { // Collapse any zero-length range // Collapse neighbouring same-type parts into a single part @@ -147,14 +147,15 @@ class LineAtlas { const distRight = Math.abs(x - range.right); const minDist = Math.min(distLeft, distRight); - const signedDistance = range.isDash ? minDist : -minDist; + const signedDistance = (range.isDash ? minDist : -minDist) + capLength; this.image.data[index + x] = Math.max(0, Math.min(255, signedDistance + 128)); } } - addDash(dasharray: Array, round: boolean) { - const key = this.getKey(dasharray, round); + addDash(dasharray: Array, lineCap: string) { + const key = this.getKey(dasharray, lineCap); + const round = lineCap === 'round'; const n = round ? 7 : 0; const height = 2 * n + 1; @@ -185,7 +186,8 @@ class LineAtlas { if (round) { this.addRoundDash(ranges, stretch, n); } else { - this.addRegularDash(ranges); + const capLength = lineCap === 'square' ? 0.5 * stretch : 0; + this.addRegularDash(ranges, capLength); } } diff --git a/test/ignores.json b/test/ignores.json index 53191757fc0..c3a536134dc 100644 --- a/test/ignores.json +++ b/test/ignores.json @@ -10,7 +10,6 @@ "render-tests/geojson/inline-linestring-fill": "skip - current behavior is arbitrary", "render-tests/icon-image/icon-sdf-non-sdf-one-layer": "skip - render sdf icon and normal icon in one layer", "render-tests/icon-text-fit/text-variable-anchor-tile-map-mode": "skip - mapbox-gl-js does not support tile-mode", - "render-tests/line-dasharray/case/square": "skip - https://github.com/mapbox/mapbox-gl-js/issues/9531", "render-tests/map-mode/static": "https://github.com/mapbox/mapbox-gl-js/issues/5649", "render-tests/map-mode/tile": "skip - mapbox-gl-js does not support tile-mode", "render-tests/map-mode/tile-avoid-edges": "skip - mapbox-gl-js does not support tile-mode", diff --git a/test/integration/render-tests/line-dasharray/case/square/expected.png b/test/integration/render-tests/line-dasharray/case/square/expected.png index 950a308bc81edef8c4a2812c8703f3782cd372d7..40aa45378f90b8b029c2d9678d76260b24b63c13 100644 GIT binary patch literal 3581 zcmd5<`#+O?AFrO0S)nN^re&Ujw2Fl;yEDj$3efi*Phntx`;oCZSb(nQDT<)6@YS1f} z^N8{_N4B076uAe=F|-0$Pc z;vkvl*APnk?PP|pqp!oR%F84ZcEAixVYiAv)WDMH=PHZ{*H1Y%pu zkoU%Lf&rvT096eV)v4oWHTPf*42t!Wccr_X#L3z|j!!xN=j(^>vZ|`9s`RRSt9VY1 z2qxkJ%Fp$T{`P!_-BN2qFvmMi7qj-rWNdM(@Oa>XueVbn{pfF_SC{V2@9m-U1IWEb3Go{;4mKT?3k!OJBS*bd{?STb&H9THfG{S27V=^mb*Zc#>gjP zt8$tszn<^<4vNx9#ow#qRrzjBH7x%*R9I#DCa@op`Y~zc!_7qoE(rS%PzDH4!HS7L-=nU95gY)51P9@6ZuifA`dXi2GH7ALbcFz@tTB`ufH~RB5apDWt=5x7 z6|Td%7)dhOwQ)ns4dX74R?qxAGVdgjsNmYn-h$$qQ}djlsm6$Ih6WH>%&Anr!9nA5 z-oULmO1ce`4Z$vYcve467nu$Wrd}NXg)=5!kR(*Xxxk?qyTKf=#$VHpb&%6J;X~(;%t~vf4OFHSVVJ|#%=eEVPC&TBsA<8_nGhG8oc@sUu+sfjCWKM*b}z_7f_5_w z+!k~U)i>{Whc2wra}fo&$E=ypmPk2{iua#c8{lsE@O}<-Dkj&EYPL{Y^7Bvs(kFpN zWbHOsWew+tI#@WP=jn!)1sW|-!Inb-4Uq)dVLkz!1f49&0Qvxe||d zE*ToPgkze&$6*gcOi|%W0`h7J-?uyG;=g>qrRk(@C%S6AATFRL02QJA+{!+_umt_?%axg6}`!3 z^C@=F*O~e-zar>}!u(QJy+c7kdBt_@RMXne+3uyxKbtVFvn{|_Bj~|1^uTx4)yS)= zMhDMjH7!(-48`jXlJrP2s|@EdvS9h@)ebJrE#7C!B|(XtFDm3%ujxx%cmf4uH#ek_ z$9A+)Pdr{oiA+`ByI1Y28wWv0;>3>7&W0y@7UmHJs~}(I`(&uX$m}`rnE2OcD~AZm zIy*aIa3dpse_5|*NYY&hE-?jPPrw)Yr#wA^pzA}!J-WB-g7;_#TwXpcPpCs?X2i&O z#Ue>s(BnS-S3XYd*<4iL6C< z#;-wg>5Gx8ri2r>NdwO=b+DtA)(Dft#kAUWn^K#zgliPnUg5x66yR5^BElel1B%I*YF66`pDn&a?xSTwsGTF))x-*UcxK`7e zsz9Pbp!R-?%{oQi->O3=?To4H%BRg7K2TIc+Ga`ybNv z6lKdKj}66K?$ff`|C1lA`!`;=gXB2G_1r`E>K zrxD*Xd$Y7NkvWgLS3bh++i_Ro>~GEiW#G2I3Lzz~jZXW+>p{Vrc{lSb|(k6adxXCOtwF zk-)u04ix24fb>o`Z#re0-xV%4pS$ZVkZZE!h$6e49`|N0feRF-tf(vfdOJP3tW3L# zf5fZqbe&h4IORulT)ttOD6S-Tw}yAVu=VXth@HD=VJJ9ORB2c;sGVT&B9e1y-Mq;M zK4)hxbRhS+)~qPQ(CA`LNM9D*o(61!U~49}V9EVen^dj#S5k7ap$&A>C?-&I9*(87 zRNYM~ElrBKEsc|jLi?Ne?IoP^4Giq{;~S#TQ=bI0J6-q)i4K(N!W7;`H60iPyfW5@ zi?ETxM=0Rbtdbv*9QdL$k(~5fH%li{n1lpQOB7&cHK}LR#wxHBVG;^B3(CF>Rc^A< zAiK+^+vfZB5&}8BZH8K;K-T8{q4u()mVfh?6bf!yCGi`9+>pOw2z;$uhO@M{$Td3^ F@jrE+xYqyx literal 9458 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 D&Q-)q