From bf93219266ea206325b405e0d8e283693b88198a Mon Sep 17 00:00:00 2001 From: min Date: Fri, 5 Jun 2026 18:19:02 +0300 Subject: [PATCH] =?UTF-8?q?fix(studio):=20=D0=BA=D0=BB=D0=B8=D0=BA=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D1=87=D0=B0=D1=81=D1=82=D0=B8=20=D0=BF=D0=B0?= =?UTF-8?q?=D0=BF=D0=BA=D0=B8=20=D0=B2=D1=8B=D0=B4=D0=B5=D0=BB=D1=8F=D0=B5?= =?UTF-8?q?=D1=82=20=D0=B2=D1=81=D1=8E=20=D0=BF=D0=B0=D0=BF=D0=BA=D1=83=20?= =?UTF-8?q?+=20free-drag=20=D0=BF=D0=B0=D0=BF=D0=BA=D0=B8=20+=20=D1=83?= =?UTF-8?q?=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE=D0=B1=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=BB=D1=8F=D0=B5=D1=82=20=D0=B4=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B2=D0=BE=20+=20=D0=B7=D0=B0=D1=82=D1=83=D1=85=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B4=D0=BB=D0=B8=D0=BD=D0=BD=D0=BE=D0=B9=20?= =?UTF-8?q?=D1=82=D0=B5=D0=BD=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1) Длинная тень-полоса: csm.frustumEdgeFalloff=8 — тень персонажа больше не тянется на весь пол. 2) Удаление примитива/модели/блока через ПКМ теперь обновляет дерево (markDirty + hierarchyDirtyRef) — раньше объект удалялся, но не пропадал. 3) Клик по СЦЕНЕ по объекту в папке → выделяется ВСЯ папка (selectByMesh проверяет folderId). Отдельную часть — через раскрытие папки в дереве. 4) Free-drag папки: зажал ЛКМ на группе и тянешь — двигается вся папка (moveFolderBy по дельте центра). Co-Authored-By: Claude Opus 4.8 --- src/editor/KubikonEditor.jsx | 6 ++++++ src/editor/engine/BabylonScene.js | 29 +++++++++++++++++++++++++++ src/editor/engine/SelectionManager.js | 17 ++++++++++++++-- 3 files changed, 50 insertions(+), 2 deletions(-) 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) {