fix(hierarchy): scroll-to-selected раскрывает workspace+группы+папки

Бага: при выделении объекта в 3D-сцене иерархия не скроллила к нему
потому что:
  - workspaceOpen=false скрывал всю секцию Сцены
  - rootPrimsOpen/rootBlocksOpen/rootModelsOpen=false скрывали корневые
    группы
  - openFolders=пустой Set скрывал все папки с импортированной геометрией

Фикс: effect раскрывает все секции содержащие выбранный объект перед
скроллом. После раскрытия использует rAF×2 чтобы дать React дорендерить
ItemRow'ы, потом scrollIntoView.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
min 2026-06-08 02:21:29 +03:00
parent cc6447b851
commit 513c9ce26f

View File

@ -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 для безопасности с двоеточием и запятыми
// 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;
});
}
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: 'nearest', behavior: 'smooth' });
el.scrollIntoView({ block: 'center', behavior: 'smooth' });
}
}, [selection?.type, selection?.id, selection?.instanceId, selection?.gridX, selection?.gridY, selection?.gridZ]);
};
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 || '' });