Skip to content

Commit 3c2cd27

Browse files
nieaoclaude
andcommitted
feat(layout): 新容器位置加遮挡检测, 自动避让 PDF/已有 group
dispatchChallenge / decomposeOntologyFurther 新建容器时撞已有节点 / PDF (用户图 41/42/43 反复反馈反驳遮挡 PDF, 排版自动崩)。 新增 findFreePosition helper: - 计算节点 absolute bbox (考虑 parentNode 嵌套) - 撞墙就向 down 偏移 80px, 最多 50 次 - 仅对**新建**容器生效, 复用已有容器不动 (尊重用户可能已手调) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8c85929 commit 3c2cd27

1 file changed

Lines changed: 74 additions & 0 deletions

File tree

src/stores/useCanvasStore.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,59 @@ const LAYOUT = {
8080
CATEGORY_SPACING: 600,
8181
}
8282

83+
// === 遮挡检测 ===
84+
// 计算节点的 absolute bbox (考虑 parentNode 嵌套)
85+
function getNodeAbsoluteBox(node, allNodes) {
86+
let x = node.position?.x || 0
87+
let y = node.position?.y || 0
88+
let parentId = node.parentNode
89+
while (parentId) {
90+
const parent = allNodes.find((n) => n.id === parentId)
91+
if (!parent) break
92+
x += parent.position?.x || 0
93+
y += parent.position?.y || 0
94+
parentId = parent.parentNode
95+
}
96+
// 节点尺寸: group 用 style.width/height, 普通节点估算
97+
const w = Number(node.style?.width) || node.width || node.measured?.width
98+
|| (node.type === 'group' ? 800 : node.type === 'challengeNode' ? 280 : node.type === 'ontologyNode' ? 220 : 200)
99+
const h = Number(node.style?.height) || node.height || node.measured?.height
100+
|| (node.type === 'group' ? 600 : node.type === 'challengeNode' ? 200 : 120)
101+
return { x, y, w, h }
102+
}
103+
104+
// 判断两 box 是否撞 (留 SAFE_GAP 间距)
105+
function boxOverlap(a, b, gap = 30) {
106+
return !(a.x + a.w + gap <= b.x || b.x + b.w + gap <= a.x || a.y + a.h + gap <= b.y || b.y + b.h + gap <= a.y)
107+
}
108+
109+
/**
110+
* 在已有节点旁边找一个不撞的位置. 优先按 strategy 偏移
111+
* @param {{x,y,w,h}} desired 期望的 box
112+
* @param {Array} allNodes 所有节点
113+
* @param {Array<string>} excludeIds 排除自身/同时新增的节点 ids
114+
* @param {'down'|'right'} strategy 撞了之后向哪个方向偏移
115+
* @returns {{x,y}} 不撞的目标位置
116+
*/
117+
function findFreePosition(desired, allNodes, excludeIds = [], strategy = 'down') {
118+
const STEP = strategy === 'down' ? 80 : 120
119+
const MAX_TRIES = 50
120+
let { x, y } = desired
121+
for (let i = 0; i < MAX_TRIES; i++) {
122+
const cur = { x, y, w: desired.w, h: desired.h }
123+
const collided = allNodes.find((n) => {
124+
if (excludeIds.includes(n.id)) return false
125+
if (n.parentNode) return false // 子节点的位置已经体现在父级 bbox 内, 跳过避免重复检测
126+
const box = getNodeAbsoluteBox(n, allNodes)
127+
return boxOverlap(cur, box)
128+
})
129+
if (!collided) return { x, y }
130+
if (strategy === 'down') y += STEP
131+
else x += STEP
132+
}
133+
return { x, y } // 兜底, 50 次还撞就只能这样
134+
}
135+
83136
// 通用节点工厂,确保创建一致性
84137
const createNodeFactory = (get, set) => (type, idPrefix, data, position = null) => {
85138
const pos = position || get().getNextGridPosition()
@@ -2266,6 +2319,17 @@ const useCanvasStore = create(
22662319
}
22672320

22682321
const existing = nodes.find((n) => n.id === challengeGroupId)
2322+
2323+
// 遮挡检测: 新建容器时避开已有节点 (PDF / 别的 challengeGroup / 其他 projectGroup)
2324+
// 仅对新建容器做; 复用已存在的不动 (用户可能已经手动调过位置)
2325+
if (!existing) {
2326+
const containerH = CHALLENGE_GROUP_PAD * 2 + Math.max(challenges.length, 3) * CHALLENGE_CARD_H
2327+
const free = findFreePosition(
2328+
{ x: challengeGroupPos.x, y: challengeGroupPos.y, w: CHALLENGE_GROUP_W, h: containerH },
2329+
nodes, [], 'down',
2330+
)
2331+
challengeGroupPos = free
2332+
}
22692333
// 已有反驳卡片数 (用于 append 时的 y 偏移)
22702334
const existingChallenges = nodes.filter((n) => n.parentNode === challengeGroupId && n.type === 'challengeNode')
22712335
const startIdx = existingChallenges.length
@@ -2404,6 +2468,16 @@ const useCanvasStore = create(
24042468
}
24052469

24062470
const existing = nodes.find((n) => n.id === dgId)
2471+
2472+
// 遮挡检测: 新建 decompose 容器时避开已有内容
2473+
if (!existing) {
2474+
const tentativeW = Math.max(COL_W * 5 + PAD * 2, COL_W * subitems.length + PAD * 2)
2475+
const free = findFreePosition(
2476+
{ x: dgPos.x, y: dgPos.y, w: tentativeW, h: CARD_H + PAD * 2 + 24 },
2477+
nodes, [], 'down',
2478+
)
2479+
dgPos = free
2480+
}
24072481
const existingChildren = nodes.filter((n) => n.parentNode === dgId && n.type === 'ontologyNode')
24082482
const startIdx = existingChildren.length
24092483
const totalAfter = startIdx + subitems.length

0 commit comments

Comments
 (0)