diff --git a/package.json b/package.json index 76422b1..c38542c 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "type": "module", "scripts": { "dev": "vite", + "test": "node --test", "prebuild": "npm run check:template-sync", "build": "vite build", "build:pages": "npm run check:template-sync && vite build --base=/OpenBox3D/", diff --git a/src/design/layers.js b/src/design/layers.js index 54f43e9..94d3bcd 100644 --- a/src/design/layers.js +++ b/src/design/layers.js @@ -19,8 +19,15 @@ export function bboxOf(l) { } export function dpiOf(l) { - if (l.kind !== 'image' || !l.pxw) return 0; - return Math.round(l.pxw / (l.w / 25.4)); + if (l.kind !== 'image') return 0; + const pxw = Number(l.pxw || l.imgW), pxh = Number(l.imgH); + const w = Number(l.w), h = Number(l.h); + const crop = Array.isArray(l.crop) ? l.crop : [0, 0, 1, 1]; + const cropW = Number(crop[2]), cropH = Number(crop[3]); + if (![pxw, pxh, w, h, cropW, cropH].every(v => Number.isFinite(v) && v > 0)) return 0; + const dpiX = pxw * cropW / (w / 25.4); + const dpiY = pxh * cropH / (h / 25.4); + return Math.round(Math.min(dpiX, dpiY)); } // 图集 UV 用:每个面板(fill 多边形)的包围盒,供 3D 与导出对齐 diff --git a/src/ui/StructureView.jsx b/src/ui/StructureView.jsx index de27ca3..7bd5263 100644 --- a/src/ui/StructureView.jsx +++ b/src/ui/StructureView.jsx @@ -37,7 +37,14 @@ export function StructureView() { { k: '含出血', v: fmt(sw + 2 * s.bleed) + ' × ' + fmt(sh + 2 * s.bleed) }, { k: '纸厚 t', v: t + ' mm' } ].concat(specTpl[s.tpl] || []); - const checks = ['外轮廓闭合', '无自交 / 孤立线段', '最小刀距 ≥ 1.5 mm', '压痕两侧均有连接面', '出血 ≥ ' + m.bleed + ' mm(' + m.note + ')']; + const bleedOk = s.bleed >= m.bleed; + const checks = [ + { label: '外轮廓闭合:未自动检测', dot: '#9a8f7e' }, + { label: '自交 / 孤立线段:未自动检测', dot: '#9a8f7e' }, + { label: '最小刀距 ≥ 1.5 mm:未自动检测', dot: '#9a8f7e' }, + { label: '压痕两侧均有连接面:未自动检测', dot: '#9a8f7e' }, + { label: '出血 ' + s.bleed + ' mm ' + (bleedOk ? '≥' : '<') + ' 材质建议 ' + m.bleed + ' mm(' + m.note + ')', dot: bleedOk ? '#3f9e5f' : '#d05a2a' } + ]; const compNote = s.tpl === 'rte' || s.tpl === 'ste' ? '当前:W+t · L+t · W+t · L+2t,高度 H+t' : s.tpl === 'mailer' ? '当前:楞厚 t=' + t + ',侧墙 H+t,盖宽 W+3t' : s.tpl === 'cyl' ? '当前:直径 D=L,筒身 24 段卷合,盖为 24 边形' : s.tpl === 'hex' ? '当前:边长 W,六棱筒,盖为正六边形' : '当前:盖/套筒按 +1mm/边 放量'; const editOn = s.editMode; const typeOvrCount = Object.keys(s.typeOvr).length; @@ -123,8 +130,8 @@ export function StructureView() {
校验 {checks.map(c => ( -
-
{c} +
+
{c.label}
))}
diff --git a/test/layers.test.js b/test/layers.test.js new file mode 100644 index 0000000..194b12a --- /dev/null +++ b/test/layers.test.js @@ -0,0 +1,33 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { dpiOf } from '../src/design/layers.js'; + +const imageLayer = overrides => ({ + kind: 'image', + imgW: 600, + imgH: 300, + pxw: 600, + w: 50.8, + h: 25.4, + ...overrides +}); + +test('dpiOf uses both full-image dimensions when no crop is set', () => { + assert.equal(dpiOf(imageLayer()), 300); +}); + +test('dpiOf returns the lower DPI after non-uniform scaling', () => { + assert.equal(dpiOf(imageLayer({ h: 50.8 })), 150); +}); + +test('dpiOf uses the cropped pixel dimensions on both axes', () => { + const cropped = { imgW: 4000, imgH: 2000, pxw: 4000, crop: [0.1, 0.2, 0.5, 0.25] }; + assert.equal(dpiOf(imageLayer({ ...cropped, w: 127, h: 25.4 })), 400); + assert.equal(dpiOf(imageLayer({ ...cropped, w: 101.6, h: 50.8 })), 250); +}); + +test('dpiOf returns zero for non-image layers or missing dimensions', () => { + assert.equal(dpiOf({ kind: 'text', pxw: 600, w: 50.8 }), 0); + assert.equal(dpiOf({ kind: 'image', pxw: 600, w: 50.8 }), 0); + assert.equal(dpiOf({ kind: 'image', pxw: 600, w: 0, h: 25.4 }), 0); +});