fix(simulation): bound rendering and expose camera inversion
This commit is contained in:
@@ -22,6 +22,7 @@ import type { SimulationWorldManifest } from "../../core/simulation/projects";
|
|||||||
export type SimulationViewMode = "visual" | "collision" | "combined";
|
export type SimulationViewMode = "visual" | "collision" | "combined";
|
||||||
export type SimulationQuality = "low" | "medium" | "high" | "ultra" | "maximum";
|
export type SimulationQuality = "low" | "medium" | "high" | "ultra" | "maximum";
|
||||||
export type SimulationLayer = "visual" | "collision";
|
export type SimulationLayer = "visual" | "collision";
|
||||||
|
export type SimulationCameraAxis = "horizontal" | "vertical";
|
||||||
|
|
||||||
export const PLAYCANVAS_IDENTITY_TRANSFORM = [
|
export const PLAYCANVAS_IDENTITY_TRANSFORM = [
|
||||||
1, 0, 0, 0,
|
1, 0, 0, 0,
|
||||||
@@ -43,6 +44,7 @@ export interface SimulationRuntime {
|
|||||||
setViewMode(mode: SimulationViewMode): Promise<void>;
|
setViewMode(mode: SimulationViewMode): Promise<void>;
|
||||||
setQuality(quality: SimulationQuality): void;
|
setQuality(quality: SimulationQuality): void;
|
||||||
setLayerWorldTransform(layer: SimulationLayer, transform: number[]): void;
|
setLayerWorldTransform(layer: SimulationLayer, transform: number[]): void;
|
||||||
|
setCameraInversion(axis: SimulationCameraAxis, inverted: boolean): void;
|
||||||
home(): void;
|
home(): void;
|
||||||
focusBounds(): void;
|
focusBounds(): void;
|
||||||
dispose(): void;
|
dispose(): void;
|
||||||
@@ -65,12 +67,14 @@ const QUALITY_PROFILES: Record<SimulationQuality, {
|
|||||||
lodRangeMin: number;
|
lodRangeMin: number;
|
||||||
lodRangeMax: number;
|
lodRangeMax: number;
|
||||||
pixelRatio: number;
|
pixelRatio: number;
|
||||||
|
splatBudget: number;
|
||||||
|
targetFps: number;
|
||||||
}> = {
|
}> = {
|
||||||
low: { lodBaseDistance: 5, lodMultiplier: 2, lodRangeMin: 3, lodRangeMax: 5, pixelRatio: 0.75 },
|
low: { lodBaseDistance: 5, lodMultiplier: 2, lodRangeMin: 3, lodRangeMax: 5, pixelRatio: 0.75, splatBudget: 350_000, targetFps: 30 },
|
||||||
medium: { lodBaseDistance: 5, lodMultiplier: 2, lodRangeMin: 2, lodRangeMax: 5, pixelRatio: 1 },
|
medium: { lodBaseDistance: 5, lodMultiplier: 2, lodRangeMin: 2, lodRangeMax: 5, pixelRatio: 1, splatBudget: 700_000, targetFps: 45 },
|
||||||
high: { lodBaseDistance: 5, lodMultiplier: 3, lodRangeMin: 1, lodRangeMax: 5, pixelRatio: 1.5 },
|
high: { lodBaseDistance: 5, lodMultiplier: 3, lodRangeMin: 1, lodRangeMax: 5, pixelRatio: 1.25, splatBudget: 1_200_000, targetFps: 60 },
|
||||||
ultra: { lodBaseDistance: 7, lodMultiplier: 3, lodRangeMin: 0, lodRangeMax: 5, pixelRatio: 2 },
|
ultra: { lodBaseDistance: 7, lodMultiplier: 3, lodRangeMin: 0, lodRangeMax: 5, pixelRatio: 1.5, splatBudget: 2_500_000, targetFps: 60 },
|
||||||
maximum: { lodBaseDistance: 7, lodMultiplier: 3, lodRangeMin: 0, lodRangeMax: 0, pixelRatio: 2 },
|
maximum: { lodBaseDistance: 7, lodMultiplier: 3, lodRangeMin: 0, lodRangeMax: 5, pixelRatio: 2, splatBudget: 4_000_000, targetFps: 60 },
|
||||||
};
|
};
|
||||||
|
|
||||||
export class PlayCanvasRuntime implements SimulationRuntime {
|
export class PlayCanvasRuntime implements SimulationRuntime {
|
||||||
@@ -87,10 +91,23 @@ export class PlayCanvasRuntime implements SimulationRuntime {
|
|||||||
private collisionLoadPromise: Promise<void> | null = null;
|
private collisionLoadPromise: Promise<void> | null = null;
|
||||||
private resizeObserver: ResizeObserver | null = null;
|
private resizeObserver: ResizeObserver | null = null;
|
||||||
private viewMode: SimulationViewMode = "visual";
|
private viewMode: SimulationViewMode = "visual";
|
||||||
private quality: SimulationQuality = "maximum";
|
private quality: SimulationQuality = "high";
|
||||||
|
private cameraInversion = { horizontal: true, vertical: false };
|
||||||
|
private renderIntervalMs = 1000 / 60;
|
||||||
|
private renderAccumulatorMs = 0;
|
||||||
|
private disposed = false;
|
||||||
|
private scheduleFrame = (deltaMs: number): void => {
|
||||||
|
if (!this.app || this.disposed) return;
|
||||||
|
this.renderAccumulatorMs += deltaMs;
|
||||||
|
if (this.renderAccumulatorMs < this.renderIntervalMs) return;
|
||||||
|
this.renderAccumulatorMs %= this.renderIntervalMs;
|
||||||
|
this.app.renderNextFrame = true;
|
||||||
|
};
|
||||||
|
|
||||||
async mount(canvas: HTMLCanvasElement): Promise<void> {
|
async mount(canvas: HTMLCanvasElement): Promise<void> {
|
||||||
if (this.app) throw new Error("PlayCanvas runtime уже смонтирован.");
|
if (this.app) throw new Error("PlayCanvas runtime уже смонтирован.");
|
||||||
|
this.disposed = false;
|
||||||
|
this.renderAccumulatorMs = 0;
|
||||||
this.canvas = canvas;
|
this.canvas = canvas;
|
||||||
canvas.tabIndex = 0;
|
canvas.tabIndex = 0;
|
||||||
canvas.addEventListener("contextmenu", preventContextMenu);
|
canvas.addEventListener("contextmenu", preventContextMenu);
|
||||||
@@ -104,6 +121,8 @@ export class PlayCanvasRuntime implements SimulationRuntime {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.app = app;
|
this.app = app;
|
||||||
|
app.autoRender = false;
|
||||||
|
app.on("frameupdate", this.scheduleFrame);
|
||||||
app.setCanvasFillMode(FILLMODE_NONE, 1, 1);
|
app.setCanvasFillMode(FILLMODE_NONE, 1, 1);
|
||||||
app.setCanvasResolution(RESOLUTION_AUTO);
|
app.setCanvasResolution(RESOLUTION_AUTO);
|
||||||
app.scene.ambientLight = new Color(0.35, 0.37, 0.42);
|
app.scene.ambientLight = new Color(0.35, 0.37, 0.42);
|
||||||
@@ -126,7 +145,7 @@ export class PlayCanvasRuntime implements SimulationRuntime {
|
|||||||
zoomRange: new Vec2(0.05, 20_000),
|
zoomRange: new Vec2(0.05, 20_000),
|
||||||
},
|
},
|
||||||
}) as CameraController | null;
|
}) as CameraController | null;
|
||||||
if (controller) invertHorizontalCameraDrag(controller);
|
if (controller) configureCameraDrag(controller, () => this.cameraInversion);
|
||||||
app.root.addChild(camera);
|
app.root.addChild(camera);
|
||||||
this.camera = camera;
|
this.camera = camera;
|
||||||
this.cameraController = controller;
|
this.cameraController = controller;
|
||||||
@@ -158,6 +177,11 @@ export class PlayCanvasRuntime implements SimulationRuntime {
|
|||||||
this.visualAsset = asset;
|
this.visualAsset = asset;
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const ready = (loaded: Asset) => {
|
const ready = (loaded: Asset) => {
|
||||||
|
if (this.disposed || !this.app) {
|
||||||
|
loaded.unload();
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const visual = new Entity("GaussianWorld");
|
const visual = new Entity("GaussianWorld");
|
||||||
visual.enabled = false;
|
visual.enabled = false;
|
||||||
applyWorldTransform(visual, manifest.transforms.worldFromVisual);
|
applyWorldTransform(visual, manifest.transforms.worldFromVisual);
|
||||||
@@ -216,6 +240,10 @@ export class PlayCanvasRuntime implements SimulationRuntime {
|
|||||||
if (this.collisionEntity) applyWorldTransform(this.collisionEntity, transform);
|
if (this.collisionEntity) applyWorldTransform(this.collisionEntity, transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCameraInversion(axis: SimulationCameraAxis, inverted: boolean): void {
|
||||||
|
this.cameraInversion[axis] = inverted;
|
||||||
|
}
|
||||||
|
|
||||||
home(): void {
|
home(): void {
|
||||||
if (this.cameraController) {
|
if (this.cameraController) {
|
||||||
this.cameraController.reset(HOME_FOCUS, HOME_POSITION);
|
this.cameraController.reset(HOME_FOCUS, HOME_POSITION);
|
||||||
@@ -241,10 +269,23 @@ export class PlayCanvasRuntime implements SimulationRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
|
if (this.disposed) return;
|
||||||
|
this.disposed = true;
|
||||||
this.resizeObserver?.disconnect();
|
this.resizeObserver?.disconnect();
|
||||||
this.resizeObserver = null;
|
this.resizeObserver = null;
|
||||||
|
const webgl = this.canvas?.getContext("webgl2") ?? this.canvas?.getContext("webgl");
|
||||||
this.unloadWorld();
|
this.unloadWorld();
|
||||||
if (this.app) this.app.destroy();
|
if (this.app) {
|
||||||
|
this.app.autoRender = false;
|
||||||
|
this.app.renderNextFrame = false;
|
||||||
|
this.app.off("frameupdate", this.scheduleFrame);
|
||||||
|
this.app.destroy();
|
||||||
|
}
|
||||||
|
webgl?.getExtension("WEBGL_lose_context")?.loseContext();
|
||||||
|
if (this.canvas) {
|
||||||
|
this.canvas.width = 1;
|
||||||
|
this.canvas.height = 1;
|
||||||
|
}
|
||||||
if (this.canvas) {
|
if (this.canvas) {
|
||||||
this.canvas.removeEventListener("contextmenu", preventContextMenu);
|
this.canvas.removeEventListener("contextmenu", preventContextMenu);
|
||||||
this.canvas.removeEventListener("pointerdown", focusCanvas, true);
|
this.canvas.removeEventListener("pointerdown", focusCanvas, true);
|
||||||
@@ -293,6 +334,8 @@ export class PlayCanvasRuntime implements SimulationRuntime {
|
|||||||
gsplat.lodRangeMax = profile.lodRangeMax;
|
gsplat.lodRangeMax = profile.lodRangeMax;
|
||||||
}
|
}
|
||||||
if (this.app) {
|
if (this.app) {
|
||||||
|
this.app.scene.gsplat.splatBudget = profile.splatBudget;
|
||||||
|
this.renderIntervalMs = 1000 / profile.targetFps;
|
||||||
const devicePixelRatio = window.devicePixelRatio || 1;
|
const devicePixelRatio = window.devicePixelRatio || 1;
|
||||||
this.app.graphicsDevice.maxPixelRatio = Math.min(devicePixelRatio, profile.pixelRatio);
|
this.app.graphicsDevice.maxPixelRatio = Math.min(devicePixelRatio, profile.pixelRatio);
|
||||||
this.resize();
|
this.resize();
|
||||||
@@ -310,6 +353,11 @@ export class PlayCanvasRuntime implements SimulationRuntime {
|
|||||||
this.collisionAsset = asset;
|
this.collisionAsset = asset;
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
asset.ready((loaded) => {
|
asset.ready((loaded) => {
|
||||||
|
if (this.disposed || !this.app) {
|
||||||
|
loaded.unload();
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const resource = loaded.resource as ContainerResource | null;
|
const resource = loaded.resource as ContainerResource | null;
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
reject(new Error("PlayCanvas не открыл collision GLB."));
|
reject(new Error("PlayCanvas не открыл collision GLB."));
|
||||||
@@ -400,15 +448,20 @@ function focusCanvas(event: Event): void {
|
|||||||
(event.currentTarget as HTMLCanvasElement | null)?.focus({ preventScroll: true });
|
(event.currentTarget as HTMLCanvasElement | null)?.focus({ preventScroll: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
function invertHorizontalCameraDrag(controller: CameraController): void {
|
function configureCameraDrag(
|
||||||
|
controller: CameraController,
|
||||||
|
inversion: () => { horizontal: boolean; vertical: boolean },
|
||||||
|
): void {
|
||||||
// CameraControls 2.21.4 deliberately exposes one pinned input source here.
|
// CameraControls 2.21.4 deliberately exposes one pinned input source here.
|
||||||
// Keep its pointer-capture and fly/orbit behavior, changing only horizontal drag semantics.
|
// Keep its pointer-capture and fly/orbit behavior; only the user-selected axes change sign.
|
||||||
const input = controller._desktopInput;
|
const input = controller._desktopInput;
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
const read = input.read.bind(input);
|
const read = input.read.bind(input);
|
||||||
input.read = () => {
|
input.read = () => {
|
||||||
const frame = read();
|
const frame = read();
|
||||||
if (frame.mouse.length > 0) frame.mouse[0] *= -1;
|
const selected = inversion();
|
||||||
|
if (selected.horizontal && frame.mouse.length > 0) frame.mouse[0] *= -1;
|
||||||
|
if (selected.vertical && frame.mouse.length > 1) frame.mouse[1] *= -1;
|
||||||
return frame;
|
return frame;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,11 +22,12 @@ import {
|
|||||||
export function SimulationViewport({ project }: { project: SimulationProject }) {
|
export function SimulationViewport({ project }: { project: SimulationProject }) {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const runtimeRef = useRef<PlayCanvasRuntime | null>(null);
|
const runtimeRef = useRef<PlayCanvasRuntime | null>(null);
|
||||||
|
const cameraInversionRef = useRef({ horizontal: true, vertical: false });
|
||||||
const settingsId = useId();
|
const settingsId = useId();
|
||||||
const [state, setState] = useState<"mounting" | "loading" | "ready" | "failed">("mounting");
|
const [state, setState] = useState<"mounting" | "loading" | "ready" | "failed">("mounting");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [viewMode, setViewMode] = useState<SimulationViewMode>("visual");
|
const [viewMode, setViewMode] = useState<SimulationViewMode>("visual");
|
||||||
const [quality, setQuality] = useState<SimulationQuality>("maximum");
|
const [quality, setQuality] = useState<SimulationQuality>("high");
|
||||||
const [collisionState, setCollisionState] = useState<"idle" | "loading" | "ready" | "failed">("idle");
|
const [collisionState, setCollisionState] = useState<"idle" | "loading" | "ready" | "failed">("idle");
|
||||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||||
const [visualInverted, setVisualInverted] = useState(
|
const [visualInverted, setVisualInverted] = useState(
|
||||||
@@ -35,6 +36,8 @@ export function SimulationViewport({ project }: { project: SimulationProject })
|
|||||||
const [collisionInverted, setCollisionInverted] = useState(
|
const [collisionInverted, setCollisionInverted] = useState(
|
||||||
() => isX180Transform(project.worldManifest?.transforms.worldFromCollision),
|
() => isX180Transform(project.worldManifest?.transforms.worldFromCollision),
|
||||||
);
|
);
|
||||||
|
const [horizontalInverted, setHorizontalInverted] = useState(true);
|
||||||
|
const [verticalInverted, setVerticalInverted] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setVisualInverted(isX180Transform(project.worldManifest?.transforms.worldFromVisual));
|
setVisualInverted(isX180Transform(project.worldManifest?.transforms.worldFromVisual));
|
||||||
@@ -53,6 +56,8 @@ export function SimulationViewport({ project }: { project: SimulationProject })
|
|||||||
setError(null);
|
setError(null);
|
||||||
void runtime.mount(canvas).then(async () => {
|
void runtime.mount(canvas).then(async () => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
runtime.setCameraInversion("horizontal", cameraInversionRef.current.horizontal);
|
||||||
|
runtime.setCameraInversion("vertical", cameraInversionRef.current.vertical);
|
||||||
setState("loading");
|
setState("loading");
|
||||||
await runtime.loadWorld(manifest);
|
await runtime.loadWorld(manifest);
|
||||||
if (!cancelled) setState("ready");
|
if (!cancelled) setState("ready");
|
||||||
@@ -96,11 +101,11 @@ export function SimulationViewport({ project }: { project: SimulationProject })
|
|||||||
runtimeRef.current?.setQuality(next);
|
runtimeRef.current?.setQuality(next);
|
||||||
}}
|
}}
|
||||||
options={[
|
options={[
|
||||||
{ value: "maximum", label: "Максимум", description: "Только полный LOD 0, Retina до 2×" },
|
{ value: "maximum", label: "Максимум", description: "До 4 млн сплатов · Retina до 2× · 60 FPS" },
|
||||||
{ value: "ultra", label: "Ультра", description: "LOD 0 и Retina до 2×" },
|
{ value: "ultra", label: "Ультра", description: "До 2,5 млн · Retina до 1,5× · 60 FPS" },
|
||||||
{ value: "high", label: "Высокое", description: "LOD 1 и Retina до 1,5×" },
|
{ value: "high", label: "Высокое", description: "До 1,2 млн · Retina до 1,25× · 60 FPS" },
|
||||||
{ value: "medium", label: "Среднее", description: "LOD 2 и обычное разрешение" },
|
{ value: "medium", label: "Среднее", description: "До 700 тыс. · 1× · 45 FPS" },
|
||||||
{ value: "low", label: "Низкое", description: "LOD 3 для слабых устройств" },
|
{ value: "low", label: "Низкое", description: "До 350 тыс. · 0,75× · 30 FPS" },
|
||||||
]}
|
]}
|
||||||
minMenuWidth={230}
|
minMenuWidth={230}
|
||||||
menuWidth={230}
|
menuWidth={230}
|
||||||
@@ -133,7 +138,7 @@ export function SimulationViewport({ project }: { project: SimulationProject })
|
|||||||
</Button>
|
</Button>
|
||||||
<div className="simulation-viewport__settings-anchor">
|
<div className="simulation-viewport__settings-anchor">
|
||||||
<IconButton
|
<IconButton
|
||||||
label="Настройки системы координат"
|
label="Настройки сцены"
|
||||||
aria-controls={settingsId}
|
aria-controls={settingsId}
|
||||||
aria-expanded={settingsOpen}
|
aria-expanded={settingsOpen}
|
||||||
aria-pressed={settingsOpen}
|
aria-pressed={settingsOpen}
|
||||||
@@ -146,20 +151,24 @@ export function SimulationViewport({ project }: { project: SimulationProject })
|
|||||||
id={settingsId}
|
id={settingsId}
|
||||||
className="simulation-viewport__settings"
|
className="simulation-viewport__settings"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-label="Настройки системы координат"
|
aria-label="Настройки сцены"
|
||||||
>
|
>
|
||||||
<div className="simulation-viewport__settings-head">
|
<div className="simulation-viewport__settings-head">
|
||||||
<div>
|
<div>
|
||||||
<strong>Система координат</strong>
|
<strong>Настройки сцены</strong>
|
||||||
<span>Мир PlayCanvas · Y вверх</span>
|
<span>Gaussian-мир и камера</span>
|
||||||
</div>
|
</div>
|
||||||
<IconButton label="Закрыть настройки" onClick={() => setSettingsOpen(false)}>
|
<IconButton label="Закрыть настройки" onClick={() => setSettingsOpen(false)}>
|
||||||
<Icon name="close" size={16} />
|
<Icon name="close" size={16} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
|
<section className="simulation-viewport__settings-group">
|
||||||
|
<div className="simulation-viewport__settings-group-head">
|
||||||
|
<strong>Система координат</strong>
|
||||||
|
<span>Мир PlayCanvas · Y вверх</span>
|
||||||
|
</div>
|
||||||
<p>
|
<p>
|
||||||
Коррекция применяется к слоям независимо и не меняет координаты камеры,
|
Коррекция слоёв не меняет координаты камеры, навигации и будущей физики.
|
||||||
навигации и будущей физики.
|
|
||||||
</p>
|
</p>
|
||||||
<div className="simulation-viewport__settings-switches">
|
<div className="simulation-viewport__settings-switches">
|
||||||
<Switch
|
<Switch
|
||||||
@@ -187,6 +196,35 @@ export function SimulationViewport({ project }: { project: SimulationProject })
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
<section className="simulation-viewport__settings-group">
|
||||||
|
<div className="simulation-viewport__settings-group-head">
|
||||||
|
<strong>Управление камерой</strong>
|
||||||
|
<span>Мышь и тачпад</span>
|
||||||
|
</div>
|
||||||
|
<div className="simulation-viewport__settings-switches">
|
||||||
|
<Switch
|
||||||
|
checked={horizontalInverted}
|
||||||
|
disabled={state !== "ready"}
|
||||||
|
label="Инверсия управления по горизонтали"
|
||||||
|
onChange={(checked) => {
|
||||||
|
cameraInversionRef.current.horizontal = checked;
|
||||||
|
setHorizontalInverted(checked);
|
||||||
|
runtimeRef.current?.setCameraInversion("horizontal", checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Switch
|
||||||
|
checked={verticalInverted}
|
||||||
|
disabled={state !== "ready"}
|
||||||
|
label="Инверсия управления по вертикали"
|
||||||
|
onChange={(checked) => {
|
||||||
|
cameraInversionRef.current.vertical = checked;
|
||||||
|
setVerticalInverted(checked);
|
||||||
|
runtimeRef.current?.setCameraInversion("vertical", checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -379,7 +379,7 @@
|
|||||||
top: calc(100% + 0.65rem);
|
top: calc(100% + 0.65rem);
|
||||||
right: 0;
|
right: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
width: min(19rem, calc(100cqw - 1.3rem));
|
width: min(21rem, calc(100cqw - 1.3rem));
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
border-radius: 0.9rem;
|
border-radius: 0.9rem;
|
||||||
background: rgb(25 27 31 / 0.98);
|
background: rgb(25 27 31 / 0.98);
|
||||||
@@ -395,12 +395,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.simulation-viewport__settings-head > div,
|
.simulation-viewport__settings-head > div,
|
||||||
|
.simulation-viewport__settings-group,
|
||||||
.simulation-viewport__settings-switches {
|
.simulation-viewport__settings-switches {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.32rem;
|
gap: 0.32rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.simulation-viewport__settings strong {
|
.simulation-viewport__settings-head strong {
|
||||||
color: var(--nodedc-text-primary);
|
color: var(--nodedc-text-primary);
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
}
|
}
|
||||||
@@ -416,13 +417,29 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.simulation-viewport__settings-switches {
|
.simulation-viewport__settings-group {
|
||||||
gap: 0.55rem;
|
gap: 0.55rem;
|
||||||
border-radius: 0.72rem;
|
border-radius: 0.72rem;
|
||||||
background: rgb(255 255 255 / 0.035);
|
background: rgb(255 255 255 / 0.035);
|
||||||
padding: 0.65rem;
|
padding: 0.65rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.simulation-viewport__settings-group-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simulation-viewport__settings-group-head strong {
|
||||||
|
color: var(--nodedc-text-secondary);
|
||||||
|
font-size: 0.62rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.simulation-viewport__settings-switches {
|
||||||
|
gap: 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
.simulation-viewport__toolbar-balance {
|
.simulation-viewport__toolbar-balance {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,8 +69,13 @@ test("PlayCanvas owns the realtime scene graph without an iframe or React entity
|
|||||||
assert.match(runtime, /camera\.script\?\.create\(CameraControls/);
|
assert.match(runtime, /camera\.script\?\.create\(CameraControls/);
|
||||||
assert.match(runtime, /const HOME_POSITION = new Vec3\(0, 1, 0\)/);
|
assert.match(runtime, /const HOME_POSITION = new Vec3\(0, 1, 0\)/);
|
||||||
assert.match(runtime, /const HOME_FOCUS = new Vec3\(1, 1, 0\)/);
|
assert.match(runtime, /const HOME_FOCUS = new Vec3\(1, 1, 0\)/);
|
||||||
assert.match(runtime, /maximum: \{[^}]*lodRangeMin: 0, lodRangeMax: 0, pixelRatio: 2/);
|
assert.match(runtime, /maximum: \{[^}]*lodRangeMin: 0, lodRangeMax: 5, pixelRatio: 2, splatBudget: 4_000_000, targetFps: 60/);
|
||||||
assert.match(runtime, /frame\.mouse\[0\] \*= -1/);
|
assert.match(runtime, /app\.scene\.gsplat\.splatBudget = profile\.splatBudget/);
|
||||||
|
assert.match(runtime, /app\.autoRender = false/);
|
||||||
|
assert.match(runtime, /WEBGL_lose_context/);
|
||||||
|
assert.match(runtime, /selected\.horizontal[^\n]+frame\.mouse\[0\] \*= -1/);
|
||||||
|
assert.match(runtime, /selected\.vertical[^\n]+frame\.mouse\[1\] \*= -1/);
|
||||||
|
assert.match(runtime, /setCameraInversion/);
|
||||||
assert.match(runtime, /new Asset\([\s\S]*"container"/);
|
assert.match(runtime, /new Asset\([\s\S]*"container"/);
|
||||||
assert.match(runtime, /instantiateRenderEntity/);
|
assert.match(runtime, /instantiateRenderEntity/);
|
||||||
assert.match(runtime, /applyWorldTransform/);
|
assert.match(runtime, /applyWorldTransform/);
|
||||||
@@ -83,9 +88,11 @@ test("PlayCanvas owns the realtime scene graph without an iframe or React entity
|
|||||||
assert.match(viewport, /Collision недоступен/);
|
assert.match(viewport, /Collision недоступен/);
|
||||||
assert.match(viewport, /Для этой сборки collision GLB не был запрошен/);
|
assert.match(viewport, /Для этой сборки collision GLB не был запрошен/);
|
||||||
assert.match(viewport, /<Select[\s\S]*Качество Gaussian-сцены/);
|
assert.match(viewport, /<Select[\s\S]*Качество Gaussian-сцены/);
|
||||||
assert.match(viewport, /Максимум/);
|
assert.match(viewport, /useState<SimulationQuality>\("high"\)/);
|
||||||
assert.match(viewport, /runtimeRef\.current\?\.home\(\)/);
|
assert.match(viewport, /runtimeRef\.current\?\.home\(\)/);
|
||||||
assert.match(viewport, /Настройки системы координат/);
|
assert.match(viewport, /Настройки сцены/);
|
||||||
assert.match(viewport, /Инверсия визуального слоя/);
|
assert.match(viewport, /Инверсия визуального слоя/);
|
||||||
assert.match(viewport, /Инверсия collision-слоя/);
|
assert.match(viewport, /Инверсия collision-слоя/);
|
||||||
|
assert.match(viewport, /Инверсия управления по горизонтали/);
|
||||||
|
assert.match(viewport, /Инверсия управления по вертикали/);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user