fix(studio): клик по части папки выделяет всю папку + free-drag папки + удаление обновляет дерево + затухание длинной тени

1) Длинная тень-полоса: csm.frustumEdgeFalloff=8 — тень персонажа больше не
   тянется на весь пол.
2) Удаление примитива/модели/блока через ПКМ теперь обновляет дерево
   (markDirty + hierarchyDirtyRef) — раньше объект удалялся, но не пропадал.
3) Клик по СЦЕНЕ по объекту в папке → выделяется ВСЯ папка (selectByMesh
   проверяет folderId). Отдельную часть — через раскрытие папки в дереве.
4) Free-drag папки: зажал ЛКМ на группе и тянешь — двигается вся папка
   (moveFolderBy по дельте центра).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
min 2026-06-05 18:19:02 +03:00
parent ed7310a532
commit bf93219266
3 changed files with 50 additions and 2 deletions

View File

@ -3351,14 +3351,20 @@ const KubikonEditor = () => {
onDeleteBlock={(x, y, z) => { onDeleteBlock={(x, y, z) => {
sceneRef.current?.blockManager?.removeBlock(x, y, z); sceneRef.current?.blockManager?.removeBlock(x, y, z);
sceneRef.current?.clearSelection(); sceneRef.current?.clearSelection();
markDirty();
hierarchyDirtyRef.current = true;
}} }}
onDeleteModel={(id) => { onDeleteModel={(id) => {
sceneRef.current?.modelManager?.removeInstance(id); sceneRef.current?.modelManager?.removeInstance(id);
sceneRef.current?.clearSelection(); sceneRef.current?.clearSelection();
markDirty();
hierarchyDirtyRef.current = true;
}} }}
onDeletePrimitive={(id) => { onDeletePrimitive={(id) => {
sceneRef.current?.primitiveManager?.removeInstance(id); sceneRef.current?.primitiveManager?.removeInstance(id);
sceneRef.current?.clearSelection(); sceneRef.current?.clearSelection();
markDirty();
hierarchyDirtyRef.current = true;
}} }}
onFocusSelection={() => sceneRef.current?.focusOnSelection()} onFocusSelection={() => sceneRef.current?.focusOnSelection()}
onCreateFolder={(name, parentId) => onCreateFolder={(name, parentId) =>

View File

@ -1723,6 +1723,9 @@ export class BabylonScene {
: ShadowGenerator.QUALITY_MEDIUM; : ShadowGenerator.QUALITY_MEDIUM;
csm.darkness = 0.4; csm.darkness = 0.4;
csm.autoCalcDepthBounds = true; csm.autoCalcDepthBounds = true;
// Плавное затухание тени у края каскада — убирает «полосу-хвост»
// тени персонажа на весь пол при движении (баг 2026-06-05).
csm.frustumEdgeFalloff = 8;
this._shadowGenerator = csm; this._shadowGenerator = csm;
} else { } else {
// Обычный ShadowGenerator. Поднял разрешение для soft до 2048. // Обычный ShadowGenerator. Поднял разрешение для soft до 2048.
@ -5679,6 +5682,14 @@ export class BabylonScene {
this.selection?.selectByMesh(m); this.selection?.selectByMesh(m);
const sel = this.selection?.getSelection(); const sel = this.selection?.getSelection();
if (!sel || sel.type === 'block' || sel.type === 'spawn') return false; if (!sel || sel.type === 'block' || sel.type === 'spawn') return false;
// Папка — тащим всю группу через folderManager (по дельте центра).
if (sel.type === 'folder') {
const g = this.folderManager?.getFolderObjects(sel.folderId);
this._freeDragCandidate = { folder: true, folderId: sel.folderId, last: { ...(g?.center || { x: 0, y: 0, z: 0 }) } };
this._freeDragHalf = { x: 0.5, y: 0.5, z: 0.5 };
this._freeDragActive = false;
return true;
}
const root = this._getSelectionRoot(sel); const root = this._getSelectionRoot(sel);
if (!root) return false; if (!root) return false;
this._freeDragCandidate = { root }; this._freeDragCandidate = { root };
@ -5693,6 +5704,24 @@ export class BabylonScene {
if (!cand) return; if (!cand) return;
const sel = this.selection?.getSelection(); const sel = this.selection?.getSelection();
if (!sel) return; if (!sel) return;
// Папка: тащим всю группу по дельте от последнего центра (проекция курсора
// на горизонтальную плоскость по высоте центра группы).
if (cand.folder) {
const ray = this.scene.createPickingRay(this.scene.pointerX, this.scene.pointerY, null, this.scene.activeCamera);
if (Math.abs(ray.direction.y) < 1e-4) return;
const t = (cand.last.y - ray.origin.y) / ray.direction.y;
if (t < 0) return;
const px = ray.origin.x + ray.direction.x * t;
const pz = ray.origin.z + ray.direction.z * t;
const dx = px - cand.last.x, dz = pz - cand.last.z;
if (dx || dz) {
this.folderManager?.moveFolderBy(cand.folderId, dx, 0, dz);
cand.last.x = px; cand.last.z = pz;
}
return;
}
const root = cand.root; const root = cand.root;
const half = this._freeDragHalf || { x: 0.5, y: 0.5, z: 0.5 }; const half = this._freeDragHalf || { x: 0.5, y: 0.5, z: 0.5 };

View File

@ -76,24 +76,37 @@ export class SelectionManager {
selectByMesh(mesh) { selectByMesh(mesh) {
if (!mesh) return this.clear(); if (!mesh) return this.clear();
const m = mesh.metadata; const m = mesh.metadata;
// Если объект лежит в папке — клик по СЦЕНЕ выделяет ВСЮ папку целиком
// (отдельную часть можно выбрать через дерево). folderId берём из data.
const folderIdOf = (kind, id) => {
let d = null;
if (kind === 'model') d = this.modelManager?.instances?.get(id);
else if (kind === 'userModel') d = this.userModelManager?.instances?.get(id);
else if (kind === 'primitive') d = this.primitiveManager?.instances?.get(id);
return d ? (d.folderId ?? null) : null;
};
if (m?.isBlock) { if (m?.isBlock) {
return this.selectBlockAt(m.gridX, m.gridY, m.gridZ); return this.selectBlockAt(m.gridX, m.gridY, m.gridZ);
} }
if (m?.isModel) { if (m?.isModel) {
// Заблокированный объект (Фаза 5.11) не выделяется кликом по
// сцене — только через иерархию (чтобы можно было снять lock).
const md = this.modelManager?.instances?.get(m.instanceId); const md = this.modelManager?.instances?.get(m.instanceId);
if (md && md.locked) return this.clear(); if (md && md.locked) return this.clear();
const fid = folderIdOf('model', m.instanceId);
if (fid != null) return this.selectFolder(fid);
return this.selectModelByInstanceId(m.instanceId); return this.selectModelByInstanceId(m.instanceId);
} }
if (m?.isUserModel) { if (m?.isUserModel) {
const ud = this.userModelManager?.instances?.get(m.instanceId); const ud = this.userModelManager?.instances?.get(m.instanceId);
if (ud && ud.locked) return this.clear(); if (ud && ud.locked) return this.clear();
const fid = folderIdOf('userModel', m.instanceId);
if (fid != null) return this.selectFolder(fid);
return this.selectUserModelByInstanceId(m.instanceId); return this.selectUserModelByInstanceId(m.instanceId);
} }
if (m?.isPrimitive) { if (m?.isPrimitive) {
const pd = this.primitiveManager?.instances?.get(m.primitiveId); const pd = this.primitiveManager?.instances?.get(m.primitiveId);
if (pd && pd.locked) return this.clear(); if (pd && pd.locked) return this.clear();
const fid = folderIdOf('primitive', m.primitiveId);
if (fid != null) return this.selectFolder(fid);
return this.selectPrimitiveById(m.primitiveId); return this.selectPrimitiveById(m.primitiveId);
} }
if (m?.isSpawn) { if (m?.isSpawn) {