From 24d235ad793a48e6ba523e0a7a3fcb4d705eca19 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 17 Mar 2026 22:04:25 +0000 Subject: [PATCH 1/2] fix: keep page size fixed in auto-adjust and correct ring score direction Co-authored-by: TaylorHere --- archery/shared/layout.ts | 2 +- archery/src/main.ts | 60 +--------------------------------------- 2 files changed, 2 insertions(+), 60 deletions(-) diff --git a/archery/shared/layout.ts b/archery/shared/layout.ts index f29c56d..05d58db 100644 --- a/archery/shared/layout.ts +++ b/archery/shared/layout.ts @@ -273,7 +273,7 @@ export function ringColorByIndex(ringIndexFromOuter: number, startWithBlack: boo export function ringScoreByIndex(ringIndexFromOuter: number, ringCount: number): number { const safeRingCount = Math.max(1, Math.round(ringCount)); - return safeRingCount - ringIndexFromOuter; + return Math.min(safeRingCount, ringIndexFromOuter + 1); } export function ringFillColor( diff --git a/archery/src/main.ts b/archery/src/main.ts index 8097641..0642e86 100644 --- a/archery/src/main.ts +++ b/archery/src/main.ts @@ -319,17 +319,7 @@ function autoAdjustToFeasible(input: LayoutInput, editedField: EditedField): str } } - const customPage = suggestNearestCustomPageForFixedDiameter(input); - if (!customPage) { - return null; - } - - refs.pagePreset.value = 'custom'; - refs.customWidth.value = `${customPage.widthMm.toFixed(1)}`; - refs.customHeight.value = `${customPage.heightMm.toFixed(1)}`; - toggleCustomSizeInputs(); - - return `已自动切换为自定义页面 ${customPage.widthMm.toFixed(1)} × ${customPage.heightMm.toFixed(1)} mm。`; + return null; } function getMaxStrictSpacingForCurrentDiameter(input: LayoutInput): number | null { @@ -367,54 +357,6 @@ function getMaxStrictSpacingForCurrentDiameter(input: LayoutInput): number | nul return Number.isFinite(maxSpacing) ? maxSpacing : null; } -function suggestNearestCustomPageForFixedDiameter( - input: LayoutInput, -): { widthMm: number; heightMm: number } | null { - const W = input.pageWidthMm; - const H = input.pageHeightMm; - const D = input.targetDiameterMm; - const s = input.minSpacingMm; - if (!Number.isFinite(W) || !Number.isFinite(H) || !Number.isFinite(D) || !Number.isFinite(s)) { - return null; - } - if (W <= 0 || H <= 0 || D <= 0 || s <= 0) { - return null; - } - - const nApprox = Math.max(1, Math.round((W - s) / (D + s))); - const mApprox = Math.max(1, Math.round((H - s) / (D + s))); - const nMin = Math.max(1, nApprox - 8); - const nMax = nApprox + 8; - const mMin = Math.max(1, mApprox - 8); - const mMax = mApprox + 8; - - let best: { widthMm: number; heightMm: number; score: number; total: number } | null = null; - for (let n = nMin; n <= nMax; n += 1) { - for (let m = mMin; m <= mMax; m += 1) { - const widthMm = n * D + (n + 1) * s; - const heightMm = m * D + (m + 1) * s; - if (widthMm <= 0 || heightMm <= 0) { - continue; - } - - const score = Math.abs(widthMm - W) + Math.abs(heightMm - H); - const total = n * m; - if ( - !best || - score < best.score - EPS || - (Math.abs(score - best.score) <= EPS && total > best.total) - ) { - best = { widthMm, heightMm, score, total }; - } - } - } - - if (!best) { - return null; - } - return { widthMm: best.widthMm, heightMm: best.heightMm }; -} - function resolveEditedField(el: HTMLElement): EditedField { if (el === refs.pagePreset) { return 'page'; From 71ec74babfcc7614926534518b4e234cde324c81 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 17 Mar 2026 22:06:54 +0000 Subject: [PATCH 2/2] refactor: remove backend layout verification button and api Co-authored-by: TaylorHere --- archery/README.md | 21 ++--------- archery/functions/api/layout.ts | 66 --------------------------------- archery/package.json | 2 +- archery/src/main.ts | 30 --------------- archery/src/style.css | 5 --- 5 files changed, 5 insertions(+), 119 deletions(-) delete mode 100644 archery/functions/api/layout.ts diff --git a/archery/README.md b/archery/README.md index 68130cb..5018da6 100644 --- a/archery/README.md +++ b/archery/README.md @@ -1,11 +1,11 @@ # 射箭训练标靶生成系统(Cloudflare 可部署) -该项目是一个可直接部署的“前端 + 后端 + PDF”一体化实现,满足以下核心目标: +该项目是一个可直接部署的“前端 + PDF”一体化实现,满足以下核心目标: - 前端动态配置参数并实时预览(SVG 矢量) - 严格几何约束下自动求最优排布(最大化 `n × m`) - 浏览器端生成可打印 PDF(矢量,毫米精确) -- Cloudflare Pages + Functions 无服务器部署 +- Cloudflare Pages 无服务器部署 --- @@ -35,15 +35,6 @@ - 尺寸按 mm 精确映射到 pt(`72 / 25.4`) - 支持下载打印 -### 后端(Cloudflare Functions) - -- `POST /api/layout` -- 入参:页面宽高、标靶直径、最小间距 -- 出参:最优 `n/m/s/total` 或无解原因 -- 前后端共用 `shared/layout.ts`,避免算法漂移 - ---- - ## 2. 关键几何约束 设页面尺寸为 `W × H`,标靶直径 `D`,列数 `n`,行数 `m`,统一间距/页边距 `s`,必须同时满足: @@ -69,11 +60,8 @@ H = m·D + (m+1)·s ```text archery/ -├─ functions/ -│ └─ api/ -│ └─ layout.ts # Cloudflare Pages Function(后端API) ├─ shared/ -│ └─ layout.ts # 前后端共用的排布算法 +│ └─ layout.ts # 排布算法 ├─ src/ │ ├─ main.ts # 前端页面与SVG实时预览 │ ├─ pdf.ts # PDF矢量导出 @@ -114,8 +102,7 @@ npm run build 1. 构建 `archery` 前端 2. 部署 `archery/dist` 静态资源 -3. 一并部署 `archery/functions` 的 Serverless API -4. 当主分支部署时,自动确保生产域名为 `archery.taylorhere.com` +3. 当主分支部署时,自动确保生产域名为 `archery.taylorhere.com` > 说明:`CLOUDFLARE_API_TOKEN` 需包含 Pages 项目与域名管理权限(至少覆盖 Pages 编辑权限)。 diff --git a/archery/functions/api/layout.ts b/archery/functions/api/layout.ts deleted file mode 100644 index 67b32da..0000000 --- a/archery/functions/api/layout.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { diagnoseNoLayoutReason, solveOptimalLayout, validateInput, type LayoutInput } from '../../shared/layout'; - -interface JsonPayload { - pageWidthMm?: unknown; - pageHeightMm?: unknown; - targetDiameterMm?: unknown; - minSpacingMm?: unknown; - layoutMode?: unknown; - desiredTargets?: unknown; -} - -export const onRequestPost: PagesFunction = async (context) => { - try { - const payload = (await context.request.json()) as JsonPayload; - const input: LayoutInput = { - pageWidthMm: Number(payload.pageWidthMm), - pageHeightMm: Number(payload.pageHeightMm), - targetDiameterMm: Number(payload.targetDiameterMm), - minSpacingMm: Number(payload.minSpacingMm), - layoutMode: payload.layoutMode === 'target_count' ? 'target_count' : 'auto_fill', - desiredTargets: payload.desiredTargets === undefined ? undefined : Number(payload.desiredTargets), - }; - - const validation = validateInput(input); - if (!validation.ok) { - return json( - { - ok: false, - layout: null, - reason: validation.reason ?? '参数无效', - }, - 400, - ); - } - - const layout = solveOptimalLayout(input); - if (!layout) { - return json({ - ok: false, - layout: null, - reason: diagnoseNoLayoutReason(input), - }); - } - - return json({ ok: true, layout }); - } catch { - return json( - { - ok: false, - layout: null, - reason: '请求体必须是 JSON', - }, - 400, - ); - } -}; - -function json(body: unknown, status = 200): Response { - return new Response(JSON.stringify(body), { - status, - headers: { - 'content-type': 'application/json; charset=utf-8', - 'cache-control': 'no-store', - }, - }); -} diff --git a/archery/package.json b/archery/package.json index 3827cfa..a4aaa7d 100644 --- a/archery/package.json +++ b/archery/package.json @@ -8,7 +8,7 @@ "build": "tsc && vite build", "preview": "vite preview", "typecheck": "tsc --noEmit", - "dev:pages": "vite build && wrangler pages dev dist --functions functions" + "dev:pages": "vite build && wrangler pages dev dist" }, "devDependencies": { "typescript": "~5.9.3", diff --git a/archery/src/main.ts b/archery/src/main.ts index 0642e86..7df35ea 100644 --- a/archery/src/main.ts +++ b/archery/src/main.ts @@ -105,7 +105,6 @@ app.innerHTML = ` 100% -
@@ -146,7 +145,6 @@ const refs = { outTotal: getEl('out-total'), outPage: getEl('out-page'), downloadBtn: getEl('download-pdf'), - verifyApiBtn: getEl('verify-api'), }; let currentLayout: LayoutSolution | null = null; @@ -210,34 +208,6 @@ refs.downloadBtn.addEventListener('click', async () => { refs.status.textContent = 'PDF 已生成:矢量图可直接打印,请使用 100% 缩放。'; }); -refs.verifyApiBtn.addEventListener('click', async () => { - const input = buildLayoutInput(); - refs.status.textContent = '正在请求后端 API 复核...'; - - try { - const response = await fetch('/api/layout', { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify(input), - }); - - if (!response.ok) { - refs.status.textContent = `后端 API 异常:HTTP ${response.status}`; - return; - } - - const data = (await response.json()) as { ok: boolean; layout: LayoutSolution | null; reason?: string }; - if (!data.ok || !data.layout) { - refs.status.textContent = `后端复核:无可行解。${data.reason ? `原因:${data.reason}` : ''}`; - return; - } - - refs.status.textContent = `后端复核成功:n=${data.layout.columns}, m=${data.layout.rows}, s=${data.layout.spacingMm.toFixed(4)}mm`; - } catch { - refs.status.textContent = '后端 API 不可达(本地前端计算仍可使用)。部署到 Cloudflare Pages 后可直接复核。'; - } -}); - function recalculate(editedField: EditedField = 'other'): void { let input = buildLayoutInput(); let layout = solveOptimalLayout(input); diff --git a/archery/src/style.css b/archery/src/style.css index 73f5a39..0ecf536 100644 --- a/archery/src/style.css +++ b/archery/src/style.css @@ -107,11 +107,6 @@ button { cursor: pointer; } -button.secondary { - background: #fff; - color: #1d4ed8; -} - button:hover { filter: brightness(0.96); }