Add browser-only editor export and 2.5D runtime presentation

This commit is contained in:
emil28092005
2026-09-12 00:05:19 +03:00
parent a25ed40805
commit 1a0bf725fa
16 changed files with 883 additions and 53 deletions
+84 -39
View File
@@ -69,7 +69,8 @@ import {
mimeFor,
} from "../engine/archive.ts";
import { FormaRuntime } from "../engine/runtime.ts";
import { loadDraft, saveDraft } from "./persistence.ts";
import { saveDraft } from "./persistence.ts";
import { loadEditorProject, type EditorStartupOptions } from "./startup.ts";
import { inspectModel } from "../engine/model.ts";
import "./editor.css";
import { BuildPanel } from "./BuildPanel.tsx";
@@ -277,7 +278,7 @@ function Modal({
</div>
);
}
export default function Editor() {
export default function Editor({ initialProject, browserOnly = false }: EditorStartupOptions = {}) {
const [project, setProject] = useState<Project | null>(null),
[selected, setSelected] = useState<string | null>(null),
[connection, setConnection] = useState<any>(null),
@@ -405,26 +406,10 @@ export default function Editor() {
let alive = true;
let unsubscribe: (() => void) | undefined;
(async () => {
let p: Project | null = null,
status: any = null;
try {
const r = await fetch("/api/status", {
signal: AbortSignal.timeout(1800),
});
if (r.ok) {
const d: any = await r.json();
if (d.forma) {
status = d;
p = (await call("/api/project")).project;
}
}
} catch {}
if (!p)
try {
p = await loadDraft();
} catch {}
const { project: startingProject, status, restored } = await loadEditorProject({ initialProject, browserOnly });
if (!alive) return;
store.current = new ProjectStore(p || defaultProject());
store.current = new ProjectStore(startingProject);
setCached(restored);
connected.current = status;
setConnection(status);
setProject(store.current.project);
@@ -507,7 +492,7 @@ export default function Editor() {
events.current?.close();
clearTimeout(saveTimer.current);
};
}, [accept, addLog, call]);
}, [accept, addLog, call, initialProject, browserOnly]);
useEffect(() => {
if (!ready || !canvas.current) return;
const r = new FormaRuntime(canvas.current, {
@@ -1162,13 +1147,13 @@ export default function Editor() {
</div>
)}
</div>
<span className="save-status">
<span className="save-status" title={browserOnly ? "Правки сохраняются только в этом браузере. Скачай .forma через меню проекта, чтобы перенести их." : undefined}>
<i />
{connection
? "На диске"
: cached
? "Автокопия сохранена"
: "Локальный проект"}
: browserOnly ? "Проект в браузере" : "Локальный проект"}
</span>
<button className="command-search" onClick={() => setModal("search")}>
<Search size={14} />
@@ -1451,7 +1436,7 @@ export default function Editor() {
</button>
</>
)}
{playing && (
{playing && !project.settings.presentation && (
<>
<div className="game-hud">
<div>
@@ -1651,20 +1636,39 @@ export default function Editor() {
<span>{c.color}</span>
</div>
</div>
{["roughness", "metallic", "emissive"].map((k, i) => (
<div className="property" key={k}>
<label>
{["Шероховатость", "Металличность", "Свечение"][i]}
</label>
<Num
label={k}
value={c[k] || 0}
min={0}
max={k === "emissive" ? 5 : 1}
onChange={(v) => void componentField(type, k, v)}
/>
</div>
))}
{["roughness", "metallic", "emissive", "alpha"].map(
(k, i) => (
<div className="property" key={k}>
<label>
{
[
"Шероховатость",
"Металличность",
"Свечение",
"Непрозрачность",
][i]
}
</label>
<Num
label={k}
value={c[k] ?? (k === "alpha" ? 1 : 0)}
min={0}
max={k === "emissive" ? 5 : 1}
onChange={(v) => void componentField(type, k, v)}
/>
</div>
),
)}
<label className="check-row">
<input
type="checkbox"
checked={c.unlit || false}
onChange={(e) =>
void componentField(type, "unlit", e.target.checked)
}
/>
Без освещения (2D / VFX)
</label>
{node.components.mesh?.type === "model" && (
<label className="check-row">
<input
@@ -1801,9 +1805,50 @@ export default function Editor() {
}
>
<option value="follow">Следование</option>
<option value="fixed">Фиксированная</option>
<option value="firstPerson">От первого лица</option>
</select>
</div>
<div className="property">
<label>Проекция</label>
<select
aria-label="Проекция камеры"
value={c.projection || "perspective"}
onChange={(e) =>
void setComponent(type, {
...c,
projection: e.target.value,
orthoWidth: c.orthoWidth || 20,
})
}
>
<option value="perspective">Перспектива</option>
<option value="orthographic">Ортографическая</option>
</select>
</div>
{c.projection === "orthographic" && (
<div className="property">
<label>Ширина кадра</label>
<Num
label="Ширина ортокамеры"
value={c.orthoWidth || 20}
min={0.1}
max={200}
onChange={(v) =>
void componentField(type, "orthoWidth", v)
}
/>
</div>
)}
{c.mode === "fixed" && (
<Vector
label="Точка взгляда"
value={c.lookAt || [0, 0, 0]}
onChange={(v) =>
void componentField(type, "lookAt", v)
}
/>
)}
<div className="property">
<label>Цель</label>
{entitySelect(