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)}
/>
+