@@ -877,6 +877,46 @@ describe("layout-audit.browser coordinate-frame findings", () => {
877877 // "knowledge-overflow" contains conn-family substrings only across word boundaries — no match.
878878 expect ( runAudit ( ) . filter ( ( issue ) => issue . code === "connector_detached" ) ) . toEqual ( [ ] ) ;
879879 } ) ;
880+
881+ it ( "rejects a shallow-curvature arc as a phantom ring node but keeps a genuine near-circular arc" , ( ) => {
882+ document . body . innerHTML = `
883+ <div id="root" data-composition-id="main" data-width="1920" data-height="1080">
884+ <svg id="dial-svg">
885+ <path id="genuine-ring" fill="none" d="ring" />
886+ <path id="shallow-arc" fill="none" d="arc" />
887+ </svg>
888+ </div>
889+ ` ;
890+ // A near-full circle (diameter tracks its bbox) vs a shallow arc on a huge
891+ // circle: the arc's Kåsa residual normalized by its enormous radius is tiny
892+ // (passes the residual gate) yet its fitted diameter is ~10x its drawn box.
893+ const genuine = { cx : 500 , cy : 500 , radius : 100 , startDeg : 0 , endDeg : 360 } ;
894+ const shallow = { cx : 500 , cy : 2500 , radius : 2000 , startDeg : 264 , endDeg : 276 } ;
895+ installGeometry (
896+ {
897+ root : rect ( { left : 0 , top : 0 , width : 1920 , height : 1080 } ) ,
898+ "dial-svg" : rect ( { left : 200 , top : 200 , width : 700 , height : 700 } ) ,
899+ "genuine-ring" : arcBBox ( genuine ) ,
900+ "shallow-arc" : arcBBox ( shallow ) ,
901+ } ,
902+ {
903+ "genuine-ring" : { fill : "none" } ,
904+ "shallow-arc" : { fill : "none" } ,
905+ } ,
906+ ) ;
907+ installArcSampling ( "genuine-ring" , genuine ) ;
908+ installArcSampling ( "shallow-arc" , shallow ) ;
909+ installAuditScript ( ) ;
910+
911+ const sample = (
912+ window as unknown as {
913+ __hyperframesConnectorSample : ( ) => { nodes : Array < { selector : string } > } ;
914+ }
915+ ) . __hyperframesConnectorSample ( ) ;
916+ const nodeSelectors = sample . nodes . map ( ( node ) => node . selector ) ;
917+ expect ( nodeSelectors ) . toContain ( "#genuine-ring" ) ;
918+ expect ( nodeSelectors ) . not . toContain ( "#shallow-arc" ) ;
919+ } ) ;
880920} ) ;
881921
882922describe ( "layout-audit.browser content overlap" , ( ) => {
@@ -1979,6 +2019,53 @@ function installConnectorGeometry(translate: CtmTranslate): void {
19792019 }
19802020}
19812021
2022+ interface ArcSpec {
2023+ cx : number ;
2024+ cy : number ;
2025+ radius : number ;
2026+ startDeg : number ;
2027+ endDeg : number ;
2028+ }
2029+
2030+ // The point ringPathBox reads at arc-length `length` (local SVG user units).
2031+ function arcPointAt ( spec : ArcSpec , length : number , total : number ) : { x : number ; y : number } {
2032+ const frac = total === 0 ? 0 : length / total ;
2033+ const rad = ( ( spec . startDeg + ( spec . endDeg - spec . startDeg ) * frac ) * Math . PI ) / 180 ;
2034+ return { x : spec . cx + spec . radius * Math . cos ( rad ) , y : spec . cy + spec . radius * Math . sin ( rad ) } ;
2035+ }
2036+
2037+ function arcTotalLength ( spec : ArcSpec ) : number {
2038+ return ( spec . radius * Math . abs ( spec . endDeg - spec . startDeg ) * Math . PI ) / 180 ;
2039+ }
2040+
2041+ // The bounding box of the 16 samples ringPathBox takes (i/16 of total, i=0..15).
2042+ function arcBBox ( spec : ArcSpec ) : DOMRect {
2043+ const total = arcTotalLength ( spec ) ;
2044+ const xs : number [ ] = [ ] ;
2045+ const ys : number [ ] = [ ] ;
2046+ for ( let i = 0 ; i < 16 ; i ++ ) {
2047+ const point = arcPointAt ( spec , ( total * i ) / 16 , total ) ;
2048+ xs . push ( point . x ) ;
2049+ ys . push ( point . y ) ;
2050+ }
2051+ const left = Math . min ( ...xs ) ;
2052+ const top = Math . min ( ...ys ) ;
2053+ return rect ( { left, top, width : Math . max ( ...xs ) - left , height : Math . max ( ...ys ) - top } ) ;
2054+ }
2055+
2056+ // happy-dom has no SVG path geometry: mock getTotalLength/getPointAtLength so
2057+ // ringPathBox samples the given circular arc in local user units.
2058+ function installArcSampling ( pathId : string , spec : ArcSpec ) : void {
2059+ const path = document . getElementById ( pathId ) ;
2060+ if ( ! path ) throw new Error ( `no path #${ pathId } ` ) ;
2061+ const total = arcTotalLength ( spec ) ;
2062+ Object . defineProperty ( path , "getTotalLength" , { value : ( ) => total , configurable : true } ) ;
2063+ Object . defineProperty ( path , "getPointAtLength" , {
2064+ value : ( length : number ) => arcPointAt ( spec , length , total ) ,
2065+ configurable : true ,
2066+ } ) ;
2067+ }
2068+
19822069function installAuditScript ( ) : void {
19832070 window . eval ( script ) ;
19842071}
0 commit comments