From a73d0ae3d6a86782530352bdb671b7f37c3d414a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=94=B0=E9=9B=AA?=
<7761656+tian_xue@user.noreply.gitee.com>
Date: Thu, 20 Aug 2026 17:01:32 +0800
Subject: [PATCH 1/2] fix: report structural validation accurately
---
src/ui/StructureView.jsx | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
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 => (
-
From a0cd34b5cc94e32bd6a468b9ec12c85f11f598d4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=94=B0=E9=9B=AA?=
<7761656+tian_xue@user.noreply.gitee.com>
Date: Thu, 20 Aug 2026 17:01:32 +0800
Subject: [PATCH 2/2] fix: compute effective DPI after crop and scaling
---
package.json | 1 +
src/design/layers.js | 11 +++++++++--
test/layers.test.js | 33 +++++++++++++++++++++++++++++++++
3 files changed, 43 insertions(+), 2 deletions(-)
create mode 100644 test/layers.test.js
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/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);
+});