Screw conveyors join by the shared rule: low ends and verticals join in the corner block (t67 phase 2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 13:30:44 -04:00
co-authored by Claude Opus 5.5
parent 198bea54da
commit fd35bf334f
14 changed files with 358 additions and 209 deletions
+84 -37
View File
@@ -97,58 +97,105 @@ with sync_playwright() as p:
}''')
check('sidewall stays behind the grain colour in pixels and smooth',
wall and wall['colors'][0] == wall['colors'][1] and wall['grainClass'] == 2, wall)
# Edge joins keep closed elbows; corner placements match two isolated free ends.
pg.set_viewport_size({"width": 2100, "height": 1300})
# Shared joins: E elbows bend inside the straight block; other placements keep collars.
pg.set_viewport_size({"width": 2100, "height": 1500})
ev("part.fill(0); blockP.fill(0); occ.fill(0); mach.fill(0); ents=[]; ghostBlocks.clear();")
ev("() => {" + (root / "tools/scenes/t62-joins.js").read_text() + "}")
ev("() => {" + (root / "tools/scenes/joins.js").read_text() + "}")
mouths = ev('''() => {
SET.zoom=6; SET.lighting="off"; SET.glow="off"; resizeRender();
SET.zoom=4; SET.lighting="off"; SET.glow="off"; resizeRender();
bot.x=CAM[0]-bot.w/2;bot.y=CAM[1]-bot.h/2;snapCamera();
const now=performance.now.bind(performance),frozen=now();
performance.now=()=>frozen;
try {
const out={};
const draw=()=>{SM.scale=1;drawWorld();};
for(const look of ["pixel","smooth"]) {
SET.look=look;draw();
const bad=[];
for(const j of T62_JOINTS) {
const slope=j.v===4 || j.v===6?-1:1;
const end=j.end*SCREW_DIRS[j.v][0];
const dx=j.vertical?0:end,dy=j.vertical?end*slope:0;
if(j.corner)continue;
for(const side of [-1,1])for(const along of [-0.3,0.3]) {
const x=j.bx*4+2+dx*(2+along)-dy*side*1.6;
const y=j.by*4+2+dy*(2+along)+dx*side*1.6;
const p=worldToScreenPx(x,y),c=R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data;
if(!(c[0]>c[1]*1.15 && c[1]>c[2]*1.1))bad.push([j,x,y,Array.from(c)]);
SET.look=look;SM.scale=1;drawWorld();
const bad=[], copper=c=>c[0]>c[1]*1.15 && c[1]>c[2]*1.1;
const sample=(x,y)=>{const p=worldToScreenPx(x,y);return Array.from(R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data);};
for(const j of JOIN_CASES.filter(j=>j.code===BP_SCREW)) {
const slope=j.v===4 || j.v===6?-1:1, side=j.end?1:-1;
const joins=diagonalScrewJoins(j.bx,j.by,j.v);
if(!j.joined) {
if(!(joins & (64<<j.end)) || (joins>>(j.end*3)&7))bad.push(['missing free collar',j]);
// The middle of the free cap, safely inside its diagonal block.
const cap=look==='pixel' ? [[1.5,2.5],[2.5,1.5]] : [[1.8,1.8]];
const colors=cap.map(([x,y])=>sample(j.bx*4+2+side*x,j.by*4+2+side*slope*y));
if(!colors.some(copper))bad.push(['free collar colour',j,colors]);
continue;
}
const ex=(j.kind>=5?j.nx:j.bx)*4+2,ey=(j.kind>=5?j.ny:j.by)*4+2;
const dx=j.vertical?0:side,dy=j.vertical?side*slope:0;
// Both walls on the outgoing axis, beyond the rounded meeting point.
for(const sign of [-1,1]) {
const x=ex+dx*1.5-dy*sign*1.6,y=ey+dy*1.5+dx*sign*1.6,c=sample(x,y);
if(!copper(c))bad.push(['open casing',j,x,y,c]);
}
// Follow both centrelines through the join: the bore must never show sky.
const ax=j.bx*4+2-side*1.5,ay=j.by*4+2-side*slope*1.5;
for(let t=0;t<=1;t+=0.05)for(const leg of [0,1]) {
const x=leg?ex+dx*t*5:ax+(ex-ax)*t,y=leg?ey+dy*t*5:ay+(ey-ay)*t,c=sample(x,y);
if(c[2]>c[0]*1.4 && c[2]>110)bad.push(['background inside tube',j,x,y,c]);
}
if(j.kind>=5) {
// The unused corner of the bent straight block must become background.
const x=j.nx*4+2+(j.vertical?side:-side)*1.8;
const y=j.ny*4+2+(j.vertical?-side*slope:side*slope)*1.8,c=sample(x,y);
if(copper(c) || c[2]<c[0]*1.2)bad.push(['square casing outside elbow',j,x,y,c]);
}
}
const sample=(bx,by)=>{
const colors=[];
for(let y=0;y<4;y++)for(let x=0;x<4;x++) {
const p=worldToScreenPx(bx*4+x+0.5,by*4+y+0.5);
colors.push(...R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data);
}return colors.join(',');
};
for(const j of T62_JOINTS.filter(j=>j.corner)) {
const [dx,dy]=SCREW_DIRS[j.v],sx=j.bx+j.end*dx,sy=j.by+j.end*dy;
const collar=sample(j.bx,j.by),straight=sample(sx,sy);
const joins=diagonalScrewJoins(j.bx,j.by,j.v),end=j.end*dx>0?1:0;
if(!(joins & (64<<end)) || (joins>>(end*3)&7))bad.push(['missing collar',j]);
removeBlock(sx,sy);draw();
if(sample(j.bx,j.by)!==collar)bad.push(['diagonal differs from isolated end',j]);
placeBlock(BP_SCREW,j.straight,sx,sy,true);
removeBlock(j.bx,j.by);draw();
if(sample(sx,sy)!==straight)bad.push(['straight differs from normal end',j]);
placeBlock(BP_SCREW,j.v,j.bx,j.by,true);draw();
}
out[look]=bad;
}return out;
} finally {performance.now=now;}
}''')
for look, bad in mouths.items():
check(f"{look}: edge elbows stay closed and corners show two separate free ends", not bad, bad[:2])
check(f"{look}: shared screw joins have closed elbows, free collars and no bore holes", not bad, bad[:2])
previews = ev('''() => {
const bad=[], now=performance.now.bind(performance),frozen=now();
performance.now=()=>frozen;
try {
for(const look of ['pixel','smooth'])for(const j of JOIN_CASES.filter(j=>j.code===BP_SCREW && j.spot===2 && j.end===1 && (j.v===5&&!j.vertical || j.v===4&&j.vertical))) {
SET.look=look;
const side=j.end?1:-1,slope=j.v===4||j.v===6?-1:1;
const ux=2+(j.vertical?side:-side)*1.8,uy=2+(j.vertical?-side*slope:side*slope)*1.8;
const b=j.by*BW+j.bx,nb=j.ny*BW+j.nx;
const plan=new Map([[b,[BP_SCREW,j.v]],[nb,[BP_SCREW,j.straight]]]);
removeBlock(j.bx,j.by);removeBlock(j.nx,j.ny);
const sample=(x,y)=>{const p=worldToScreenPx(j.nx*4+x,j.ny*4+y);return Array.from(R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),1,1).data).join(',');};
SM.scale=1;drawWorld();const bg=sample(ux,uy),empty=sample(2,2);
for(const [n,piece] of plan)ghostBlocks.set(n,piece);
drawWorld();
if(sample(ux,uy)!==bg || sample(2,2)===empty)bad.push(['ghost elbow silhouette',look,j]);
ghostBlocks.clear();
const ctx=R.ctx,canvas=document.createElement('canvas');canvas.width=canvas.height=192;
try {
R.ctx=canvas.getContext('2d');R.ctx.fillStyle='#ffffff';
previewBlock(BP_SCREW,64,64,64,64,false,j.straight,j.nx,j.ny,plan);
const alpha=(x,y)=>R.ctx.getImageData(Math.floor(64+x*16),Math.floor(64+y*16),1,1).data[3];
if(alpha(ux,uy)!==0 || alpha(2,2)===0)bad.push(['planned preview elbow silhouette',look,j]);
} finally {R.ctx=ctx;}
placeBlock(BP_SCREW,j.v,j.bx,j.by,true);placeBlock(BP_SCREW,j.straight,j.nx,j.ny,true);
}
} finally {performance.now=now;ghostBlocks.clear();}
return bad;
}''')
check('both looks: ghosts and planned previews leave the unused elbow corner empty', not previews, previews[:2])
tree_tags = ev('''() => {
SET.look='smooth';SM.scale=1;
const now=performance.now.bind(performance),frozen=now();performance.now=()=>frozen;
try {
const j=JOIN_CASES.find(j=>j.v===5 && j.end===1 && !j.vertical && j.spot===2 && j.code===BP_SCREW);
const bx=j.nx+1,by=j.ny;
const sample=()=>{const p=worldToScreenPx(bx*4,by*4);return Array.from(R.ctx.getImageData(Math.floor(p.x),Math.floor(p.y),4*R.zp,4*R.zp).data).join(',');};
drawWorld();const plain=sample(),bad=[];
for(const m of [WOOD,LEAVES]) {
for(let y=by*4;y<by*4+4;y++)for(let x=bx*4;x<bx*4+4;x++)bgm[y*W+x]=m;
drawWorld();if(sample()!==plain)bad.push(m);
}
for(let y=by*4;y<by*4+4;y++)for(let x=bx*4;x<bx*4+4;x++)bgm[y*W+x]=0;
return bad;
} finally {performance.now=now;}
}''')
check('smooth: trees behind a straight screw cannot become elbow join bits', not tree_tags, tree_tags)
# a browser without hardware drawing gets the pixel look and a note
pg2 = b.new_page(viewport={'width': 800, 'height': 500})
pg2.add_init_script('localStorage.clear();')