fix: 修复 3D NaN 崩溃/样品图不刷新/画布点选失效
- replayShape 段过滤补 Y 轴有限值校验(此前仅校验 X) - inferSashGlyphs bdRect 增加有限值守卫 - WindoorThreed 逐网格有限值防护 + maxDim 守卫, 杜绝 NaN 网格 - CombineCanvas shapePts 过滤非有限点, 修复包围盒污染导致的点选失效 - 组合画布引入 rev 版本号: 原地变异几何后强制 3D 预览重建 (修复尺寸编辑后样品图不变化)
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
# CHANGELOG
|
# 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: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: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)
|
- [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)
|
||||||
|
|||||||
@@ -47,10 +47,12 @@ function shapePts(shape: any): Pt[] {
|
|||||||
const out: Pt[] = []
|
const out: Pt[] = []
|
||||||
for (const r of (shape.polygon?.polygon ?? []))
|
for (const r of (shape.polygon?.polygon ?? []))
|
||||||
for (const seg of r) {
|
for (const seg of r) {
|
||||||
if (seg.ps && Number.isFinite(seg.ps.x)) out.push(seg.ps)
|
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)) out.push(seg.pe)
|
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 && Number.isFinite(sp.line.pt.x) && Number.isFinite(sp.line.pt.y)) out.push(sp.line.pt)
|
||||||
}
|
}
|
||||||
for (const sp of (shape.mm?.sp ?? [])) if (sp.line?.pt) out.push(sp.line.pt)
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
function translateUid(uid: number, dx: number, dy: number) {
|
function translateUid(uid: number, dx: number, dy: number) {
|
||||||
|
|||||||
@@ -79,13 +79,17 @@ function build(drawing: DrawingData) {
|
|||||||
const pts: Pt[] = []
|
const pts: Pt[] = []
|
||||||
for (const s of drawing.sm) {
|
for (const s of drawing.sm) {
|
||||||
for (const r of replayShape(s).regions) {
|
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 }
|
if (!pts.length) { info.value = '无几何数据'; hasOpenable.value = false; return }
|
||||||
const gb = bboxOf(pts)
|
const gb = bboxOf(pts)
|
||||||
const gw = gb.x1 - gb.x0, gh = gb.y1 - gb.y0
|
const gw = gb.x1 - gb.x0, gh = gb.y1 - gb.y0
|
||||||
const maxDim = Math.max(gw, gh)
|
const maxDim = Math.max(gw, gh)
|
||||||
|
if (!Number.isFinite(maxDim) || maxDim <= 0) { info.value = '无有效几何'; hasOpenable.value = false; return }
|
||||||
const S = 1 / maxDim
|
const S = 1 / maxDim
|
||||||
const cx = (gb.x0 + gb.x1) / 2, cy = (gb.y0 + gb.y1) / 2
|
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)
|
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 w = (bb.x1 - bb.x0) * S, h = (bb.y1 - bb.y0) * S
|
||||||
const mx = ((bb.x0 + bb.x1) / 2 - cx) * S
|
const mx = ((bb.x0 + bb.x1) / 2 - cx) * S
|
||||||
const my = ((bb.y0 + bb.y1) / 2 - cy) * 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 inSash = sashRects.some(r => {
|
||||||
const cxr = (r.x0 + r.x1) / 2, cyr = (r.y0 + r.y1) / 2
|
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
|
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) => {
|
sashes.forEach((s, k) => {
|
||||||
const w = (s.x1 - s.x0) * S, h = (s.y1 - s.y0) * S
|
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 cxr = ((s.x0 + s.x1) / 2 - cx) * S
|
||||||
const cyr = ((s.y0 + s.y1) / 2 - cy) * 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 group = new THREE.Group()
|
||||||
const panel = new THREE.Group()
|
const panel = new THREE.Group()
|
||||||
const glass = new THREE.Mesh(new THREE.BoxGeometry(w, h, 0.006), gm)
|
const glass = new THREE.Mesh(new THREE.BoxGeometry(w, h, 0.006), gm)
|
||||||
|
|||||||
@@ -143,7 +143,9 @@ export function replayShape(shape: FrameShape): {
|
|||||||
} {
|
} {
|
||||||
const base = (shape.polygon?.polygon ?? [])
|
const base = (shape.polygon?.polygon ?? [])
|
||||||
.map(r => r
|
.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 } })))
|
.map(s => ({ ps: { ...s.ps }, pe: { ...s.pe } })))
|
||||||
.filter(r => r.length >= 3)
|
.filter(r => r.length >= 3)
|
||||||
const regions: Segment[][] = base.map(r => r)
|
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 regionBBs = regions.map(r => bboxOf(regionPoints(r)))
|
||||||
const glyphs: SashGlyph[] = []
|
const glyphs: SashGlyph[] = []
|
||||||
const bdRect = (bd: NonNullable<SashShape['bd']>) => {
|
const bdRect = (bd: NonNullable<SashShape['bd']>) => {
|
||||||
|
if (!bd.from || !bd.to) return null
|
||||||
const r = {
|
const r = {
|
||||||
x0: Math.min(bd.from.x, bd.to.x), y0: Math.min(bd.from.y, bd.to.y),
|
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),
|
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 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) => {
|
const pushGlyphs = (r: { x0: number; y0: number; x1: number; y1: number }, entries: any[], sub: SashShape) => {
|
||||||
|
|||||||
@@ -109,10 +109,14 @@ const saveName = ref('')
|
|||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const canvasRef = ref()
|
const canvasRef = ref()
|
||||||
let uidSeq = 1
|
let uidSeq = 1
|
||||||
|
const rev = ref(0)
|
||||||
|
|
||||||
const combinedJson = computed(() =>
|
const combinedJson = computed(() => {
|
||||||
shapes.value.length ? JSON.stringify({ s: { cs: 60, cjs: 100 }, cp: [], sm: shapes.value,
|
void rev.value // 画布原地变异几何后通过版本号强制重算(驱动 3D 预览重建)
|
||||||
epi: [], sbf: [], wlc: '', wall: [], scale: 0, note: [] }) : '')
|
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() {
|
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) }
|
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() {
|
function onSelResize() {
|
||||||
if (selSize.w >= 300 && selSize.h >= 300) canvasRef.value?.resizeSelected(selSize.w, selSize.h)
|
if (selSize.w >= 300 && selSize.h >= 300) canvasRef.value?.resizeSelected(selSize.w, selSize.h)
|
||||||
|
|||||||
Reference in New Issue
Block a user