From 513c9ce26f899590f5da10e8fa4078f59ff37e5f Mon Sep 17 00:00:00 2001 From: min Date: Mon, 8 Jun 2026 02:21:29 +0300 Subject: [PATCH] =?UTF-8?q?fix(hierarchy):=20scroll-to-selected=20=D1=80?= =?UTF-8?q?=D0=B0=D1=81=D0=BA=D1=80=D1=8B=D0=B2=D0=B0=D0=B5=D1=82=20worksp?= =?UTF-8?q?ace+=D0=B3=D1=80=D1=83=D0=BF=D0=BF=D1=8B+=D0=BF=D0=B0=D0=BF?= =?UTF-8?q?=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Бага: при выделении объекта в 3D-сцене иерархия не скроллила к нему потому что: - workspaceOpen=false скрывал всю секцию Сцены - rootPrimsOpen/rootBlocksOpen/rootModelsOpen=false скрывали корневые группы - openFolders=пустой Set скрывал все папки с импортированной геометрией Фикс: effect раскрывает все секции содержащие выбранный объект перед скроллом. После раскрытия использует rAF×2 чтобы дать React дорендерить ItemRow'ы, потом scrollIntoView. Co-Authored-By: Claude Opus 4.7 --- src/editor/HierarchyPanel.jsx | 45 ++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/src/editor/HierarchyPanel.jsx b/src/editor/HierarchyPanel.jsx index 946b662..af4da1f 100644 --- a/src/editor/HierarchyPanel.jsx +++ b/src/editor/HierarchyPanel.jsx @@ -269,24 +269,51 @@ const HierarchyPanel = ({ const [renaming, setRenaming] = useState(null); // Авто-скролл к выбранному элементу: когда юзер выделяет объект в 3D-сцене - // (или приходит выделение извне) — прокручиваем иерархию к нему. - // Работает по data-sel-id который ItemRow получает через style (см. ниже). + // (или приходит выделение извне) — раскрываем родительские папки и + // прокручиваем иерархию к нему. const hierarchyRootRef = useRef(null); useEffect(() => { if (!selection) return; - const root = hierarchyRootRef.current; - if (!root) return; let selId = null; if (selection.type === 'primitive') selId = `primitive:${selection.id}`; else if (selection.type === 'model') selId = `model:${selection.instanceId}`; else if (selection.type === 'block') selId = `block:${selection.gridX},${selection.gridY},${selection.gridZ}`; if (!selId) return; - // querySelector через CSS.escape для безопасности с двоеточием и запятыми - const el = root.querySelector(`[data-sel-id="${CSS.escape(selId)}"]`); - if (el && typeof el.scrollIntoView === 'function') { - el.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + + // 1) Раскрываем ВСЁ что может скрывать выбранный объект: + // - все папки (folders) - вдруг примитив сидит в Folder + // - rootPrimsOpen - группа "Примитивы" в корне + // - sceneOpen — корень сцены + if (folders && folders.length > 0) { + setOpenFolders(prev => { + const next = new Set(prev); + let changed = false; + for (const f of folders) { + if (!next.has(f.id)) { next.add(f.id); changed = true; } + } + return changed ? next : prev; + }); } - }, [selection?.type, selection?.id, selection?.instanceId, selection?.gridX, selection?.gridY, selection?.gridZ]); + if (!workspaceOpen) setWorkspaceOpen(true); + if (selection.type === 'primitive' && !rootPrimsOpen) setRootPrimsOpen(true); + if (selection.type === 'block' && !rootBlocksOpen) setRootBlocksOpen(true); + if (selection.type === 'model' && !rootModelsOpen) setRootModelsOpen(true); + + // 2) Скролл через 2 кадра (даём React перерендерить после раскрытия) + const tick = () => { + const root = hierarchyRootRef.current; + if (!root) return; + const el = root.querySelector(`[data-sel-id="${CSS.escape(selId)}"]`); + if (el && typeof el.scrollIntoView === 'function') { + el.scrollIntoView({ block: 'center', behavior: 'smooth' }); + } + }; + const raf1 = requestAnimationFrame(() => { + const raf2 = requestAnimationFrame(tick); + return () => cancelAnimationFrame(raf2); + }); + return () => cancelAnimationFrame(raf1); + }, [selection?.type, selection?.id, selection?.instanceId, selection?.gridX, selection?.gridY, selection?.gridZ, folders?.length, workspaceOpen, rootPrimsOpen, rootBlocksOpen, rootModelsOpen, openFolders]); const startRename = (kind, refKey, currentValue) => { setRenaming({ kind, refKey, value: currentValue || '' });