diff --git a/CHANGELOG b/CHANGELOG index 3f0a131..533c58b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # CHANGELOG +- [2026-09-21 11:00] FIX: v1.7 修复 3D NaN 崩溃/样品图不刷新/画布点选失效——①THREE NaN: drawing_data 畸形数据进入 3D 网格, replayShape 段过滤补 Y 轴有限值校验、inferSashGlyphs bdRect 有限值校验、WindoorThreed 逐网格有限值防护+maxDim 守卫、CombineCanvas shapePts 过滤; ②样品图不刷新: 组合画布原地变异 shapes 数组, combinedJson computed 感知不到深层变化 → 引入 rev 版本号强制重算驱动 3D 重建; ③点选失效: 单元包围盒被非有限值污染致命中测试失效, 随①修复 (Files: frontend/src/types/drawing.ts, frontend/src/components/WindoorThreed.vue, frontend/src/components/CombineCanvas.vue, frontend/src/views/CombineView.vue) - [2026-09-21 10:50] FIX: 修复 spa_fallback 的 NameError(JSONResponse 未导入导致未匹配 /api 路径 500)与 JWT 密钥长度警告(默认密钥补足 32 字节以上) (Files: src/huamenchuang/main.py, src/huamenchuang/core/config.py) - [2026-09-21 10:55] FEAT: v1.6 组合画布编辑能力增强——①选中单元可视化: 蓝色虚线框+四角手柄+实时尺寸标注(W×H mm); ②精确尺寸编辑: 工具栏 W/H 输入框(选中单元几何按比例缩放, 锚定左下角, 中梃点位同步); ③方向键微调: 1mm/Shift 10mm; ④右侧单元清单点击选中联动画布; selSize 双向同步(画布拖拽/输入框改动均实时刷新) (Files: frontend/src/components/CombineCanvas.vue, frontend/src/views/CombineView.vue) - [2026-09-21 10:30] FEAT: v1.5 3D 门窗开合动画——①扇推断逻辑抽取为共享模块 inferSashGlyphs(types/drawing.ts), 2D 符号与 3D 动画共用; ②WindoorThreed 重写: 每个扇构建独立网格组(玻璃+扇框), 平开扇绕铰链旋转(od 定铰链侧, 门75°/平开32°/双向门40°), 推拉扇沿轨道平移(双扇前后轨交替), 区域归属扇的不再重复渲染静态玻璃; ③开合控制条: 开合度滑杆+自动演示(正弦摆动)+全开/全关, 平滑插值动画; 组合画布 3D 预览自动获得开合能力 (Files: frontend/src/types/drawing.ts, frontend/src/components/WindoorThreed.vue, frontend/src/components/WindoorCanvas.vue) diff --git a/frontend/src/components/CombineCanvas.vue b/frontend/src/components/CombineCanvas.vue index 6eb7ee2..95092e1 100644 --- a/frontend/src/components/CombineCanvas.vue +++ b/frontend/src/components/CombineCanvas.vue @@ -47,10 +47,12 @@ function shapePts(shape: any): Pt[] { const out: Pt[] = [] for (const r of (shape.polygon?.polygon ?? [])) for (const seg of r) { - if (seg.ps && Number.isFinite(seg.ps.x)) out.push(seg.ps) - if (seg.pe && Number.isFinite(seg.pe.x)) out.push(seg.pe) + if (seg.ps && Number.isFinite(seg.ps.x) && Number.isFinite(seg.ps.y)) out.push(seg.ps) + if (seg.pe && Number.isFinite(seg.pe.x) && Number.isFinite(seg.pe.y)) out.push(seg.pe) } - for (const sp of (shape.mm?.sp ?? [])) if (sp.line?.pt) out.push(sp.line.pt) + for (const sp of (shape.mm?.sp ?? [])) { + if (sp.line?.pt && Number.isFinite(sp.line.pt.x) && Number.isFinite(sp.line.pt.y)) out.push(sp.line.pt) + } return out } function translateUid(uid: number, dx: number, dy: number) { diff --git a/frontend/src/components/WindoorThreed.vue b/frontend/src/components/WindoorThreed.vue index fd7fab7..0ec0de5 100644 --- a/frontend/src/components/WindoorThreed.vue +++ b/frontend/src/components/WindoorThreed.vue @@ -79,13 +79,17 @@ function build(drawing: DrawingData) { const pts: Pt[] = [] for (const s of drawing.sm) { for (const r of replayShape(s).regions) { - for (const seg of r) { pts.push(seg.ps); pts.push(seg.pe) } + for (const seg of r) { + if (Number.isFinite(seg.ps?.x) && Number.isFinite(seg.ps?.y)) pts.push(seg.ps) + if (Number.isFinite(seg.pe?.x) && Number.isFinite(seg.pe?.y)) pts.push(seg.pe) + } } } if (!pts.length) { info.value = '无几何数据'; hasOpenable.value = false; return } const gb = bboxOf(pts) const gw = gb.x1 - gb.x0, gh = gb.y1 - gb.y0 const maxDim = Math.max(gw, gh) + if (!Number.isFinite(maxDim) || maxDim <= 0) { info.value = '无有效几何'; hasOpenable.value = false; return } const S = 1 / maxDim const cx = (gb.x0 + gb.x1) / 2, cy = (gb.y0 + gb.y1) / 2 const DEPTH = Math.max(0.04, (drawing.s?.cs ?? 60) * S * 1.2) @@ -132,6 +136,7 @@ function build(drawing: DrawingData) { const w = (bb.x1 - bb.x0) * S, h = (bb.y1 - bb.y0) * S const mx = ((bb.x0 + bb.x1) / 2 - cx) * S const my = ((bb.y0 + bb.y1) / 2 - cy) * S + if (![w, h, mx, my].every(Number.isFinite) || w <= 0 || h <= 0) continue const inSash = sashRects.some(r => { const cxr = (r.x0 + r.x1) / 2, cyr = (r.y0 + r.y1) / 2 return Math.abs(cxr - (bb.x0 + bb.x1) / 2) < w / 2 && Math.abs(cyr - (bb.y0 + bb.y1) / 2) < h / 2 @@ -145,9 +150,9 @@ function build(drawing: DrawingData) { // 扇面板(可开合):玻璃 + 四周扇框,装入铰链/滑动组 sashes.forEach((s, k) => { const w = (s.x1 - s.x0) * S, h = (s.y1 - s.y0) * S - if (w < 0.02 || h < 0.02) return const cxr = ((s.x0 + s.x1) / 2 - cx) * S const cyr = ((s.y0 + s.y1) / 2 - cy) * S + if (![w, h, cxr, cyr].every(Number.isFinite) || w < 0.02 || h < 0.02) return const group = new THREE.Group() const panel = new THREE.Group() const glass = new THREE.Mesh(new THREE.BoxGeometry(w, h, 0.006), gm) diff --git a/frontend/src/types/drawing.ts b/frontend/src/types/drawing.ts index 5f0a564..f747c77 100644 --- a/frontend/src/types/drawing.ts +++ b/frontend/src/types/drawing.ts @@ -143,7 +143,9 @@ export function replayShape(shape: FrameShape): { } { const base = (shape.polygon?.polygon ?? []) .map(r => r - .filter(s => s && typeof s === 'object' && s.ps && s.pe && Number.isFinite(s.ps.x) && Number.isFinite(s.pe.x)) + .filter(s => s && typeof s === 'object' && s.ps && s.pe + && Number.isFinite(s.ps.x) && Number.isFinite(s.ps.y) + && Number.isFinite(s.pe.x) && Number.isFinite(s.pe.y)) .map(s => ({ ps: { ...s.ps }, pe: { ...s.pe } }))) .filter(r => r.length >= 3) const regions: Segment[][] = base.map(r => r) @@ -237,11 +239,13 @@ export function inferSashGlyphs(shape: FrameShape, replayed?: Segment[][]): { const regionBBs = regions.map(r => bboxOf(regionPoints(r))) const glyphs: SashGlyph[] = [] const bdRect = (bd: NonNullable) => { + if (!bd.from || !bd.to) return null const r = { x0: Math.min(bd.from.x, bd.to.x), y0: Math.min(bd.from.y, bd.to.y), x1: Math.max(bd.from.x, bd.to.x), y1: Math.max(bd.from.y, bd.to.y), } - return !bd.hidden && r.x1 - r.x0 > 10 && r.y1 - r.y0 > 10 ? r : null + const ok = [r.x0, r.y0, r.x1, r.y1].every(v => Number.isFinite(v)) + return ok && !bd.hidden && r.x1 - r.x0 > 10 && r.y1 - r.y0 > 10 ? r : null } const kindOf = (sub: SashShape) => (sub.type === 'DoubleSash' ? 'sliding' : 'casement') as 'sliding' | 'casement' const pushGlyphs = (r: { x0: number; y0: number; x1: number; y1: number }, entries: any[], sub: SashShape) => { diff --git a/frontend/src/views/CombineView.vue b/frontend/src/views/CombineView.vue index 4619848..d10f9af 100644 --- a/frontend/src/views/CombineView.vue +++ b/frontend/src/views/CombineView.vue @@ -109,10 +109,14 @@ const saveName = ref('') const saving = ref(false) const canvasRef = ref() let uidSeq = 1 +const rev = ref(0) -const combinedJson = computed(() => - shapes.value.length ? JSON.stringify({ s: { cs: 60, cjs: 100 }, cp: [], sm: shapes.value, - epi: [], sbf: [], wlc: '', wall: [], scale: 0, note: [] }) : '') +const combinedJson = computed(() => { + void rev.value // 画布原地变异几何后通过版本号强制重算(驱动 3D 预览重建) + if (!shapes.value.length) return '' + return JSON.stringify({ s: { cs: 60, cjs: 100 }, cp: [], sm: shapes.value, + epi: [], sbf: [], wlc: '', wall: [], scale: 0, note: [], rev: rev.value }) +}) // ---------- 样板库 ---------- async function loadLib() { @@ -198,7 +202,7 @@ function syncSelSize() { if (bb) { selSize.w = Math.round(bb.x1 - bb.x0); selSize.h = Math.round(bb.y1 - bb.y0) } } -function onCanvasChange() { refreshMeta(); syncSelSize() } +function onCanvasChange() { refreshMeta(); syncSelSize(); rev.value++ } function onSelResize() { if (selSize.w >= 300 && selSize.h >= 300) canvasRef.value?.resizeSelected(selSize.w, selSize.h)