From 2cee9febe55618a566bd05ee8b4ae87a6e0f4da4 Mon Sep 17 00:00:00 2001 From: HHY Date: Mon, 21 Sep 2026 10:05:44 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20v2.4=20=E7=BB=84=E5=90=88=E7=94=BB?= =?UTF-8?q?=E5=B8=83=E5=9B=9E=E5=BD=92=E8=87=AA=E7=94=B1=E6=8B=96=E6=8B=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除拖动松手后的强制自动排列(恢复自由摆放能力) - 吸附升级为标准六边缘对齐: 拖动单元左/中/右×其余单元左/中/右 (上/中/下同理), 取最近者吸附并绘制辅助线 - 允许自由重叠摆放(用户自主决定布局) - 自动排列保留为可选整理按钮 - 参考 Konva/Fabric 吸附标准模式实现 --- CHANGELOG | 1 + frontend/src/components/CombineCanvas.vue | 46 ++++++++++------------- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index cb49c66..c404cff 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # CHANGELOG +- [2026-09-21 13:05] FIX: v2.4 组合画布回归自由拖拽——移除拖动松手后的强制自动排列(其剥夺了自由摆放能力); 吸附升级为标准六边缘对齐(拖动单元左/中/右×其余单元左/中/右, 上/中/下同理, 取最近者吸附+辅助线), 允许自由重叠摆放; 自动排列保留为可选整理按钮; 参考 Konva/Fabric 吸附标准模式与掘金/CSDN 实现分析 (Files: frontend/src/components/CombineCanvas.vue, frontend/src/views/CombineView.vue) - [2026-09-21 12:35] FIX: v2.3 组合排列 3D 重叠修复——①CombineCanvas 新增 autoArrange: 全部单元按 X 序紧贴排布+底边对齐; ②拖动松手重叠检测: 移动单元与其他单元相交时自动重新排列(消除 3D 玻璃面穿插/z-fighting); ③CombineView 工具栏新增自动排列按钮; ④组合画布移除孤儿 @drop(归父容器) (Files: frontend/src/components/CombineCanvas.vue, frontend/src/views/CombineView.vue) - [2026-09-21 12:40] FIX: v2.2 前端完整性治理——引入 vue-tsc 全量类型检查并清零全部错误: ①组合画布 3D 扇面板解构键名错误(rect/s→rect/sash)致扇面板全挂(3D 混乱根因之一); ②WindoorCanvas isDoor 标注 cx 作用域泄漏; ③WindoorThreed rebuild 函数缺失(墙体开关点击报错)+wallBoxes 元组类型; ④量尺页 openCreate 未定义(点击报错); ⑤状态筛选 ref 类型纳入空串(el-select clearable 语义); ⑥CombineCanvas 移除孤儿 @drop(归父容器)+容器补 dragover.prevent(修复拖放入库); ⑦AIRenderCanvas style 属性名保留处理+表单联合类型; ⑧零散隐式 any 参数; 新增检查脚本 scripts/check-frontend.cjs(未定义引用+API 路径一致性比对) (Files: frontend/src/**, frontend/scripts/check-frontend.cjs) - [2026-09-21 12:25] FIX: v2.2 修复 3D 截图为纯色空白——墙体尺寸单位错误(bw=1/S*0.72 误用模型单位≈2304, 而放置坐标是世界单位±0.5), 相机被包在数千单位宽的巨墙内只见背景色; 改为世界单位(侧墙 0.55, 上下墙覆盖窗洞±2.2), 墙体不投影(避免自身遮挡), Node 验证四墙尺寸全部 ≤2.2 世界单位 (Files: frontend/src/components/WindoorThreed.vue) diff --git a/frontend/src/components/CombineCanvas.vue b/frontend/src/components/CombineCanvas.vue index af21c70..ed78e7c 100644 --- a/frontend/src/components/CombineCanvas.vue +++ b/frontend/src/components/CombineCanvas.vue @@ -133,19 +133,30 @@ function mirrorUnit(uid: number) { } // 吸附: 单元边缘贴近其他单元边缘时对齐(返回修正后的位置与辅助线) +/** 六边缘对齐吸附(标准画布编辑器模式): 拖动单元的左/中/右、上/中/下与其余单元对应边比对, 取最近者吸附 */ function snap(bb: BBox, excludeUid: number): { dx: number; dy: number; guides: { v: number[]; h: number[] } } { - const TOL = 12 const others = uids().filter(u => u !== excludeUid) .map(u => unitBBox(u)).filter(b => b) as BBox[] const gv: number[] = [], gh: number[] = [] let dx = 0, dy = 0 + const myX = [bb.x0, (bb.x0 + bb.x1) / 2, bb.x1] + const myY = [bb.y0, (bb.y0 + bb.y1) / 2, bb.y1] + let bestX: { d: number; from: number; to: number } | null = null + let bestY: { d: number; from: number; to: number } | null = null for (const o of others) { - // 左缘贴对方右缘(无缝拼接) - if (Math.abs(bb.x0 - o.x1) < TOL) { dx = o.x1 - bb.x0; gv.push(o.x1) } - if (Math.abs(bb.x1 - o.x0) < TOL) { dx = o.x0 - bb.x1; gv.push(o.x0) } - if (Math.abs(bb.y0 - o.y0) < TOL) { dy = o.y0 - bb.y0; gh.push(o.y0) } - if (Math.abs(bb.y1 - o.y1) < TOL) { dy = o.y1 - bb.y1; gh.push(o.y1) } + const oX = [o.x0, (o.x0 + o.x1) / 2, o.x1] + const oY = [o.y0, (o.y0 + o.y1) / 2, o.y1] + for (const a of myX) for (const b of oX) { + const d = Math.abs(a - b) + if (d < 12 && (!bestX || d < bestX.d)) bestX = { d, from: a, to: b } + } + for (const a of myY) for (const b of oY) { + const d = Math.abs(a - b) + if (d < 12 && (!bestY || d < bestY.d)) bestY = { d, from: a, to: b } + } } + if (bestX) { dx = bestX.to - bestX.from; gv.push(bestX.to) } + if (bestY) { dy = bestY.to - bestY.from; gh.push(bestY.to) } return { dx, dy, guides: { v: gv, h: gh } } } @@ -305,16 +316,11 @@ function onMove(e: MouseEvent) { function onUp() { if (dragUnit) { - const uid = dragUnit.uid const moved = dragUnit.moved guides = { v: [], h: [] } dragUnit = null - if (moved && uid !== null && unitOverlaps(uid)) { - // 拖动后与其他单元重叠 → 自动重新排列消除重叠 - autoArrange() - return - } render() + if (moved) emit('change') return } pan = null @@ -368,21 +374,7 @@ function nudgeSelected(dx: number, dy: number) { emit('change') } -function rectsIntersect(a: BBox, b: BBox): boolean { - return a.x0 < b.x1 - 1 && b.x0 < a.x1 - 1 && a.y0 < b.y1 - 1 && b.y0 < a.y1 - 1 -} - -/** 单元是否与其它单元重叠 */ -function unitOverlaps(uid: number): boolean { - const bb = unitBBox(uid) - if (!bb) return false - return uids().filter(u => u !== uid).some(u => { - const o = unitBBox(u) - return o ? rectsIntersect(bb, o) : false - }) -} - -/** 自动排列: 全部单元按 X 序紧贴排布、底边对齐(消除重叠) */ +/** 自动排列: 全部单元按 X 序紧贴排布、底边对齐(可选的整理工具,不强制) */ function autoArrange() { const ids = uids().filter(u => unitBBox(u)).sort((a, b) => (unitBBox(a)!.x0) - (unitBBox(b)!.x0))