diff --git a/app/page.tsx b/app/page.tsx index a480a9c..85895fb 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -25,8 +25,8 @@ export default function Home() {
-
-
+
+
diff --git a/app/three/SceneView.ts b/app/three/SceneView.ts index db71f91..f627afc 100644 --- a/app/three/SceneView.ts +++ b/app/three/SceneView.ts @@ -19,7 +19,10 @@ import { Orientation, buildClippingplanes, } from "./utils/build-clipping-planes"; -import { buildCoordinateGrid } from "./utils/build-coordinate-grid"; +import { + buildCoordinateGrid, + buildHeightGrid, +} from "./utils/build-coordinate-grid"; import { DragControls, OBJExporter, @@ -371,9 +374,15 @@ async function init(container: HTMLElement, modelId = MODEL_ID) { // Add a coordinate grid to the scene const { gridHelper, annotations } = buildCoordinateGrid(extent); + const { heightGridHelper, heightAnnotations } = buildHeightGrid(extent); const annotationsGroup = new Group(); annotationsGroup.name = "coordinate-grid"; - annotationsGroup.add(...annotations, gridHelper); + annotationsGroup.add( + ...annotations, + gridHelper, + ...heightAnnotations, + heightGridHelper + ); annotationsGroup.visible = false; scene.add(annotationsGroup); diff --git a/app/three/utils/build-coordinate-grid.ts b/app/three/utils/build-coordinate-grid.ts index f697cfe..abf686c 100644 --- a/app/three/utils/build-coordinate-grid.ts +++ b/app/three/utils/build-coordinate-grid.ts @@ -1,118 +1,157 @@ import { + BufferGeometry, CanvasTexture, - GridHelper, + Group, + Line, + LineBasicMaterial, Sprite, SpriteMaterial, - Vector4, + Vector3, } from "three"; import { Extent } from "./build-scene"; -import { getCenter3D } from "./utils"; enum Orientation { - Horizontal, - Vertical, + X, + Y, + Z, } export function buildCoordinateGrid(extent: Extent) { - const center = getCenter3D(extent); // Calculate the width and height of the grid const gridWidth = extent.xmax - extent.xmin; const gridHeight = extent.ymax - extent.ymin; - // Decide on the number of divisions (e.g., 20 divisions along each axis) - const divisions = 20; + // Decide on the number of divisions + const divisions = 10; - // Create a grid helper with the calculated grid size and divisions - const gridHelper = new GridHelper(Math.max(gridWidth, gridHeight), divisions); - - // Position the grid in the scene to match the given extent - gridHelper.position.set(center.x, center.y, 0); - - // Rotate the grid to align with the XY-plane - gridHelper.rotation.x = Math.PI / 2; - - // Retrieve the geometry of the grid helper - const geometry = gridHelper.geometry; - - const positionAttr = geometry.getAttribute("position"); - const startingPointsHorizontal = []; - const startingPointsVertical = []; - for (let i = 0; i < positionAttr.count; i++) { - const x = positionAttr.getX(i); - const z = positionAttr.getZ(i); - const v = new Vector4(x + center.x, z + center.y, 0, 1); - - if (i % 4 === 0) { - startingPointsVertical.push(v); - } else if (i % 2 == 0) { - startingPointsHorizontal.push(v); - } + const xOffset = gridWidth / divisions; + let x = extent.xmin - xOffset; + const xPairs = []; + for (let i = 0; i < divisions + 1; i++) { + x += xOffset; + const start = new Vector3(x, extent.ymin, extent.zmin); + const end = new Vector3(x, extent.ymax, extent.zmin); + xPairs.push([start, end]); } + const xLines = createLines(xPairs); + + const yOffset = gridHeight / divisions; + let y = extent.ymin - yOffset; + const yPairs = []; + for (let i = 0; i < divisions + 1; i++) { + y += yOffset; + const start = new Vector3(extent.xmin, y, extent.zmin); + const end = new Vector3(extent.xmax, y, extent.zmin); + yPairs.push([start, end]); + } + const yLines = createLines(yPairs); const annotations = []; - for (const point of startingPointsHorizontal) { - const label = createLabel( - `${point.x.toFixed(2)}`, - point, - Orientation.Horizontal - ); + for (let i = 0; i < xPairs.length - 1; i++) { + const [start, _] = xPairs[i]; + const label = createLabel(`${start.x.toFixed(0)}m`, start, Orientation.X); annotations.push(label); } - for (const point of startingPointsVertical) { - const label = createLabel( - `${point.y.toFixed(2)}`, - point, - Orientation.Vertical - ); + for (let i = 0; i < yPairs.length - 1; i++) { + const [start, _] = yPairs[i]; + const label = createLabel(`${start.y.toFixed(0)}m`, start, Orientation.Y); annotations.push(label); } + const gridHelper = new Group(); + gridHelper.add(...xLines, ...yLines); + return { gridHelper, annotations }; } +export function buildHeightGrid(extent: Extent) { + const gridHeight = extent.zmax - extent.zmin; + + const divisions = 5; + const offset = gridHeight / divisions; + + let z = extent.zmin - offset; + const pointPairs = []; + for (let i = 0; i < divisions + 1; i++) { + z += offset; + const start = new Vector3(extent.xmax, extent.ymin, z); + const end = new Vector3(extent.xmax, extent.ymax, z); + pointPairs.push([start, end]); + } + const lines = createLines(pointPairs); + + const annotations = []; + for (const pointPair of pointPairs) { + const start = pointPair[0]; + const label = createLabel(`${start.z.toFixed(0)}m`, start, Orientation.Z); + annotations.push(label); + } + + const gridHelper = new Group(); + gridHelper.add(...lines); + + return { heightGridHelper: gridHelper, heightAnnotations: annotations }; +} + // Function to create annotation (sprite with text) function createLabel( text: string, - position: Vector4, + position: Vector3, orientation: Orientation ) { const spriteMaterial = new SpriteMaterial({ - map: new CanvasTexture(generateTextCanvas(text, orientation)), // Create text texture + map: new CanvasTexture(generateTextCanvas(text)), // Create text texture transparent: true, }); const sprite = new Sprite(spriteMaterial); // Set position according to axis orientation - if (orientation === Orientation.Horizontal) { - sprite.position.set(position.x + 1000, position.y - 1500, position.z + 500); + if (orientation === Orientation.X) { + sprite.position.set(position.x + 500, position.y - 1500, position.z + 500); + } else if (orientation === Orientation.Y) { + sprite.position.set(position.x - 3000, position.y + 500, position.z + 500); } else { - sprite.position.set(position.x, position.y - 500, position.z + 500); + sprite.position.set(position.x + 3000, position.y, position.z + 500); } - sprite.scale.set(5000, 2500, 1); // Scale the sprite to make the text readable + sprite.scale.set(5000, 2500, 1); return sprite; } // Function to generate a text canvas for the annotation -function generateTextCanvas(text: string, orientation: Orientation) { +function generateTextCanvas(text: string) { const canvas = document.createElement("canvas"); const context = canvas.getContext("2d"); if (context) { - canvas.width = 800; - canvas.height = 160; + const width = 800; + const height = 200; + canvas.width = width; + canvas.height = height; // Set the text style - context.font = "45px Arial"; + context.font = `${height - 30}px Arial`; context.fillStyle = "black"; - if (orientation === Orientation.Horizontal) { - //context.fillText(text, 300, 160); - context.fillText(text, 100, 90); - } else { - context.fillText(text, 100, 90); - } + context.fillText(text, 0, height - 15); } return canvas; } + +function createLines(pointPairs: Vector3[][]) { + const lines = []; + + for (const pair of pointPairs) { + const geometry = new BufferGeometry().setFromPoints(pair); + + // Line material + const material = new LineBasicMaterial({ color: 0x444444 }); + + // Create line + const line = new Line(geometry, material); + lines.push(line); + } + + return lines; +} diff --git a/app/three/utils/build-scene.ts b/app/three/utils/build-scene.ts index 61bef77..505ad5e 100644 --- a/app/three/utils/build-scene.ts +++ b/app/three/utils/build-scene.ts @@ -6,6 +6,13 @@ import { DirectionalLight, Group, Object3D, + AxesHelper, + OrthographicCamera, + Camera, + CanvasTexture, + SpriteMaterial, + Sprite, + Euler, } from "three"; import { OrbitControls } from "three/addons/controls/OrbitControls.js"; @@ -24,6 +31,9 @@ let controls: OrbitControls; let renderer: WebGLRenderer; let camera: PerspectiveCamera; let scene: Scene; +let axesHelper: AxesHelper; +let uiCamera: Camera; +let uiScene: Scene; export function buildScene(container: HTMLElement, extent: Extent) { const maxSize = getMaxSize(extent); const center = getCenter3D(extent); @@ -61,6 +71,7 @@ export function buildScene(container: HTMLElement, extent: Extent) { controls = new OrbitControls(camera, renderer.domElement); controls.target.set(center.x, center.y, center.z); // Focus on the center controls.enableDamping = true; // Smooth camera movement + controls.dampingFactor = 0.1; controls.maxDistance = maxSize * 3; controls.minDistance = maxSize / 5; controls.update(); @@ -73,6 +84,27 @@ export function buildScene(container: HTMLElement, extent: Extent) { // Add lights to the scene buildDefaultLights(scene, extent); + uiScene = new Scene(); + + // Create an orthographic camera + uiCamera = new OrthographicCamera(-1, 1, 1, -1, 0.1, 10); + uiCamera.up.set(0, 0, 1); + uiCamera.position.z = 2; + + // Create the AxesHelper (small size) + axesHelper = new AxesHelper(0.1); + axesHelper.position.set(-0.9, -0.8, 0); + + const xLabel = createTextSprite("X", "red"); + const yLabel = createTextSprite("Y", "green"); + const zLabel = createTextSprite("Z", "blue"); + xLabel.position.set(0.125, 0, 0); + yLabel.position.set(0, 0.125, 0); + zLabel.position.set(0, 0, 0.125); + + axesHelper.add(xLabel, yLabel, zLabel); + uiScene.add(axesHelper); + return { renderer, scene, camera, controls }; } @@ -88,7 +120,16 @@ function onWindowResize(container: HTMLElement) { } function animate() { + // axesHelper.quaternion.copy(camera.quaternion); + let rot = new Euler(); + rot.x = -camera.rotation.x; + rot.y = camera.rotation.y; + rot.z = camera.rotation.z; + axesHelper.setRotationFromEuler(rot); + renderer.autoClear = true; renderer.render(scene, camera); + renderer.autoClear = false; + renderer.render(uiScene, uiCamera); // Render UI scene // required if controls.enableDamping or controls.autoRotate are set to true controls.update(); @@ -131,3 +172,25 @@ function buildDefaultLights(scene: Scene, extent: Extent) { lightsGroup.add(...lights); scene.add(lightsGroup); } + +function createTextSprite(text: string, color = "white") { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + + canvas.width = 256; + canvas.height = 128; + + if (ctx) { + ctx.fillStyle = color; + ctx.font = "24px Arial"; + ctx.textAlign = "center"; + ctx.fillText(text, canvas.width / 2, canvas.height / 2); + } + + const texture = new CanvasTexture(canvas); + const material = new SpriteMaterial({ map: texture }); + const sprite = new Sprite(material); + sprite.scale.set(0.3, 0.15, 1); + + return sprite; +}