import assert from "node:assert/strict"; import { execFile } from "node:child_process"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { promisify } from "node:util"; import test from "node:test"; import * as A from "../kit/audit.mjs"; import { turning } from "../kit/turn-build.mjs"; const P = { origin: [0, 0], scale: 1, azimuth: 45, elevation: 30 }; const fixtures = [ ["rotated rectangle", ''], ["rotated circle", ''], ["rotated ellipse", ''], ["nested affine ellipse", ''], ["horizontal skew", ''], ["vertical skew", ''], ["ordinary rectangle", ''], ["ordinary circle", ''], ["ordinary transformed line", ''], ]; let native; const directory = await mkdtemp(join(tmpdir(), "anatomy-affine-bounds-")); try { const page = join(directory, "figure.html"); await writeFile(page, `
${fixtures.map(([,markup],i)=>`${markup}`).join("")}
`); const expression = `Array.from({length:${fixtures.length}},(_,i)=>{const group=document.querySelector('#fixture-'+i),shape=group.querySelector('rect,circle,ellipse,line'),matrix=shape.getCTM(),points=[];if(shape.tagName==='rect'){const b=shape.getBBox();for(const [x,y] of [[b.x,b.y],[b.x+b.width,b.y],[b.x+b.width,b.y+b.height],[b.x,b.y+b.height]])points.push(new DOMPoint(x,y).matrixTransform(matrix));}else{const length=shape.getTotalLength();for(let j=0;j<=4096;j++)points.push(shape.getPointAtLength(length*j/4096).matrixTransform(matrix));}const b=group.getBBox();return{geometry:[Math.min(...points.map(p=>p.x)),Math.min(...points.map(p=>p.y)),Math.max(...points.map(p=>p.x)),Math.max(...points.map(p=>p.y))],groupBox:[b.x,b.y,b.x+b.width,b.y+b.height]}})`; const result = await promisify(execFile)(process.execPath, ["scripts/drive.mjs", page, JSON.stringify([["eval",expression]])], { timeout: 30000 }); const observations = JSON.parse(result.stdout.match(/eval: (.*)/)[1]); console.log("native SVG group bounds:", JSON.stringify(observations.map(v=>v.groupBox))); native = observations.map(v=>v.geometry); } finally { await rm(directory, { recursive: true, force: true }); } fixtures.forEach(([name,markup],i)=>test(`public bounds and fixed parts match native SVG geometry for ${name}`,()=>{ const expected = native[i]; const observed = A.markupBox(markup); for(let j=0;j<4;j++)assert.ok(Math.abs(observed[j]-expected[j])<.002, `${name} bound ${j}: ${observed[j]} != ${expected[j]}`); const T = turning(P);T.layer("figure"); const part = T.fixed({name:"authored",svg:markup,anchor:[0,0,0],shapes:[A.ball([0,0,0],20)]}); for(let j=0;j<4;j++)assert.ok(Math.abs(part.spec.box[j]-(expected[j]+(j<2?-.5:.5)))<.002); })); test("coverage rejects a narrow stand-in for a rotated rectangle",()=>{ const cam=A.cameraBasis(P); const end=cam.D.map(v=>v*Math.sqrt(200)/cam.k); const standIn=A.tube([[0,0,0],end],.15); const item={name:"plate",svg:fixtures[0][1],shapes:[standIn]}; const result=A.coverage([item],P,{pad:0}); assert.equal(result.length,1);assert.ok(result[0].share<.1); assert.equal(A.coverage([{...item,shapes:[A.ball([0,0,0],20)]}],P,{pad:0}).length,0); }); test("transformed rectangular clips keep every corner",()=>{ const clipped=''; const observed=A.markupBox(clipped),root=Math.sqrt(50); for(const [i,v] of [-root,0,root,2*root].entries())assert.ok(Math.abs(observed[i]-v)<1e-8); });