fix(studio): выделение папки раскрывает дерево + Delete удаляет всю папку с содержимым

1) При выделении папки (клик по сцене / вставка кита) дерево авто-раскрывается:
   workspace + цепочка родителей + scrollIntoView к строке папки. Раньше папка
   выделялась на сцене, но в свёрнутом дереве её не было видно.
2) Delete на выделенной папке (type='folder') → removeFolder(id, true) удаляет
   всю папку со ВСЕМ содержимым + _cleanupOrphanScripts чистит осиротевшие
   скрипты привязанные к удалённым объектам.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
min 2026-06-05 18:36:55 +03:00
parent 46414d874b
commit 2d669a3ff3
3 changed files with 50 additions and 1 deletions

View File

@ -376,6 +376,23 @@ const HierarchyPanel = ({
useEffect(() => {
if (!selection) return;
const t = selection.type;
// Выделена ПАПКА (клик по сцене / вставка кита из тулбокса) раскрыть
// «Сцену» и цепочку папок до неё, чтобы папка стала видна в дереве.
if (t === 'folder') {
setWorkspaceOpen(true);
setOpenFolders(prev => {
const n = new Set(prev);
let cur = selection.folderId;
const guard = new Set();
while (cur != null && !guard.has(cur)) {
guard.add(cur);
const f = folders.find(ff => ff.id === cur);
if (f && f.parentId != null) { n.add(f.parentId); cur = f.parentId; } else cur = null;
}
return n;
});
return;
}
// Находим объект и его folderId по выделению.
let obj = null, kind = null;
if (t === 'block') {
@ -486,10 +503,12 @@ const HierarchyPanel = ({
const subUserModels = userModelsByFolder.get(folder.id) || [];
const totalCount = subBlocks.length + subModels.length + subPrims.length + subUserModels.length + subFolders.length;
const folderSelected = selection?.type === 'folder' && selection?.folderId === folder.id;
return (
<div key={`folder-${folder.id}`} className={cl.folderWrap}>
<div
className={`${cl.folderHeader} ${selection?.type === 'folder' && selection?.folderId === folder.id ? cl.itemSelected : ''}`}
ref={folderSelected ? (el) => { if (el) el.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); } : null}
className={`${cl.folderHeader} ${folderSelected ? cl.itemSelected : ''}`}
style={{ paddingLeft: depth * 12 + 8 }}
onClick={() => onSelectFolder?.(folder.id)}
onContextMenu={(e) => handleContextMenu(e, { type: 'folder', ...folder })}

View File

@ -6604,6 +6604,25 @@ export class BabylonScene {
if (this._onSceneChange) this._onSceneChange();
}
/** Удалить скрипты, чей объект-носитель больше не существует (после удаления
* папки/объектов). Глобальные (target null/'game') не трогаем. */
_cleanupOrphanScripts() {
if (!Array.isArray(this._scripts)) return;
const exists = (t) => {
if (!t || t === 'game') return true;
if (t.kind === 'primitive') return !!this.primitiveManager?.instances?.has(t.id);
if (t.kind === 'model') return !!this.modelManager?.instances?.has(t.id);
if (t.kind === 'userModel') return !!this.userModelManager?.instances?.has(t.id);
return true; // block и пр. — не чистим
};
const before = this._scripts.length;
this._scripts = this._scripts.filter(s => exists(s.target));
if (this._scripts.length !== before) {
this.history?.markChange();
if (this._onSceneChange) this._onSceneChange();
}
}
/**
* Зарегистрировать колбэк для уведомлений об изменении режима Play
* (вызывается когда player сам инициирует exit, например по Esc).

View File

@ -724,6 +724,17 @@ export class SelectionManager {
} else if (this._selection.type === 'spawn') {
// Удаление точки спавна → игрок будет появляться в (0, высота, 0).
this._scene3d?.deleteSpawn?.();
} else if (this._selection.type === 'folder') {
// Папка целиком — удаляем со ВСЕМ содержимым (рекурсивно).
const fid = this._selection.folderId;
this.clear();
this._scene3d?.folderManager?.removeFolder?.(fid, true);
// Удаляем скрипты, привязанные к объектам этой папки? Они привязаны
// к примитивам, которые removeFolder удалит; скрипты на них чистятся
// через _onSceneChange / при сохранении. Дополнительно пусть движок
// подчистит «осиротевшие» скрипты.
this._scene3d?._cleanupOrphanScripts?.();
return;
}
this.clear();
}