From 79afda0085ad80820dcdeaa0c004efbc1fa622a4 Mon Sep 17 00:00:00 2001 From: toshiocn <48112360+toshiocn@users.noreply.github.com> Date: Mon, 21 Sep 2026 17:58:02 +0800 Subject: [PATCH] feat(airport): draw six equal apron fans instead of strip runways Sectors are 60-degree wedges from the board centre, tucked against the tray. Radial seams mark the ABCDEF flats. Roll back with git checkout feat/airport-edge-runway. --- labs/airport/README.md | 2 +- labs/airport/app.js | 31 ++++++++++++++++++++++--------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/labs/airport/README.md b/labs/airport/README.md index 6d0e649..58d1894 100644 --- a/labs/airport/README.md +++ b/labs/airport/README.md @@ -8,7 +8,7 @@ 点击地面飞机起飞,点击场外停机坪上的飞机降落;左右转每次 60°。成功、失败与旋转都消耗一次。灰色轨道不分颜色;来机匹配彩色专用机位。P 标记表示已停好的来机。它再次起飞后,停到实际出口那条边的停机坪,仍需重新停好。 -板边轨道一直画到托盘外缘,不伸出棋盘。托盘外的浅色停机坪航线固定不动。当等待机在当前朝向下接得上同色机位时,盘内通道用中性强调色点亮;旋转时通道会亮灭,据此判断能否进场。 +板边轨道一直画到托盘外缘,不伸出棋盘。托盘外是以棋盘中心为焦点的六等分扇形停机坪,对应六条平边;扇形不随底盘旋转。当等待机在当前朝向下接得上同色机位时,提示线从飞机接到盘内通道;旋转时通道会亮灭,据此判断能否进场。 ## 关卡证据 diff --git a/labs/airport/app.js b/labs/airport/app.js index 09dce02..480177d 100644 --- a/labs/airport/app.js +++ b/labs/airport/app.js @@ -1,4 +1,4 @@ -import { AirportGame, SIDES, boardAt, catalogs, parkedCount, copy, boardRadius, waitingPose, apronDistance, trayHull, rimCrossing, bayNormal, TRAY_PAD } from './core.js'; +import { AirportGame, SIDES, boardAt, catalogs, parkedCount, copy, boardRadius, waitingPose, apronDistance, trayHull, rimCrossing, TRAY_PAD } from './core.js'; import { LEVELS } from './levels.js'; import { requestReward } from './rewards.js'; import { position, PORTS, hexPoints, trackPoints, sweepPath } from '../../src/path-math.js'; @@ -22,7 +22,6 @@ try { const value = JSON.parse(localStorage.getItem(STORAGE)); if (value && Arra function persist() { try { localStorage.setItem(STORAGE, JSON.stringify(saved)); } catch {} } let index = Number.isInteger(saved.last) && saved.last >= 0 && saved.last < LEVELS.length ? saved.last : 0; let game, motions = [], rotation = null, feedback = null, queue = [], requestId = 0, diagnosis = null, generation = 0; -const APRON_WIDTH = 1.9; let view = { width: 390, height: 460, size: 28, ox: 195, oy: 230 }, audioContext; const worker = new Worker(new URL('./solver-worker.js', import.meta.url), { type: 'module' }); worker.onmessage = ({ data }) => { @@ -242,6 +241,19 @@ function clipToHull(points,poly){ } return out; } +function apronSector(hull,side,outerR,steps=12){ + const a0=-Math.PI/2+side*Math.PI/3,a1=a0+Math.PI/3,pts=[]; + for(let i=0;i<=steps;i++){ + const a=a0+(a1-a0)*i/steps,dir={x:Math.cos(a),y:Math.sin(a)}; + const t=rimCrossing(hull,{x:0,y:0},dir),r=(t??0)+.08; + pts.push({x:dir.x*r,y:dir.y*r}); + } + for(let i=steps;i>=0;i--){ + const a=a0+(a1-a0)*i/steps; + pts.push({x:Math.cos(a)*outerR,y:Math.sin(a)*outerR}); + } + return pts; +} function aircraft(plane,where,alpha=1){ ctx.save();ctx.translate(where.x,where.y);ctx.globalAlpha=alpha; const color=pigments[plane.color].color; @@ -264,15 +276,16 @@ function draw(now){ ctx.translate(view.ox,view.oy);ctx.scale(view.size,view.size); const board=rotation?rotation.old:catalogs(game.level)[game.state.orientation].board; const hullPts=trayHull(board.cells); - // Fixed apron slabs sit under everything: waiting planes park on a soft lane outside - // the tray, on each side's flat-side normal, so a plane's side is readable before any - // hint appears. They never turn with the base. + // Six equal apron fans, fixed around the board centre. They replace the old + // strip runways so each visual flat (the ABCDEF wedges) is a readable sector. const radius=boardRadius(game.level.cells),apronR=apronDistance(radius); + const stacks=Math.max(1,...[0,1,2,3,4,5].map(s=>game.level.arrivals.filter(a=>a.side===s).length)); + const outerR=apronR+(stacks-1)*1.12+.85; + for(let side=0;side<6;side++)polygon(apronSector(hullPts,side,outerR),materials.shadowSoft); for(let side=0;side<6;side++){ - const n=bayNormal(side),count=Math.max(1,game.level.arrivals.filter(a=>a.side===side).length); - const inner=rimCrossing(hullPts,{x:0,y:0},n)+0.20,outer=apronR+(count-1)*1.12+0.85; - if(!(outer>inner))continue; - line([{x:n.x*inner,y:n.y*inner},{x:n.x*outer,y:n.y*outer}],materials.shadowSoft,APRON_WIDTH); + const a=-Math.PI/2+side*Math.PI/3,dir={x:Math.cos(a),y:Math.sin(a)}; + const t=rimCrossing(hullPts,{x:0,y:0},dir),inner=(t??0)+.08; + line([{x:dir.x*inner,y:dir.y*inner},{x:dir.x*outerR,y:dir.y*outerR}],materials.shadow,.045); } ctx.save(); if(rotation){const p=Math.min(1,(now-rotation.start)/rotation.duration),u=p*p*(3-2*p);ctx.rotate(rotation.direction*Math.PI/3*u);}