fix(studio): кит спавнится на поверхности в фокусе + многочастный в папку + снятие выделения при Play

1) Предметы кита из тулбокса спавнятся на ТВЁРДОЙ поверхности под центром
   экрана (getPlacementPointAtCenter: raycast в пол/объект), а не под камерой.
2) Многочастный кит (дверь) теперь реально попадает в папку: insertGameplayKit
   ставил folderId, но дерево не пересобиралось (markDirty не трогает
   hierarchyDirtyRef) — добавлен hierarchyDirtyRef.current=true.
3) enterPlayMode снимает любое выделение редактора (объект/папка) + убирает
   пивот папки и gizmo — в Play больше нет подсветки выбранного.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
min 2026-06-05 18:30:41 +03:00
parent bf93219266
commit 46414d874b
2 changed files with 42 additions and 6 deletions

View File

@ -778,12 +778,17 @@ const KubikonEditor = () => {
const kit = getKit(kitId); const kit = getKit(kitId);
const s = sceneRef.current; const s = sceneRef.current;
if (!kit || !s) return; if (!kit || !s) return;
// Точка вставки перед камерой редактора (~6м), как у paste. // Точка вставки на ТВЁРДОЙ поверхности под центром экрана (пол/объект),
let px = 0, pz = 0; // чтобы предмет встал на землю в фокусе камеры, а не висел под камерой.
let px = 0, pz = 0, py = 0;
try { try {
const cam = s.camera; const gp = s.getPlacementPointAtCenter?.();
const fwd = cam?.getForwardRay ? cam.getForwardRay().direction : null; if (gp) { px = gp.x; pz = gp.z; py = gp.y; }
if (cam && fwd) { px = cam.position.x + fwd.x * 6; pz = cam.position.z + fwd.z * 6; } else {
const cam = s.camera;
const fwd = cam?.getForwardRay ? cam.getForwardRay().direction : null;
if (cam && fwd) { px = cam.position.x + fwd.x * 6; pz = cam.position.z + fwd.z * 6; }
}
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
// 1) Создаём примитивы кита. Запоминаем все id (первый для on-target скрипта). // 1) Создаём примитивы кита. Запоминаем все id (первый для on-target скрипта).
@ -792,7 +797,7 @@ const KubikonEditor = () => {
if (Array.isArray(kit.prims)) { if (Array.isArray(kit.prims)) {
for (const p of kit.prims) { for (const p of kit.prims) {
const newId = s.primitiveManager?.addInstance(p.type || 'cube', { const newId = s.primitiveManager?.addInstance(p.type || 'cube', {
x: px + (p.x || 0), y: (p.y != null ? p.y : 1), z: pz + (p.z || 0), x: px + (p.x || 0), y: py + (p.y != null ? p.y : 1), z: pz + (p.z || 0),
sx: p.sx, sy: p.sy, sz: p.sz, sx: p.sx, sy: p.sy, sz: p.sz,
color: p.color, material: p.material, color: p.color, material: p.material,
canCollide: p.canCollide !== false, visible: true, anchored: true, canCollide: p.canCollide !== false, visible: true, anchored: true,
@ -829,6 +834,7 @@ const KubikonEditor = () => {
} }
markDirty(); markDirty();
hierarchyDirtyRef.current = true; // пересобрать дерево (примитивы с folderId)
setScriptsList(s.getScripts?.() || []); setScriptsList(s.getScripts?.() || []);
if (s.folderManager) setFoldersList(s.folderManager.getAll()); if (s.folderManager) setFoldersList(s.folderManager.getAll());
// Выделим созданное и наведём камеру (видно, куда добавилось). // Выделим созданное и наведём камеру (видно, куда добавилось).

View File

@ -2936,6 +2936,29 @@ export class BabylonScene {
return { mesh, point: pi.pickedPoint, pickInfo: pi }; return { mesh, point: pi.pickedPoint, pickInfo: pi };
} }
/**
* Точка под центром экрана на твёрдой поверхности (пол/объект) для
* спавна предметов из тулбокса «в фокусе камеры на земле».
* Возвращает { x, y, z } (y высота поверхности). Fallback: проекция на y=0.
*/
getPlacementPointAtCenter() {
const hit = this._pickFromCenter();
if (hit && hit.point) {
return { x: hit.point.x, y: hit.point.y, z: hit.point.z };
}
// Нет попадания — проецируем луч из центра на плоскость y=0.
try {
const w = this.engine?.getRenderWidth?.() || this.canvas.width;
const h = this.engine?.getRenderHeight?.() || this.canvas.height;
const ray = this.scene.createPickingRay(w / 2, h / 2, null, this.scene.activeCamera);
if (Math.abs(ray.direction.y) > 1e-4) {
const t = -ray.origin.y / ray.direction.y;
if (t > 0) return { x: ray.origin.x + ray.direction.x * t, y: 0, z: ray.origin.z + ray.direction.z * t };
}
} catch (e) { /* ignore */ }
return null;
}
/** /**
* Извлечь target {kind, ref} из mesh (proxy/прим/модель). * Извлечь target {kind, ref} из mesh (proxy/прим/модель).
* Используется при клике/touch в Play. * Используется при клике/touch в Play.
@ -5921,6 +5944,13 @@ export class BabylonScene {
*/ */
enterPlayMode() { enterPlayMode() {
if (this._isPlaying) return; if (this._isPlaying) return;
// Снять любое выделение редактора (объект/папка) перед запуском игры —
// иначе в Play остаётся подсветка/гизмо выбранного объекта.
try {
if (this._folderPivot) { this._folderPivot.dispose(); this._folderPivot = null; this._folderPivotId = null; }
this._gizmo?.attachTo(null);
this.selection?.clear?.();
} catch (e) { /* ignore */ }
this._isPlaying = true; this._isPlaying = true;
// Сброс состояния касаний — каждый прогон начинается «не касаясь», // Сброс состояния касаний — каждый прогон начинается «не касаясь»,
// иначе rising-edge touch не сработает, если при стопе игрок стоял на цели. // иначе rising-edge touch не сработает, если при стопе игрок стоял на цели.