diff --git a/src/editor/KubikonEditor.jsx b/src/editor/KubikonEditor.jsx index f090f6f..7061068 100644 --- a/src/editor/KubikonEditor.jsx +++ b/src/editor/KubikonEditor.jsx @@ -3351,14 +3351,20 @@ const KubikonEditor = () => { onDeleteBlock={(x, y, z) => { sceneRef.current?.blockManager?.removeBlock(x, y, z); sceneRef.current?.clearSelection(); + markDirty(); + hierarchyDirtyRef.current = true; }} onDeleteModel={(id) => { sceneRef.current?.modelManager?.removeInstance(id); sceneRef.current?.clearSelection(); + markDirty(); + hierarchyDirtyRef.current = true; }} onDeletePrimitive={(id) => { sceneRef.current?.primitiveManager?.removeInstance(id); sceneRef.current?.clearSelection(); + markDirty(); + hierarchyDirtyRef.current = true; }} onFocusSelection={() => sceneRef.current?.focusOnSelection()} onCreateFolder={(name, parentId) => diff --git a/src/editor/engine/BabylonScene.js b/src/editor/engine/BabylonScene.js index 7813a55..7b7b31d 100644 --- a/src/editor/engine/BabylonScene.js +++ b/src/editor/engine/BabylonScene.js @@ -1723,6 +1723,9 @@ export class BabylonScene { : ShadowGenerator.QUALITY_MEDIUM; csm.darkness = 0.4; csm.autoCalcDepthBounds = true; + // Плавное затухание тени у края каскада — убирает «полосу-хвост» + // тени персонажа на весь пол при движении (баг 2026-06-05). + csm.frustumEdgeFalloff = 8; this._shadowGenerator = csm; } else { // Обычный ShadowGenerator. Поднял разрешение для soft до 2048. @@ -5679,6 +5682,14 @@ export class BabylonScene { this.selection?.selectByMesh(m); const sel = this.selection?.getSelection(); 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); if (!root) return false; this._freeDragCandidate = { root }; @@ -5693,6 +5704,24 @@ export class BabylonScene { if (!cand) return; const sel = this.selection?.getSelection(); 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 half = this._freeDragHalf || { x: 0.5, y: 0.5, z: 0.5 }; diff --git a/src/editor/engine/SelectionManager.js b/src/editor/engine/SelectionManager.js index 8e51719..22b0a94 100644 --- a/src/editor/engine/SelectionManager.js +++ b/src/editor/engine/SelectionManager.js @@ -76,24 +76,37 @@ export class SelectionManager { selectByMesh(mesh) { if (!mesh) return this.clear(); 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) { return this.selectBlockAt(m.gridX, m.gridY, m.gridZ); } if (m?.isModel) { - // Заблокированный объект (Фаза 5.11) не выделяется кликом по - // сцене — только через иерархию (чтобы можно было снять lock). const md = this.modelManager?.instances?.get(m.instanceId); 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); } if (m?.isUserModel) { const ud = this.userModelManager?.instances?.get(m.instanceId); 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); } if (m?.isPrimitive) { const pd = this.primitiveManager?.instances?.get(m.primitiveId); 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); } if (m?.isSpawn) {