@@ -98,6 +98,28 @@ function rThetaToXY(r: number, theta: number, x: number, y: number) {
9898 } ;
9999}
100100
101+ function pathFullCircle (
102+ ctx : CanvasRenderingContext2D ,
103+ element : ArcElement ,
104+ offset : number ,
105+ spacing : number ,
106+ ) {
107+ const { x, y, startAngle : start , pixelMargin, innerRadius : innerR } = element ;
108+ const outerRadius = Math . max ( element . outerRadius + spacing + offset - pixelMargin , 0 ) ;
109+ const innerRadius = innerR > 0 ? innerR + spacing + offset + pixelMargin : 0 ;
110+
111+ ctx . beginPath ( ) ;
112+ ctx . arc ( x , y , outerRadius , start , start + TAU ) ;
113+
114+ if ( innerRadius > 0 ) {
115+ // Start the inner contour as a separate subpath to avoid a seam connector.
116+ ctx . moveTo ( x + Math . cos ( start ) * innerRadius , y + Math . sin ( start ) * innerRadius ) ;
117+ ctx . arc ( x , y , innerRadius , start + TAU , start , true ) ;
118+ }
119+
120+ ctx . closePath ( ) ;
121+ }
122+
101123
102124/**
103125 * Path the arc, respecting border radius by separating into left and right halves.
@@ -122,39 +144,88 @@ function pathArc(
122144 circular : boolean ,
123145) {
124146 const { x, y, startAngle : start , pixelMargin, innerRadius : innerR } = element ;
147+ const { spacingMode = 'angular' } = element . options ;
148+ const alpha = end - start ;
149+
150+ if ( circular && element . options . selfJoin && Math . abs ( alpha ) >= TAU - 1e-4 ) {
151+ pathFullCircle ( ctx , element , offset , spacing ) ;
152+ return ;
153+ }
125154
126155 const outerRadius = Math . max ( element . outerRadius + spacing + offset - pixelMargin , 0 ) ;
127- const innerRadius = innerR > 0 ? innerR + spacing + offset + pixelMargin : 0 ;
156+ let innerRadius = innerR > 0 ? innerR + spacing + offset + pixelMargin : 0 ;
128157
129- let spacingOffset = 0 ;
130- const alpha = end - start ;
158+ let outerSpacingOffset = 0 ;
159+ let innerSpacingOffset = 0 ;
160+ const beta = outerRadius > 0
161+ ? Math . max ( 0.001 , alpha * outerRadius - offset / PI ) / outerRadius
162+ : 0.001 ;
163+ const angleOffset = ( alpha - beta ) / 2 ;
131164
132165 if ( spacing ) {
133166 // When spacing is present, it is the same for all items
134167 // So we adjust the start and end angle of the arc such that
135168 // the distance is the same as it would be without the spacing
136169 const noSpacingInnerRadius = innerR > 0 ? innerR - spacing : 0 ;
137170 const noSpacingOuterRadius = outerRadius > 0 ? outerRadius - spacing : 0 ;
138- const avNogSpacingRadius = ( noSpacingInnerRadius + noSpacingOuterRadius ) / 2 ;
139- const adjustedAngle = avNogSpacingRadius !== 0 ? ( alpha * avNogSpacingRadius ) / ( avNogSpacingRadius + spacing ) : alpha ;
140- spacingOffset = ( alpha - adjustedAngle ) / 2 ;
171+ const avgNoSpacingRadius = ( noSpacingInnerRadius + noSpacingOuterRadius ) / 2 ;
172+ const proportionalOffset = ( ( ) => {
173+ const adjustedAngle = avgNoSpacingRadius !== 0 ? ( alpha * avgNoSpacingRadius ) / ( avgNoSpacingRadius + spacing ) : alpha ;
174+ return ( alpha - adjustedAngle ) / 2 ;
175+ } ) ( ) ;
176+ const angularOffset = avgNoSpacingRadius > 0 ? Math . asin ( Math . min ( 1 , spacing / avgNoSpacingRadius ) ) : 0 ;
177+
178+ // Keep spacing trims below half the available span after base offset trimming.
179+ const maxOffset = Math . max ( 0 , beta / 2 - 0.001 ) ;
180+ const maxOffsetSin = Math . sin ( maxOffset ) ;
181+
182+ if ( spacingMode === 'parallel' ) {
183+ if ( innerRadius === 0 && maxOffsetSin > 0 ) {
184+ // A root radius of zero cannot realize a non-zero parallel separator width.
185+ // Raise the root just enough for the available angular span.
186+ const minInnerRadius = spacing / maxOffsetSin ;
187+ const maxInnerRadius = Math . max ( 0 , outerRadius - 0.001 ) ;
188+ innerRadius = Math . min ( minInnerRadius , maxInnerRadius ) ;
189+ }
190+
191+ // Use one bounded spacing value for both radii so large spacing keeps stable geometry.
192+ const maxParallelSpacing = Math . min (
193+ outerRadius > 0 ? outerRadius * maxOffsetSin : Number . POSITIVE_INFINITY ,
194+ innerRadius > 0 ? innerRadius * maxOffsetSin : Number . POSITIVE_INFINITY
195+ ) ;
196+ const parallelSpacing = Math . min ( spacing , maxParallelSpacing ) ;
197+
198+ outerSpacingOffset = outerRadius > 0
199+ ? Math . asin ( Math . min ( 1 , parallelSpacing / outerRadius ) )
200+ : Math . min ( maxOffset , angularOffset ) ;
201+ innerSpacingOffset = innerRadius > 0
202+ ? Math . asin ( Math . min ( 1 , parallelSpacing / innerRadius ) )
203+ : outerSpacingOffset ;
204+ } else if ( spacingMode === 'proportional' ) {
205+ outerSpacingOffset = Math . min ( maxOffset , proportionalOffset ) ;
206+ innerSpacingOffset = Math . min ( maxOffset , proportionalOffset ) ;
207+ } else {
208+ outerSpacingOffset = Math . min ( maxOffset , angularOffset ) ;
209+ innerSpacingOffset = Math . min ( maxOffset , angularOffset ) ;
210+ }
141211 }
142212
143- const beta = Math . max ( 0.001 , alpha * outerRadius - offset / PI ) / outerRadius ;
144- const angleOffset = ( alpha - beta ) / 2 ;
145- const startAngle = start + angleOffset + spacingOffset ;
146- const endAngle = end - angleOffset - spacingOffset ;
147- const { outerStart, outerEnd, innerStart, innerEnd} = parseBorderRadius ( element , innerRadius , outerRadius , endAngle - startAngle ) ;
213+ const outerStartAngle = start + angleOffset + outerSpacingOffset ;
214+ const outerEndAngle = end - angleOffset - outerSpacingOffset ;
215+ const innerStartAngle = start + angleOffset + innerSpacingOffset ;
216+ const innerEndAngle = end - angleOffset - innerSpacingOffset ;
217+ const angleDelta = Math . min ( outerEndAngle - outerStartAngle , innerEndAngle - innerStartAngle ) ;
218+ const { outerStart, outerEnd, innerStart, innerEnd} = parseBorderRadius ( element , innerRadius , outerRadius , angleDelta ) ;
148219
149220 const outerStartAdjustedRadius = outerRadius - outerStart ;
150221 const outerEndAdjustedRadius = outerRadius - outerEnd ;
151- const outerStartAdjustedAngle = startAngle + outerStart / outerStartAdjustedRadius ;
152- const outerEndAdjustedAngle = endAngle - outerEnd / outerEndAdjustedRadius ;
222+ const outerStartAdjustedAngle = outerStartAngle + outerStart / outerStartAdjustedRadius ;
223+ const outerEndAdjustedAngle = outerEndAngle - outerEnd / outerEndAdjustedRadius ;
153224
154225 const innerStartAdjustedRadius = innerRadius + innerStart ;
155226 const innerEndAdjustedRadius = innerRadius + innerEnd ;
156- const innerStartAdjustedAngle = startAngle + innerStart / innerStartAdjustedRadius ;
157- const innerEndAdjustedAngle = endAngle - innerEnd / innerEndAdjustedRadius ;
227+ const innerStartAdjustedAngle = innerStartAngle + innerStart / innerStartAdjustedRadius ;
228+ const innerEndAdjustedAngle = innerEndAngle - innerEnd / innerEndAdjustedRadius ;
158229
159230 ctx . beginPath ( ) ;
160231
@@ -167,38 +238,38 @@ function pathArc(
167238 // The corner segment from point 2 to point 3
168239 if ( outerEnd > 0 ) {
169240 const pCenter = rThetaToXY ( outerEndAdjustedRadius , outerEndAdjustedAngle , x , y ) ;
170- ctx . arc ( pCenter . x , pCenter . y , outerEnd , outerEndAdjustedAngle , endAngle + HALF_PI ) ;
241+ ctx . arc ( pCenter . x , pCenter . y , outerEnd , outerEndAdjustedAngle , outerEndAngle + HALF_PI ) ;
171242 }
172243
173244 // The line from point 3 to point 4
174- const p4 = rThetaToXY ( innerEndAdjustedRadius , endAngle , x , y ) ;
245+ const p4 = rThetaToXY ( innerEndAdjustedRadius , innerEndAngle , x , y ) ;
175246 ctx . lineTo ( p4 . x , p4 . y ) ;
176247
177248 // The corner segment from point 4 to point 5
178249 if ( innerEnd > 0 ) {
179250 const pCenter = rThetaToXY ( innerEndAdjustedRadius , innerEndAdjustedAngle , x , y ) ;
180- ctx . arc ( pCenter . x , pCenter . y , innerEnd , endAngle + HALF_PI , innerEndAdjustedAngle + Math . PI ) ;
251+ ctx . arc ( pCenter . x , pCenter . y , innerEnd , innerEndAngle + HALF_PI , innerEndAdjustedAngle + Math . PI ) ;
181252 }
182253
183254 // The inner arc from point 5 to point b to point 6
184- const innerMidAdjustedAngle = ( ( endAngle - ( innerEnd / innerRadius ) ) + ( startAngle + ( innerStart / innerRadius ) ) ) / 2 ;
185- ctx . arc ( x , y , innerRadius , endAngle - ( innerEnd / innerRadius ) , innerMidAdjustedAngle , true ) ;
186- ctx . arc ( x , y , innerRadius , innerMidAdjustedAngle , startAngle + ( innerStart / innerRadius ) , true ) ;
255+ const innerMidAdjustedAngle = ( ( innerEndAngle - ( innerEnd / innerRadius ) ) + ( innerStartAngle + ( innerStart / innerRadius ) ) ) / 2 ;
256+ ctx . arc ( x , y , innerRadius , innerEndAngle - ( innerEnd / innerRadius ) , innerMidAdjustedAngle , true ) ;
257+ ctx . arc ( x , y , innerRadius , innerMidAdjustedAngle , innerStartAngle + ( innerStart / innerRadius ) , true ) ;
187258
188259 // The corner segment from point 6 to point 7
189260 if ( innerStart > 0 ) {
190261 const pCenter = rThetaToXY ( innerStartAdjustedRadius , innerStartAdjustedAngle , x , y ) ;
191- ctx . arc ( pCenter . x , pCenter . y , innerStart , innerStartAdjustedAngle + Math . PI , startAngle - HALF_PI ) ;
262+ ctx . arc ( pCenter . x , pCenter . y , innerStart , innerStartAdjustedAngle + Math . PI , innerStartAngle - HALF_PI ) ;
192263 }
193264
194265 // The line from point 7 to point 8
195- const p8 = rThetaToXY ( outerStartAdjustedRadius , startAngle , x , y ) ;
266+ const p8 = rThetaToXY ( outerStartAdjustedRadius , outerStartAngle , x , y ) ;
196267 ctx . lineTo ( p8 . x , p8 . y ) ;
197268
198269 // The corner segment from point 8 to point 1
199270 if ( outerStart > 0 ) {
200271 const pCenter = rThetaToXY ( outerStartAdjustedRadius , outerStartAdjustedAngle , x , y ) ;
201- ctx . arc ( pCenter . x , pCenter . y , outerStart , startAngle - HALF_PI , outerStartAdjustedAngle ) ;
272+ ctx . arc ( pCenter . x , pCenter . y , outerStart , outerStartAngle - HALF_PI , outerStartAdjustedAngle ) ;
202273 }
203274 } else {
204275 ctx . moveTo ( x , y ) ;
@@ -265,6 +336,7 @@ function drawBorder(
265336 }
266337
267338 let endAngle = element . endAngle ;
339+ const isFullCircle = Math . abs ( endAngle - startAngle ) >= TAU - 1e-4 ;
268340 if ( fullCircles ) {
269341 pathArc ( ctx , element , offset , spacing , endAngle , circular ) ;
270342 for ( let i = 0 ; i < fullCircles ; ++ i ) {
@@ -279,7 +351,8 @@ function drawBorder(
279351 clipArc ( ctx , element , endAngle ) ;
280352 }
281353
282- if ( options . selfJoin && endAngle - startAngle >= PI && borderRadius === 0 && borderJoinStyle !== 'miter' ) {
354+ const skipSelfClip = isFullCircle && element . innerRadius > 0 ;
355+ if ( ! skipSelfClip && options . selfJoin && endAngle - startAngle >= PI && borderRadius === 0 && borderJoinStyle !== 'miter' ) {
283356 clipSelf ( ctx , element , endAngle ) ;
284357 }
285358
@@ -311,6 +384,7 @@ export default class ArcElement extends Element<ArcProps, ArcOptions> {
311384 borderWidth : 2 ,
312385 offset : 0 ,
313386 spacing : 0 ,
387+ spacingMode : 'angular' ,
314388 angle : undefined ,
315389 circular : true ,
316390 selfJoin : false ,
@@ -332,6 +406,7 @@ export default class ArcElement extends Element<ArcProps, ArcOptions> {
332406 outerRadius : number ;
333407 pixelMargin : number ;
334408 startAngle : number ;
409+ circular : boolean ;
335410
336411 constructor ( cfg ) {
337412 super ( ) ;
@@ -344,6 +419,7 @@ export default class ArcElement extends Element<ArcProps, ArcOptions> {
344419 this . outerRadius = undefined ;
345420 this . pixelMargin = 0 ;
346421 this . fullCircles = 0 ;
422+ this . circular = false ;
347423
348424 if ( cfg ) {
349425 Object . assign ( this , cfg ) ;
0 commit comments