Compare commits
7 Commits
feat/studi
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 542949eaff | |||
|
|
b638842948 | ||
|
|
22026f2f1d | ||
| 89bdeff657 | |||
| b457a29a95 | |||
| 143e529d11 | |||
|
|
a9a9668071 |
@ -1,8 +1,10 @@
|
||||
/* === Hierarchy Panel === */
|
||||
/* Компактные строки (как Roblox Explorer): меньше вертикальных отступов —
|
||||
больше объектов влезает без скролла. */
|
||||
.hierarchy {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 6px 0;
|
||||
padding: 4px 0;
|
||||
font-size: 12px;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
@ -13,13 +15,13 @@
|
||||
}
|
||||
|
||||
.rootLine {
|
||||
padding: 6px 8px;
|
||||
padding: 3px 8px;
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.systemItem {
|
||||
padding: 4px 8px 4px 28px;
|
||||
padding: 2px 8px 2px 26px;
|
||||
color: var(--text-dim);
|
||||
font-size: 12px;
|
||||
}
|
||||
@ -28,11 +30,11 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 8px;
|
||||
padding: 3px 8px;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
margin-top: 6px;
|
||||
margin-top: 3px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@ -72,8 +74,8 @@
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 8px;
|
||||
gap: 7px;
|
||||
padding: 2px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
color: var(--text);
|
||||
|
||||
@ -73,6 +73,9 @@ const GLYPHS = {
|
||||
'arrow-down': () => (<><path d="M12 5v14" {...S}/><path d="M6 13l6 6 6-6" {...S}/></>),
|
||||
'arrow-left': () => (<><path d="M19 12H5" {...S}/><path d="M11 6l-6 6 6 6" {...S}/></>),
|
||||
'arrow-right': () => (<><path d="M5 12h14" {...S}/><path d="M13 6l6 6-6 6" {...S}/></>),
|
||||
// Полноэкранный режим: 4 уголка наружу / внутрь.
|
||||
'fullscreen': () => (<><path d="M4 9V4h5" {...S}/><path d="M20 9V4h-5" {...S}/><path d="M4 15v5h5" {...S}/><path d="M20 15v5h-5" {...S}/></>),
|
||||
'fullscreen-exit': () => (<><path d="M9 4v5H4" {...S}/><path d="M15 4v5h5" {...S}/><path d="M9 20v-5H4" {...S}/><path d="M15 20v-5h5" {...S}/></>),
|
||||
refresh: () => (<><path d="M20 11a8 8 0 0 0-14-4.5L4 9" {...S}/><path d="M4 4v5h5" {...S}/><path d="M4 13a8 8 0 0 0 14 4.5L20 15" {...S}/><path d="M20 20v-5h-5" {...S}/></>),
|
||||
cycle: () => (<><path d="M20 11a8 8 0 0 0-14-4.5L4 9" {...S}/><path d="M4 4v5h5" {...S}/><path d="M4 13a8 8 0 0 0 14 4.5L20 15" {...S}/><path d="M20 20v-5h-5" {...S}/></>),
|
||||
flag: () => (<><path d="M6 21V4" {...S}/><path d="M6 4h11l-2.5 4L17 12H6" {...S}/></>),
|
||||
|
||||
@ -47,6 +47,11 @@ import cl from './KubikonEditor.module.css';
|
||||
import Icon from './Icon';
|
||||
import ConfirmModal from './ConfirmModal';
|
||||
|
||||
// В десктоп-приложении (Electron-обёртка, см. rublox-desktop) окно и так на
|
||||
// весь экран без браузерной панели — fullscreen НЕ нужен. preload выставляет
|
||||
// window.__RUBLOX_DESKTOP__. Глушим авто-fullscreen, чтобы не дёргать окно.
|
||||
const IS_DESKTOP_APP = typeof window !== 'undefined' && !!window.__RUBLOX_DESKTOP__;
|
||||
|
||||
const AUTOSAVE_DEBOUNCE_MS = 10000; // 10 секунд тишины → авто-сохранение
|
||||
|
||||
// Шаблон глобального скрипта.
|
||||
@ -470,6 +475,98 @@ const KubikonEditor = () => {
|
||||
|
||||
const canvasRef = useRef(null);
|
||||
const sceneRef = useRef(null);
|
||||
// === Fullscreen редактора ===
|
||||
// Верхняя панель браузера съедает ~20% экрана. Браузер НЕ даёт включить
|
||||
// fullscreen автоматически при загрузке (нужен user gesture), поэтому:
|
||||
// 1) кнопка в шапке;
|
||||
// 2) автоматический вход при ПЕРВОМ клике пользователя по редактору.
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const fsAutoTriedRef = useRef(false); // авто-вход пробуем только 1 раз
|
||||
const requestEditorFullscreen = React.useCallback(() => {
|
||||
try {
|
||||
const root = document.documentElement;
|
||||
const req = root.requestFullscreen
|
||||
|| root.webkitRequestFullscreen
|
||||
|| root.mozRequestFullScreen
|
||||
|| root.msRequestFullscreen;
|
||||
if (req && !document.fullscreenElement) req.call(root).catch(() => {});
|
||||
} catch (e) { /* юзер запретил — работаем в окне */ }
|
||||
}, []);
|
||||
const exitEditorFullscreen = React.useCallback(() => {
|
||||
try {
|
||||
const ex = document.exitFullscreen || document.webkitExitFullscreen
|
||||
|| document.mozCancelFullScreen || document.msExitFullscreen;
|
||||
if (ex && document.fullscreenElement) ex.call(document).catch?.(() => {});
|
||||
} catch (e) { /* ignore */ }
|
||||
}, []);
|
||||
const toggleEditorFullscreen = React.useCallback(() => {
|
||||
if (document.fullscreenElement) exitEditorFullscreen();
|
||||
else requestEditorFullscreen();
|
||||
}, [requestEditorFullscreen, exitEditorFullscreen]);
|
||||
// Следим за состоянием fullscreen (кнопка показывает актуальную иконку).
|
||||
useEffect(() => {
|
||||
const onFsChange = () => setIsFullscreen(!!document.fullscreenElement);
|
||||
document.addEventListener('fullscreenchange', onFsChange);
|
||||
document.addEventListener('webkitfullscreenchange', onFsChange);
|
||||
return () => {
|
||||
document.removeEventListener('fullscreenchange', onFsChange);
|
||||
document.removeEventListener('webkitfullscreenchange', onFsChange);
|
||||
};
|
||||
}, []);
|
||||
// Автовход в fullscreen при ПЕРВОМ клике/нажатии по редактору. Один раз.
|
||||
useEffect(() => {
|
||||
// В десктоп-приложении окно и так на весь экран — авто-FS не нужен.
|
||||
if (IS_DESKTOP_APP) return;
|
||||
const tryAuto = () => {
|
||||
if (fsAutoTriedRef.current) return;
|
||||
fsAutoTriedRef.current = true;
|
||||
if (!document.fullscreenElement) requestEditorFullscreen();
|
||||
};
|
||||
window.addEventListener('pointerdown', tryAuto, { once: true, capture: true });
|
||||
window.addEventListener('keydown', tryAuto, { once: true, capture: true });
|
||||
return () => {
|
||||
window.removeEventListener('pointerdown', tryAuto, { capture: true });
|
||||
window.removeEventListener('keydown', tryAuto, { capture: true });
|
||||
};
|
||||
}, [requestEditorFullscreen]);
|
||||
// При уходе со страницы редактора — выходим из fullscreen.
|
||||
useEffect(() => () => { exitEditorFullscreen(); }, [exitEditorFullscreen]);
|
||||
|
||||
// === Регулируемая граница между «Объекты сцены» и «Свойства» ===
|
||||
// Доля высоты под список объектов (0.2..0.85). Сохраняем в localStorage.
|
||||
const [hierFraction, setHierFraction] = useState(() => {
|
||||
const v = parseFloat(localStorage.getItem('rbxStudioHierFraction'));
|
||||
return Number.isFinite(v) && v >= 0.2 && v <= 0.85 ? v : 0.5;
|
||||
});
|
||||
const rightPanelRef = useRef(null);
|
||||
const splitDragRef = useRef(false);
|
||||
useEffect(() => {
|
||||
const onMove = (e) => {
|
||||
if (!splitDragRef.current || !rightPanelRef.current) return;
|
||||
const rect = rightPanelRef.current.getBoundingClientRect();
|
||||
// Доля от верха панели до курсора (за вычетом верхнего заголовка ~24px).
|
||||
let f = (e.clientY - rect.top - 24) / Math.max(1, rect.height - 24);
|
||||
f = Math.max(0.2, Math.min(0.85, f));
|
||||
setHierFraction(f);
|
||||
};
|
||||
const onUp = () => {
|
||||
if (!splitDragRef.current) return;
|
||||
splitDragRef.current = false;
|
||||
document.body.style.cursor = '';
|
||||
try { localStorage.setItem('rbxStudioHierFraction', String(hierFraction)); } catch (_) {}
|
||||
};
|
||||
window.addEventListener('pointermove', onMove);
|
||||
window.addEventListener('pointerup', onUp);
|
||||
return () => {
|
||||
window.removeEventListener('pointermove', onMove);
|
||||
window.removeEventListener('pointerup', onUp);
|
||||
};
|
||||
}, [hierFraction]);
|
||||
const startSplitDrag = React.useCallback((e) => {
|
||||
e.preventDefault();
|
||||
splitDragRef.current = true;
|
||||
document.body.style.cursor = 'row-resize';
|
||||
}, []);
|
||||
// Team Create — клиент совместного редактирования + presence-overlay.
|
||||
const collabRef = useRef(null);
|
||||
const collabOverlayRef = useRef(null);
|
||||
@ -2146,7 +2243,9 @@ const KubikonEditor = () => {
|
||||
// fullscreen — иначе Ctrl+W/Ctrl+D случайно закрывают вкладку
|
||||
// в режиме игры. Это user gesture (клик по кнопке Play),
|
||||
// поэтому requestFullscreen() разрешён.
|
||||
try {
|
||||
// В десктоп-приложении этого риска нет (нет вкладок браузера) —
|
||||
// окно и так на весь экран, FS не нужен.
|
||||
if (!IS_DESKTOP_APP) try {
|
||||
const root = document.documentElement;
|
||||
const req = root.requestFullscreen
|
||||
|| root.webkitRequestFullscreen
|
||||
@ -2309,6 +2408,18 @@ const KubikonEditor = () => {
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{/* Полноэкранный режим — съедает верхнюю панель браузера.
|
||||
В десктоп-приложении не нужен (окно и так на весь экран). */}
|
||||
{!IS_DESKTOP_APP && (
|
||||
<button
|
||||
onClick={toggleEditorFullscreen}
|
||||
title={isFullscreen ? 'Выйти из полноэкранного режима (F11)' : 'На весь экран (F11)'}
|
||||
className={cl.toolbarBtn}
|
||||
style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: 38, height: 38, padding: 0, flexShrink: 0 }}
|
||||
>
|
||||
<Icon name={isFullscreen ? 'fullscreen-exit' : 'fullscreen'} size={16} />
|
||||
</button>
|
||||
)}
|
||||
{/* Кнопка баг-репорта в шапке — кликает по скрытой плавающей. */}
|
||||
<button
|
||||
onClick={() => document.querySelector('[data-kubikon-bug-btn]')?.click()}
|
||||
@ -3520,7 +3631,8 @@ const KubikonEditor = () => {
|
||||
</div>
|
||||
|
||||
{/* Правая панель — Hierarchy + Inspector */}
|
||||
<aside className={cl.rightPanel}>
|
||||
<aside className={cl.rightPanel} ref={rightPanelRef}>
|
||||
<div className={cl.rightSection} style={{ flexGrow: hierFraction, flexBasis: 0 }}>
|
||||
<div className={cl.panelTitle}>Объекты сцены</div>
|
||||
<HierarchyPanel
|
||||
blocks={blocksList}
|
||||
@ -3745,7 +3857,16 @@ const KubikonEditor = () => {
|
||||
onAssignToFolder={(kind, ref, folderId) =>
|
||||
sceneRef.current?.assignToFolder(kind, ref, folderId)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Перетаскиваемая граница между списком объектов и свойствами. */}
|
||||
<div
|
||||
className={cl.rightSplitter}
|
||||
onPointerDown={startSplitDrag}
|
||||
title="Потяните, чтобы изменить высоту"
|
||||
/>
|
||||
|
||||
<div className={cl.rightSection} style={{ flexGrow: (1 - hierFraction), flexBasis: 0 }}>
|
||||
<div className={cl.panelTitle}>Свойства</div>
|
||||
<InspectorPanel
|
||||
selection={selection}
|
||||
@ -3882,6 +4003,7 @@ const KubikonEditor = () => {
|
||||
markDirty();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
|
||||
@ -31,7 +31,8 @@
|
||||
font-family: "Roboto Condensed", system-ui, -apple-system, sans-serif;
|
||||
|
||||
display: grid;
|
||||
grid-template-rows: 56px auto 1fr 28px;
|
||||
/* UI уменьшен на ~10% (ближе к Roblox Studio): topbar 56→50, status 28→26. */
|
||||
grid-template-rows: 50px auto 1fr 26px;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
@ -39,7 +40,7 @@
|
||||
color: var(--text);
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.editor *,
|
||||
@ -52,8 +53,8 @@
|
||||
.topBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 0 16px;
|
||||
gap: 12px;
|
||||
padding: 0 14px;
|
||||
background: linear-gradient(180deg, var(--bg-dark) 0%, var(--bg-darkest) 100%);
|
||||
border-bottom: 2px solid var(--border);
|
||||
z-index: 10;
|
||||
@ -145,12 +146,12 @@
|
||||
}
|
||||
|
||||
.toolbarBtn {
|
||||
padding: 8px 16px;
|
||||
padding: 7px 13px;
|
||||
background: var(--bg-mid);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 6px;
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
font-weight: 600;
|
||||
@ -210,7 +211,9 @@
|
||||
/* === WORKSPACE === */
|
||||
.workspace {
|
||||
display: grid;
|
||||
grid-template-columns: 240px minmax(0, 1fr) 280px;
|
||||
/* Правая панель шире (280→320) — длинные имена объектов не обрезаются,
|
||||
больше места под список и свойства. Левая чуть уже (240→224). */
|
||||
grid-template-columns: 224px minmax(0, 1fr) 320px;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
@ -233,18 +236,61 @@
|
||||
|
||||
.rightPanel {
|
||||
border-left: 2px solid var(--border);
|
||||
overflow: hidden; /* секции скроллятся внутри себя, панель — нет */
|
||||
}
|
||||
|
||||
/* Секция правой панели (список объектов / свойства) — занимает свою долю
|
||||
высоты, скроллится независимо. Доля регулируется сплиттером. */
|
||||
.rightSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Перетаскиваемая граница между списком объектов и свойствами. */
|
||||
.rightSplitter {
|
||||
height: 6px;
|
||||
flex-shrink: 0;
|
||||
cursor: row-resize;
|
||||
background: var(--bg-mid);
|
||||
border-top: 1px solid var(--border);
|
||||
border-bottom: 1px solid var(--border);
|
||||
position: relative;
|
||||
transition: background 0.12s;
|
||||
}
|
||||
.rightSplitter::before {
|
||||
/* визуальная «ручка» — три точки по центру */
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 28px;
|
||||
height: 2px;
|
||||
border-radius: 2px;
|
||||
background: var(--text-dim);
|
||||
opacity: 0.5;
|
||||
}
|
||||
.rightSplitter:hover {
|
||||
background: var(--accent);
|
||||
}
|
||||
.rightSplitter:hover::before {
|
||||
background: #fff;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.panelTitle {
|
||||
padding: 12px 16px;
|
||||
font-size: 11px;
|
||||
padding: 7px 14px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.5px;
|
||||
letter-spacing: 1.3px;
|
||||
color: var(--text-dim);
|
||||
background: var(--bg-mid);
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-top: 1px solid var(--border);
|
||||
font-weight: 800;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.panelTitle:first-child {
|
||||
|
||||
@ -76,6 +76,14 @@ export class VehicleManager {
|
||||
veh.bodyInstanceId = bodyId;
|
||||
const inst = this._models.instances.get(bodyId);
|
||||
if (inst && inst.rootMesh) {
|
||||
// Кузов машины — динамический объект: им двигает VehicleManager
|
||||
// (через парентинг к chassisNode). Исключаем из LOD-freeze, иначе
|
||||
// freezeWorldMatrix замораживает меш и он перестаёт следовать за
|
||||
// chassisNode → «едешь на невидимой машине, видимая стоит».
|
||||
// (LOD меряет дистанцию по локальной root.position запарентенного
|
||||
// кузова ≈0,0,0 — некорректно, и замораживает машину.)
|
||||
inst._spawnedAtRuntime = true;
|
||||
inst._isVehicleBody = true;
|
||||
// Габариты AABB + вертикальный offset кузова СЧИТАЕМ ДО парентинга
|
||||
// (в мировых координатах, кузов ещё в (x,y,z)).
|
||||
try {
|
||||
|
||||
@ -789,7 +789,8 @@ const KubikonPlayer = () => {
|
||||
|| root.webkitRequestFullscreen
|
||||
|| root.mozRequestFullScreen
|
||||
|| root.msRequestFullscreen;
|
||||
if (req) {
|
||||
// В десктоп-приложении (Electron) окно и так на весь экран — FS не нужен.
|
||||
if (req && !(typeof window !== 'undefined' && window.__RUBLOX_DESKTOP__)) {
|
||||
try { await req.call(root); } catch (e) { /* отменено */ }
|
||||
}
|
||||
setMobileStartTapped(true);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user