diff --git a/frontend/editor3d/app/pages/PageEnigma/Editor/InfiniteGridHelper.ts b/frontend/editor3d/app/pages/PageEnigma/Editor/InfiniteGridHelper.ts new file mode 100644 index 0000000000..af41dcb356 --- /dev/null +++ b/frontend/editor3d/app/pages/PageEnigma/Editor/InfiniteGridHelper.ts @@ -0,0 +1,96 @@ +import * as THREE from "three"; + +export class InfiniteGridHelper extends THREE.Mesh { + constructor( + size1?: number, + size2?: number, + color?: THREE.Color, + distance?: number, + axes: string = "xzy", + ) { + color = color || new THREE.Color("white"); + size1 = size1 || 10; + size2 = size2 || 100; + distance = distance || 8000; + + const planeAxes = axes.substr(0, 2); + const geometry = new THREE.PlaneGeometry(2, 2, 1, 1); + + const material = new THREE.ShaderMaterial({ + side: THREE.DoubleSide, + uniforms: { + uSize1: { + value: size1, + }, + uSize2: { + value: size2, + }, + uColor: { + value: color, + }, + uDistance: { + value: distance, + }, + uFadeStrength: { + value: 1.5, + }, + uFadeStart: { + value: 0.2, + }, + }, + transparent: true, + vertexShader: ` + varying vec3 worldPosition; + uniform float uDistance; + + void main() { + vec3 pos = position.${axes} * uDistance; + pos.${planeAxes} += cameraPosition.${planeAxes}; + + worldPosition = pos; + + gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); + } + `, + fragmentShader: ` + varying vec3 worldPosition; + + uniform float uSize1; + uniform float uSize2; + uniform vec3 uColor; + uniform float uDistance; + uniform float uFadeStrength; + uniform float uFadeStart; + + float getGrid(float size) { + vec2 r = worldPosition.${planeAxes} / size; + vec2 grid = abs(fract(r - 0.5) - 0.5) / (fwidth(r) * 1.5); + float line = min(grid.x, grid.y); + return 1.0 - min(line, 1.0); + } + + void main() { + float distFromCamera = length(cameraPosition.${planeAxes} - worldPosition.${planeAxes}) / uDistance; + float fadeOut = 1.0 - smoothstep(uFadeStart, 1.0, distFromCamera); + fadeOut = pow(fadeOut, uFadeStrength); + + float g1 = getGrid(uSize1); + float g2 = getGrid(uSize2); + + float grid = mix(g2 * 0.3, g1, g1); + + gl_FragColor = vec4(uColor.rgb, grid * fadeOut); + gl_FragColor.a = mix(0.5 * gl_FragColor.a, gl_FragColor.a, g2); + + if (gl_FragColor.a <= 0.0) discard; + } + `, + extensions: { + derivatives: true, + }, + }); + + super(geometry, material); + this.frustumCulled = false; + } +} diff --git a/frontend/editor3d/app/pages/PageEnigma/Editor/scene.ts b/frontend/editor3d/app/pages/PageEnigma/Editor/scene.ts index 0ca247e7d9..0d32a1645f 100644 --- a/frontend/editor3d/app/pages/PageEnigma/Editor/scene.ts +++ b/frontend/editor3d/app/pages/PageEnigma/Editor/scene.ts @@ -15,10 +15,13 @@ import { ClipGroup, ClipType } from "~/enums"; import { ClipUI } from "../clips/clip_ui"; import { GetCdnOrigin } from "~/api/GetCdnOrigin"; import { GetFrontendEnvironment } from "~/Classes/GetFrontendEnvironment"; +import { gridVisibility } from "../signals/engine"; +import { InfiniteGridHelper } from "./InfiniteGridHelper"; class Scene { name: string; - gridHelper: THREE.GridHelper | undefined; + gridHelper: InfiniteGridHelper | undefined; + groundPlane: THREE.Mesh | undefined; scene: THREE.Scene; hot_items: THREE.Object3D[] | undefined; @@ -87,6 +90,19 @@ class Scene { this.helper = new MMDAnimationHelper({ afterglow: 0.0 }); this.scene.userData["helper"] = this.helper; + + // Subscribe to grid visibility changes + gridVisibility.subscribe((isVisible) => { + if (this.gridHelper) { + if (isVisible) { + if (!this.scene.children.includes(this.gridHelper)) { + this.scene.add(this.gridHelper); + } + } else { + this.scene.remove(this.gridHelper); + } + } + }); } clear() { @@ -357,9 +373,9 @@ class Scene { const camera_position = new THREE.Vector3(0, 0.6, 1.5); // TODO(bt,2025-02-10): Make the local dev assets more configurable or seedable. - const camera_id = GetFrontendEnvironment().getIsLocalDev() ? - "m_4djqzq89mbn6sdbbqm25ks6drhr55k" : // Local development - "m_cxh4asqhapdz10j880755dg4yevshb"; // Production + const camera_id = GetFrontendEnvironment().getIsLocalDev() + ? "m_4djqzq89mbn6sdbbqm25ks6drhr55k" // Local development + : "m_cxh4asqhapdz10j880755dg4yevshb"; // Production const camera_obj = await this.loadGlbWithPlaceholder( camera_id, @@ -848,16 +864,40 @@ class Scene { } _createGrid() { - const size = 25; - const divisions = 50; - this.gridHelper = new THREE.GridHelper( - size, - divisions, - new THREE.Color("rgb(199,195,195)"), - new THREE.Color("rgb(161,157,157)"), - ); + // Create visual infinite grid + const size1 = 0.5; // Primary grid + const size2 = 2.5; // Secondary grid + const color = new THREE.Color(0x444444); + const distance = 80; + + this.gridHelper = new InfiniteGridHelper(size1, size2, color, distance); this.gridHelper.layers.set(1); // Enable default layer this.scene.add(this.gridHelper); + + // Create invisible ground plane for proper object placement + const planeGeometry = new THREE.PlaneGeometry(50, 50); // Same as original grid size + const planeMaterial = new THREE.MeshBasicMaterial({ + visible: false, + side: THREE.DoubleSide, + }); + this.groundPlane = new THREE.Mesh(planeGeometry, planeMaterial); + this.groundPlane.rotation.x = -Math.PI / 2; + this.groundPlane.name = "GroundPlane"; + this.groundPlane.layers.set(2); // Put in layer 2 to avoid selection + this.groundPlane.userData["selectable"] = false; // Mark as non-selectable + this.scene.add(this.groundPlane); + } + + // Update grid visibility based on the signal + updateGridVisibility() { + if (gridVisibility.value) { + if (!this.scene.children.includes(this.gridHelper)) { + this.scene.add(this.gridHelper); + } + } else { + this.scene.remove(this.gridHelper); + } + // Keep the ground plane regardless of grid visibility } } diff --git a/frontend/editor3d/app/pages/PageEnigma/comps/PromptBox/PromptBox.tsx b/frontend/editor3d/app/pages/PageEnigma/comps/PromptBox/PromptBox.tsx index dfbac8f424..e2b870c4e9 100644 --- a/frontend/editor3d/app/pages/PageEnigma/comps/PromptBox/PromptBox.tsx +++ b/frontend/editor3d/app/pages/PageEnigma/comps/PromptBox/PromptBox.tsx @@ -20,6 +20,7 @@ import { faRectangleVertical, faSquare, faRectangle, + faTableCellsLarge, } from "@fortawesome/pro-regular-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -31,6 +32,8 @@ import { disableHotkeyInput, DomLevels, enableHotkeyInput, + gridVisibility, + setGridVisibility, } from "~/pages/PageEnigma/signals"; import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames"; import Queue from "~/pages/PageEnigma/Queue/Queue"; @@ -45,7 +48,6 @@ interface ReferenceImage { mediaToken: string; } import { EngineApi } from "~/Classes/ApiManager/EngineApi"; -import { scene } from "~/signals/scene"; import { UploaderStates } from "~/enums/UploaderStates"; import { CameraSettingsModal } from "../CameraSettingsModal"; @@ -111,6 +113,7 @@ export const PromptBox = () => { [], ); const [activeLibraryTab, setActiveLibraryTab] = useState("my-media"); + const [isGridVisible, setIsGridVisible] = useState(true); useEffect(() => { if (textareaRef.current) { @@ -138,6 +141,10 @@ export const PromptBox = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [cameraAspectRatio.value]); + useEffect(() => { + setGridVisibility(isGridVisible); + }, [isGridVisible]); + const handleCameraSelect = (selectedItem: PopoverItem) => { setCameraList((prev) => prev.map((item) => ({ @@ -600,6 +607,19 @@ export const PromptBox = () => { onClick={() => setUseSystemPrompt(!useSystemPrompt)} /> + + setGridVisibility(!gridVisibility.value)} + /> +
{ cameraAspectRatio.value = newAspectRatio; }; export const previewSrc = signal(""); + +export const gridVisibility = signal(true); + +export const setGridVisibility = (isVisible: boolean) => { + gridVisibility.value = isVisible; +};