auto-git:
[add] .prettierrc.json [add] eslint.config.js [add] index.html [add] package.json [add] playwright.config.ts [add] src/app/App.tsx [add] src/app/app.css [add] src/app/editor-store.ts [add] src/app/use-editor-store.ts [add] src/assets/.gitkeep [add] src/commands/command-history.ts [add] src/commands/command.ts [add] src/commands/set-scene-name-command.ts [add] src/core/ids.ts [add] src/core/selection.ts [add] src/core/tool-mode.ts [add] src/core/vector.ts [add] src/document/migrate-scene-document.ts [add] src/document/scene-document.ts [add] src/entities/.gitkeep [add] src/geometry/.gitkeep [add] src/main.tsx [add] src/materials/.gitkeep [add] src/runtime-three/.gitkeep [add] src/serialization/local-draft-storage.ts [add] src/serialization/scene-document-json.ts [add] src/shared-ui/Panel.tsx [add] src/viewport-three/ViewportCanvas.tsx [add] src/viewport-three/viewport-host.ts [add] src/vite-env.d.ts [add] tests/domain/create-empty-scene-document.test.ts [add] tests/domain/editor-store.test.ts [add] tests/e2e/app-smoke.e2e.ts [add] tests/serialization/scene-document-json.test.ts [add] tests/setup/vitest.setup.ts [add] tsconfig.json [add] vite.config.ts [add] vitest.config.ts
This commit is contained in:
37
src/viewport-three/ViewportCanvas.tsx
Normal file
37
src/viewport-three/ViewportCanvas.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import type { WorldSettings } from "../document/scene-document";
|
||||
|
||||
import { ViewportHost } from "./viewport-host";
|
||||
|
||||
interface ViewportCanvasProps {
|
||||
world: WorldSettings;
|
||||
}
|
||||
|
||||
export function ViewportCanvas({ world }: ViewportCanvasProps) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const hostRef = useRef<ViewportHost | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
|
||||
if (container === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewportHost = new ViewportHost();
|
||||
hostRef.current = viewportHost;
|
||||
viewportHost.mount(container, world);
|
||||
|
||||
return () => {
|
||||
viewportHost.dispose();
|
||||
hostRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
hostRef.current?.updateWorld(world);
|
||||
}, [world]);
|
||||
|
||||
return <div ref={containerRef} className="viewport-canvas" data-testid="viewport-shell" aria-label="Editor viewport" />;
|
||||
}
|
||||
100
src/viewport-three/viewport-host.ts
Normal file
100
src/viewport-three/viewport-host.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import {
|
||||
AmbientLight,
|
||||
AxesHelper,
|
||||
Color,
|
||||
DirectionalLight,
|
||||
GridHelper,
|
||||
PerspectiveCamera,
|
||||
Scene,
|
||||
Vector3,
|
||||
WebGLRenderer
|
||||
} from "three";
|
||||
|
||||
import type { WorldSettings } from "../document/scene-document";
|
||||
|
||||
export class ViewportHost {
|
||||
private readonly scene = new Scene();
|
||||
private readonly camera = new PerspectiveCamera(60, 1, 0.1, 1000);
|
||||
private readonly renderer = new WebGLRenderer({ antialias: true });
|
||||
private readonly ambientLight = new AmbientLight();
|
||||
private readonly sunLight = new DirectionalLight();
|
||||
private resizeObserver: ResizeObserver | null = null;
|
||||
private animationFrame = 0;
|
||||
private container: HTMLElement | null = null;
|
||||
|
||||
constructor() {
|
||||
this.camera.position.set(8, 8, 8);
|
||||
this.camera.lookAt(new Vector3(0, 0, 0));
|
||||
|
||||
const gridHelper = new GridHelper(40, 40, 0xc68d67, 0x4e596b);
|
||||
const axesHelper = new AxesHelper(2);
|
||||
|
||||
this.scene.add(gridHelper);
|
||||
this.scene.add(axesHelper);
|
||||
this.scene.add(this.ambientLight);
|
||||
this.scene.add(this.sunLight);
|
||||
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
}
|
||||
|
||||
mount(container: HTMLElement, world: WorldSettings) {
|
||||
this.container = container;
|
||||
container.appendChild(this.renderer.domElement);
|
||||
this.updateWorld(world);
|
||||
this.resize();
|
||||
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
this.resize();
|
||||
});
|
||||
this.resizeObserver.observe(container);
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
updateWorld(world: WorldSettings) {
|
||||
this.scene.background = new Color(world.background.colorHex);
|
||||
this.ambientLight.color.set(world.ambientLight.colorHex);
|
||||
this.ambientLight.intensity = world.ambientLight.intensity;
|
||||
this.sunLight.color.set(world.sunLight.colorHex);
|
||||
this.sunLight.intensity = world.sunLight.intensity;
|
||||
this.sunLight.position.set(world.sunLight.direction.x, world.sunLight.direction.y, world.sunLight.direction.z).normalize().multiplyScalar(18);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this.animationFrame !== 0) {
|
||||
cancelAnimationFrame(this.animationFrame);
|
||||
this.animationFrame = 0;
|
||||
}
|
||||
|
||||
this.resizeObserver?.disconnect();
|
||||
this.resizeObserver = null;
|
||||
this.renderer.dispose();
|
||||
|
||||
if (this.container !== null && this.container.contains(this.renderer.domElement)) {
|
||||
this.container.removeChild(this.renderer.domElement);
|
||||
}
|
||||
|
||||
this.container = null;
|
||||
}
|
||||
|
||||
private resize() {
|
||||
if (this.container === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const width = this.container.clientWidth;
|
||||
const height = this.container.clientHeight;
|
||||
|
||||
if (width === 0 || height === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.camera.aspect = width / height;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.renderer.setSize(width, height, false);
|
||||
}
|
||||
|
||||
private render = () => {
|
||||
this.animationFrame = window.requestAnimationFrame(this.render);
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user