diff --git a/frontend/src/app/pages/particles/components/render-container/render-container.component.html b/frontend/src/app/pages/particles/components/render-container/render-container.component.html
index cc5392c..d90af58 100644
--- a/frontend/src/app/pages/particles/components/render-container/render-container.component.html
+++ b/frontend/src/app/pages/particles/components/render-container/render-container.component.html
@@ -5,6 +5,10 @@
Opacity
+
+ Grid density
+
+
+
+
@if (isPlaneLocked) {
diff --git a/frontend/src/app/pages/particles/components/render-container/render-container.component.ts b/frontend/src/app/pages/particles/components/render-container/render-container.component.ts
index 99abd4b..defaa73 100644
--- a/frontend/src/app/pages/particles/components/render-container/render-container.component.ts
+++ b/frontend/src/app/pages/particles/components/render-container/render-container.component.ts
@@ -86,6 +86,23 @@ export class RenderContainerComponent implements AfterViewInit, OnDestroy {
return this.intersectionPlaneService.currentOpacity;
}
+ // Grid proxies
+ public get gridVisible(): boolean {
+ return this.intersectionPlaneService.getGridVisible();
+ }
+
+ public set gridVisible(v: boolean) {
+ this.intersectionPlaneService.setGridVisible(v);
+ }
+
+ public get gridDensity(): number {
+ return this.intersectionPlaneService.getGridDensity();
+ }
+
+ public set gridDensity(d: number) {
+ this.intersectionPlaneService.setGridDensity(d);
+ }
+
/**
* Toggle the plane locked state
*/
diff --git a/frontend/src/app/pages/particles/services/intersection-plane.service.ts b/frontend/src/app/pages/particles/services/intersection-plane.service.ts
index 12e8b22..c287bbe 100644
--- a/frontend/src/app/pages/particles/services/intersection-plane.service.ts
+++ b/frontend/src/app/pages/particles/services/intersection-plane.service.ts
@@ -28,6 +28,11 @@ export class IntersectionPlaneService {
private planeLocked: boolean = false;
private opacity: number = 0.05;
+ // Grid overlay
+ private gridHelper?: THREE.GridHelper;
+ private gridVisible: boolean = true;
+ private gridDensity: number = 4;
+
// Emits whenever plane position, orientation, or lock-affecting orientation updates change visuals
public readonly planeChanged$ = new Subject();
private lastPlaneSignature: string | null = null;
@@ -35,6 +40,73 @@ export class IntersectionPlaneService {
constructor(private rendererService: RendererService) {
}
+ /**
+ * Creates or updates the grid helper attached to the intersection plane
+ * without affecting raycasting/placement.
+ */
+ private createOrUpdateGrid(): void {
+ if (!this.intersectionPlane) return;
+
+ if (this.gridHelper) {
+ this.intersectionPlane.remove(this.gridHelper);
+ (this.gridHelper.geometry as THREE.BufferGeometry).dispose();
+ if (this.gridHelper.material.dispose) {
+ this.gridHelper.material.dispose();
+ }
+ this.gridHelper = undefined;
+ }
+
+ const size = 5;
+ const divisions = Math.max(1, Math.floor(size * this.gridDensity));
+
+ this.gridHelper = new THREE.GridHelper(size, divisions, 0x888888, 0xcccccc);
+
+ this.gridHelper.rotation.x = Math.PI / 2;
+ this.gridHelper.position.z -= 0.005;
+
+ this.gridHelper.renderOrder = 2;
+ const gridMat = this.gridHelper.material as THREE.Material | THREE.Material[];
+ if (Array.isArray(gridMat)) {
+ gridMat.forEach(material => {
+ material.transparent = true;
+ material.depthWrite = false;
+ if (material.opacity !== undefined) {
+ material.opacity = 0.25;
+ }
+ });
+ } else {
+ gridMat.transparent = true;
+ gridMat.depthWrite = false;
+ gridMat.opacity = 0.25;
+ }
+
+ this.gridHelper.raycast = () => {
+ };
+
+ this.gridHelper.visible = this.gridVisible;
+ this.intersectionPlane.add(this.gridHelper);
+ }
+
+ public setGridVisible(visible: boolean): void {
+ this.gridVisible = visible;
+ if (this.gridHelper) this.gridHelper.visible = visible;
+ }
+
+ public getGridVisible(): boolean {
+ return this.gridVisible;
+ }
+
+ public setGridDensity(density: number): void {
+ this.gridDensity = Math.max(1, Math.min(64, Math.floor(density)));
+ if (this.intersectionPlane) {
+ this.createOrUpdateGrid();
+ }
+ }
+
+ public getGridDensity(): number {
+ return this.gridDensity;
+ }
+
/**
* Creates the intersection plane and adds it to the scene
*/
@@ -51,6 +123,10 @@ export class IntersectionPlaneService {
this.intersectionPlane.position.z = 0;
// Center the plane vertically with the player (player is about 2 blocks tall)
this.intersectionPlane.position.y = 1;
+
+ // Add grid overlay as a child so it follows rotation/position
+ this.createOrUpdateGrid();
+
this.rendererService.scene.add(this.intersectionPlane);
this.intersectionPlane.renderOrder = 1;