drag and drop route markers
This commit is contained in:
@@ -1,71 +1,114 @@
|
|||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
|
|
||||||
export class InputManager {
|
export class InputManager {
|
||||||
constructor(camera, domElement, scene) {
|
constructor(camera, domElement, scene, controls) {
|
||||||
this.camera = camera;
|
this.camera = camera;
|
||||||
this.domElement = domElement;
|
this.domElement = domElement;
|
||||||
this.scene = scene;
|
this.scene = scene;
|
||||||
|
this.controls = controls; // Need access to controls to disable them during drag
|
||||||
|
|
||||||
this.raycaster = new THREE.Raycaster();
|
this.raycaster = new THREE.Raycaster();
|
||||||
this.mouse = new THREE.Vector2();
|
this.mouse = new THREE.Vector2();
|
||||||
|
|
||||||
// State for Pan detection
|
// Interaction State
|
||||||
this.downPosition = new THREE.Vector2();
|
this.downPosition = new THREE.Vector2();
|
||||||
this.upPosition = new THREE.Vector2();
|
this.dragObject = null; // The object currently being dragged (marker)
|
||||||
this.isDragging = false;
|
this.isPanning = false;
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
this.onClick = null; // Function(point, intersectionObject)
|
this.onClick = null; // (point, object) -> void
|
||||||
|
this.onDrag = null; // (object, newPoint) -> void
|
||||||
|
this.onDragEnd = null; // () -> void
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.domElement.addEventListener('pointerdown', this.onPointerDown.bind(this));
|
this.domElement.addEventListener('pointerdown', this.onPointerDown.bind(this));
|
||||||
|
this.domElement.addEventListener('pointermove', this.onPointerMove.bind(this));
|
||||||
this.domElement.addEventListener('pointerup', this.onPointerUp.bind(this));
|
this.domElement.addEventListener('pointerup', this.onPointerUp.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
onPointerDown(event) {
|
onPointerDown(event) {
|
||||||
if (event.button !== 0) return; // Only left click
|
if (event.button !== 0) return; // Left click only
|
||||||
|
|
||||||
|
// Record start position for Pan detection
|
||||||
this.downPosition.set(event.clientX, event.clientY);
|
this.downPosition.set(event.clientX, event.clientY);
|
||||||
this.isDragging = false;
|
this.isPanning = false;
|
||||||
|
|
||||||
|
// Raycast to see what we hit (Marker vs Ground)
|
||||||
|
const hit = this.raycast(event);
|
||||||
|
|
||||||
|
if (hit) {
|
||||||
|
// Case A: We hit a Marker -> Start Dragging
|
||||||
|
if (hit.object.userData.isMarker) {
|
||||||
|
this.dragObject = hit.object;
|
||||||
|
this.controls.enabled = false; // Disable camera orbit
|
||||||
|
this.domElement.style.cursor = 'grabbing';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPointerMove(event) {
|
||||||
|
// Case A: Dragging a Marker
|
||||||
|
if (this.dragObject) {
|
||||||
|
// Raycast against the GROUND to find where we are dragging to
|
||||||
|
const hit = this.raycastGround(event);
|
||||||
|
if (hit && this.onDrag) {
|
||||||
|
this.onDrag(this.dragObject, hit.point);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case B: Detecting Pan
|
||||||
|
// If mouse is down and moving, check distance
|
||||||
|
// (We don't need continuous logic here, just the final check in pointerUp is usually enough,
|
||||||
|
// but for "floating pointer" later we'd use this.)
|
||||||
}
|
}
|
||||||
|
|
||||||
onPointerUp(event) {
|
onPointerUp(event) {
|
||||||
if (event.button !== 0) return;
|
if (event.button !== 0) return;
|
||||||
|
|
||||||
this.upPosition.set(event.clientX, event.clientY);
|
// 1. If we were dragging a marker, stop now.
|
||||||
|
if (this.dragObject) {
|
||||||
// Calculate distance moved
|
this.dragObject = null;
|
||||||
const distance = this.downPosition.distanceTo(this.upPosition);
|
this.controls.enabled = true; // Re-enable camera
|
||||||
|
this.domElement.style.cursor = 'auto';
|
||||||
// Threshold (pixels): If moved more than 3px, it's a pan, not a click
|
if (this.onDragEnd) this.onDragEnd();
|
||||||
if (distance > 3) {
|
return; // Don't trigger a click
|
||||||
this.isDragging = true;
|
|
||||||
return; // Ignore
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.handleClick(event);
|
// 2. Check if it was a Camera Pan (move > 3px)
|
||||||
|
const upPosition = new THREE.Vector2(event.clientX, event.clientY);
|
||||||
|
if (this.downPosition.distanceTo(upPosition) > 3) {
|
||||||
|
return; // It was a pan, ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. It was a clean Click (Place new node)
|
||||||
|
const hit = this.raycast(event);
|
||||||
|
if (hit && hit.object.name === "GROUND" && this.onClick) {
|
||||||
|
this.onClick(hit.point, hit.object);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClick(event) {
|
// --- Helpers ---
|
||||||
// 1. Normalize Mouse
|
|
||||||
this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
|
||||||
this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
|
||||||
|
|
||||||
// 2. Raycast
|
getMouse(event) {
|
||||||
this.raycaster.setFromCamera(this.mouse, this.camera);
|
const r = this.domElement.getBoundingClientRect();
|
||||||
|
const x = ((event.clientX - r.left) / r.width) * 2 - 1;
|
||||||
|
const y = -((event.clientY - r.top) / r.height) * 2 + 1;
|
||||||
|
return new THREE.Vector2(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
raycast(event) {
|
||||||
|
this.raycaster.setFromCamera(this.getMouse(event), this.camera);
|
||||||
|
// Intersection order: Markers (sorted by dist) -> Ground
|
||||||
const intersects = this.raycaster.intersectObjects(this.scene.children, true);
|
const intersects = this.raycaster.intersectObjects(this.scene.children, true);
|
||||||
|
// Return first valid hit (Marker or Ground)
|
||||||
|
return intersects.find(obj => obj.object.name === "GROUND" || obj.object.userData.isMarker);
|
||||||
|
}
|
||||||
|
|
||||||
if (intersects.length > 0) {
|
raycastGround(event) {
|
||||||
// Find the first relevant hit (Ground or Markers)
|
this.raycaster.setFromCamera(this.getMouse(event), this.camera);
|
||||||
// For now, we prioritize Markers, then Ground
|
const intersects = this.raycaster.intersectObjects(this.scene.children, true);
|
||||||
const hit = intersects.find(obj =>
|
return intersects.find(obj => obj.object.name === "GROUND");
|
||||||
obj.object.name === "GROUND" || obj.object.userData.isMarker
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hit && this.onClick) {
|
|
||||||
this.onClick(hit.point, hit.object);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,46 +11,37 @@ export class RouteManager {
|
|||||||
this.currentRouteNodes = [];
|
this.currentRouteNodes = [];
|
||||||
|
|
||||||
// Visuals
|
// Visuals
|
||||||
this.markers = [];
|
this.markers = []; // Array of Meshes. index matches currentRouteNodes
|
||||||
this.pathMesh = null;
|
this.pathMesh = null;
|
||||||
|
|
||||||
// Constants
|
this.ROAD_OFFSET = 2.5; // Meters
|
||||||
this.ROAD_OFFSET = 3.0; // Meters to right
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initGraph(data) {
|
initGraph(data) {
|
||||||
this.graphData = data;
|
this.graphData = data;
|
||||||
|
|
||||||
// Prepare Adjacency List (mirrors previous logic)
|
|
||||||
// IMPORTANT: Fix coordinates here once, so logic uses correct Z
|
|
||||||
this.graphData.adjacency = {};
|
this.graphData.adjacency = {};
|
||||||
|
|
||||||
// 1. Flip Y to Z for Nodes
|
// 1. Flip Coordinates (Data is +Y North, 3D is -Z North)
|
||||||
for (let key in this.graphData.nodes) {
|
for (let key in this.graphData.nodes) {
|
||||||
this.graphData.nodes[key].y = -this.graphData.nodes[key].y;
|
this.graphData.nodes[key].y = -this.graphData.nodes[key].y;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Process Edges
|
// 2. Build Adjacency
|
||||||
this.graphData.edges.forEach((edge, index) => {
|
this.graphData.edges.forEach((edge, index) => {
|
||||||
// Flip geometry points
|
// Flip edge geometry
|
||||||
if (edge.points) {
|
if (edge.points) edge.points.forEach(p => { p[1] = -p[1]; });
|
||||||
edge.points.forEach(p => { p[1] = -p[1]; });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Forward
|
||||||
if (!this.graphData.adjacency[edge.u]) this.graphData.adjacency[edge.u] = [];
|
if (!this.graphData.adjacency[edge.u]) this.graphData.adjacency[edge.u] = [];
|
||||||
this.graphData.adjacency[edge.u].push({
|
this.graphData.adjacency[edge.u].push({
|
||||||
to: edge.v,
|
to: edge.v, cost: edge.length || 1, edgeIndex: index
|
||||||
cost: edge.length || 1,
|
|
||||||
edgeIndex: index
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Reverse (if not oneway)
|
||||||
if (!edge.oneway) {
|
if (!edge.oneway) {
|
||||||
if (!this.graphData.adjacency[edge.v]) this.graphData.adjacency[edge.v] = [];
|
if (!this.graphData.adjacency[edge.v]) this.graphData.adjacency[edge.v] = [];
|
||||||
this.graphData.adjacency[edge.v].push({
|
this.graphData.adjacency[edge.v].push({
|
||||||
to: edge.u,
|
to: edge.u, cost: edge.length || 1, edgeIndex: index, isReverse: true
|
||||||
cost: edge.length || 1,
|
|
||||||
edgeIndex: index,
|
|
||||||
isReverse: true
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -60,15 +51,12 @@ export class RouteManager {
|
|||||||
// Interaction Methods
|
// Interaction Methods
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when user clicks the map. Adds a node to the route.
|
|
||||||
*/
|
|
||||||
addNodeByWorldPosition(vector3) {
|
addNodeByWorldPosition(vector3) {
|
||||||
if (!this.graphData) return;
|
if (!this.graphData) return;
|
||||||
|
|
||||||
const nodeId = this.findNearestNode(vector3.x, vector3.z);
|
const nodeId = this.findNearestNode(vector3.x, vector3.z);
|
||||||
|
if (nodeId === null) return;
|
||||||
|
|
||||||
// Prevent adding same node twice in a row
|
// Don't add duplicate adjacent nodes
|
||||||
if (this.currentRouteNodes.length > 0 &&
|
if (this.currentRouteNodes.length > 0 &&
|
||||||
this.currentRouteNodes[this.currentRouteNodes.length - 1] === nodeId) {
|
this.currentRouteNodes[this.currentRouteNodes.length - 1] === nodeId) {
|
||||||
return;
|
return;
|
||||||
@@ -76,24 +64,129 @@ export class RouteManager {
|
|||||||
|
|
||||||
this.currentRouteNodes.push(nodeId);
|
this.currentRouteNodes.push(nodeId);
|
||||||
|
|
||||||
// Visuals
|
// Add new marker
|
||||||
this.addMarker(nodeId);
|
this.addMarkerVisual(nodeId);
|
||||||
|
|
||||||
|
// Update path
|
||||||
this.updatePathVisuals();
|
this.updatePathVisuals();
|
||||||
}
|
}
|
||||||
|
|
||||||
resetRoute() {
|
/**
|
||||||
this.currentRouteNodes = [];
|
* Called while dragging a marker.
|
||||||
// Clear Visuals
|
* Updates the node at markerIndex to the nearest graph node at worldPoint.
|
||||||
this.markers.forEach(m => this.scene.remove(m));
|
*/
|
||||||
this.markers = [];
|
dragNode(markerObject, worldPoint) {
|
||||||
if (this.pathMesh) {
|
if (!this.graphData) return;
|
||||||
this.scene.remove(this.pathMesh);
|
|
||||||
this.pathMesh = null;
|
// 1. Identify which node index this marker represents
|
||||||
|
const index = this.markers.indexOf(markerObject);
|
||||||
|
if (index === -1) return;
|
||||||
|
|
||||||
|
// 2. Find nearest node to new mouse position
|
||||||
|
const newNodeId = this.findNearestNode(worldPoint.x, worldPoint.z);
|
||||||
|
|
||||||
|
// 3. Optimization: Only update if the node ID actually changed
|
||||||
|
if (this.currentRouteNodes[index] !== newNodeId) {
|
||||||
|
|
||||||
|
this.currentRouteNodes[index] = newNodeId;
|
||||||
|
|
||||||
|
// Update Marker Visual Position
|
||||||
|
const nodeData = this.graphData.nodes[newNodeId];
|
||||||
|
markerObject.position.set(nodeData.x, 2, nodeData.y);
|
||||||
|
markerObject.userData.nodeId = newNodeId; // Keep sync
|
||||||
|
|
||||||
|
// Recalculate Path
|
||||||
|
this.updatePathVisuals();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// Logic & Algorithms
|
// Visual Logic
|
||||||
|
// ============================
|
||||||
|
|
||||||
|
updatePathVisuals() {
|
||||||
|
// Need 2+ nodes to make a path
|
||||||
|
if (this.currentRouteNodes.length < 2) {
|
||||||
|
if (this.pathMesh) {
|
||||||
|
this.scene.remove(this.pathMesh);
|
||||||
|
this.pathMesh = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Calculate Geometry
|
||||||
|
let fullPathPoints = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < this.currentRouteNodes.length - 1; i++) {
|
||||||
|
const start = this.currentRouteNodes[i];
|
||||||
|
const end = this.currentRouteNodes[i + 1];
|
||||||
|
|
||||||
|
// Run A* for this segment
|
||||||
|
const segmentEdges = this.computePathAStar(start, end);
|
||||||
|
|
||||||
|
if (!segmentEdges) {
|
||||||
|
// No path found (disconnected graph?), just draw straight line or skip
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process Geometry
|
||||||
|
segmentEdges.forEach(step => {
|
||||||
|
const rawPoints = step.edgeData.points;
|
||||||
|
let segmentPoints = rawPoints.map(p => new THREE.Vector2(p[0], p[1]));
|
||||||
|
if (step.isReverse) segmentPoints.reverse();
|
||||||
|
|
||||||
|
// Offset
|
||||||
|
const offsetSegment = this.getOffsetPath(segmentPoints, this.ROAD_OFFSET);
|
||||||
|
offsetSegment.forEach(p => fullPathPoints.push(new THREE.Vector3(p.x, 0.5, p.y)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Update/Create Mesh
|
||||||
|
if (this.pathMesh) {
|
||||||
|
this.scene.remove(this.pathMesh);
|
||||||
|
this.pathMesh.geometry.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullPathPoints.length < 2) return;
|
||||||
|
|
||||||
|
const curve = new THREE.CatmullRomCurve3(fullPathPoints);
|
||||||
|
// Low tension = smoother corners
|
||||||
|
const tubeGeom = new THREE.TubeGeometry(curve, fullPathPoints.length, 1.5, 6, false);
|
||||||
|
const tubeMat = new THREE.MeshBasicMaterial({ color: this.settings.colors.route });
|
||||||
|
|
||||||
|
this.pathMesh = new THREE.Mesh(tubeGeom, tubeMat);
|
||||||
|
this.scene.add(this.pathMesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
addMarkerVisual(nodeId) {
|
||||||
|
const node = this.graphData.nodes[nodeId];
|
||||||
|
const geom = new THREE.SphereGeometry(4);
|
||||||
|
|
||||||
|
// Color Logic: Start(Green) -> End(Red). Intermediate? Yellow.
|
||||||
|
let color = this.settings.colors.pathStart;
|
||||||
|
if (this.markers.length > 0) color = this.settings.colors.pathEnd; // Default to End color
|
||||||
|
|
||||||
|
// If we are adding a new end, turn the PREVIOUS end into a waypoint (Yellow)
|
||||||
|
if (this.markers.length > 0) {
|
||||||
|
// Change the previous last marker to yellow (waypoint)
|
||||||
|
// Unless it was the start marker (index 0)
|
||||||
|
if (this.markers.length > 1) {
|
||||||
|
this.markers[this.markers.length - 1].material.color.setHex(0xFFFF00);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mat = new THREE.MeshBasicMaterial({ color: color });
|
||||||
|
const mesh = new THREE.Mesh(geom, mat);
|
||||||
|
|
||||||
|
mesh.position.set(node.x, 2, node.y);
|
||||||
|
mesh.userData = { isMarker: true, nodeId: nodeId };
|
||||||
|
|
||||||
|
this.scene.add(mesh);
|
||||||
|
this.markers.push(mesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================
|
||||||
|
// Algorithms (A* & Math)
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
findNearestNode(x, z) {
|
findNearestNode(x, z) {
|
||||||
@@ -101,7 +194,7 @@ export class RouteManager {
|
|||||||
let minDist = Infinity;
|
let minDist = Infinity;
|
||||||
for (const [id, node] of Object.entries(this.graphData.nodes)) {
|
for (const [id, node] of Object.entries(this.graphData.nodes)) {
|
||||||
const dx = node.x - x;
|
const dx = node.x - x;
|
||||||
const dz = node.y - z; // Graph Y is World Z
|
const dz = node.y - z;
|
||||||
const d2 = dx * dx + dz * dz;
|
const d2 = dx * dx + dz * dz;
|
||||||
if (d2 < minDist) {
|
if (d2 < minDist) {
|
||||||
minDist = d2;
|
minDist = d2;
|
||||||
@@ -111,76 +204,9 @@ export class RouteManager {
|
|||||||
return closestId;
|
return closestId;
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePathVisuals() {
|
|
||||||
// We need at least 2 nodes to draw a path
|
|
||||||
if (this.currentRouteNodes.length < 2) return;
|
|
||||||
|
|
||||||
// 1. Calculate Full Path (Segment by Segment)
|
|
||||||
let fullPathPoints = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < this.currentRouteNodes.length - 1; i++) {
|
|
||||||
const start = this.currentRouteNodes[i];
|
|
||||||
const end = this.currentRouteNodes[i + 1];
|
|
||||||
|
|
||||||
const segmentEdges = this.computePathAStar(start, end);
|
|
||||||
|
|
||||||
if (!segmentEdges) {
|
|
||||||
console.warn(`No path found between ${start} and ${end}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process Geometry for this segment
|
|
||||||
segmentEdges.forEach(step => {
|
|
||||||
const rawPoints = step.edgeData.points;
|
|
||||||
let segmentPoints = rawPoints.map(p => new THREE.Vector2(p[0], p[1]));
|
|
||||||
if (step.isReverse) segmentPoints.reverse();
|
|
||||||
|
|
||||||
const offsetSegment = this.getOffsetPath(segmentPoints, this.ROAD_OFFSET);
|
|
||||||
|
|
||||||
offsetSegment.forEach(p => {
|
|
||||||
fullPathPoints.push(new THREE.Vector3(p.x, 0.5, p.y));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Draw Tube
|
|
||||||
if (this.pathMesh) this.scene.remove(this.pathMesh);
|
|
||||||
if (fullPathPoints.length < 2) return;
|
|
||||||
|
|
||||||
const curve = new THREE.CatmullRomCurve3(fullPathPoints);
|
|
||||||
const tubeGeom = new THREE.TubeGeometry(curve, fullPathPoints.length, 1.5, 6, false);
|
|
||||||
const tubeMat = new THREE.MeshBasicMaterial({ color: this.settings.colors.route });
|
|
||||||
|
|
||||||
this.pathMesh = new THREE.Mesh(tubeGeom, tubeMat);
|
|
||||||
this.scene.add(this.pathMesh);
|
|
||||||
}
|
|
||||||
|
|
||||||
addMarker(nodeId) {
|
|
||||||
const node = this.graphData.nodes[nodeId];
|
|
||||||
const geom = new THREE.SphereGeometry(4);
|
|
||||||
|
|
||||||
// Color logic: Green for start, Red for end, Yellow for waypoints
|
|
||||||
let color = this.settings.colors.pathStart;
|
|
||||||
if (this.markers.length > 0) color = 0xFFFF00; // Middle
|
|
||||||
|
|
||||||
const mat = new THREE.MeshBasicMaterial({ color: color });
|
|
||||||
const mesh = new THREE.Mesh(geom, mat);
|
|
||||||
mesh.position.set(node.x, 2, node.y);
|
|
||||||
mesh.userData = { isMarker: true, nodeId: nodeId }; // Tag for input manager
|
|
||||||
|
|
||||||
this.scene.add(mesh);
|
|
||||||
this.markers.push(mesh);
|
|
||||||
|
|
||||||
// Update last marker to Red
|
|
||||||
if (this.markers.length > 1) {
|
|
||||||
this.markers[this.markers.length - 1].material.color.setHex(this.settings.colors.pathEnd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================
|
|
||||||
// A* Implementation
|
|
||||||
// ============================
|
|
||||||
computePathAStar(start, end) {
|
computePathAStar(start, end) {
|
||||||
|
if (start === end) return [];
|
||||||
|
|
||||||
const openSet = new Set([start]);
|
const openSet = new Set([start]);
|
||||||
const cameFrom = {};
|
const cameFrom = {};
|
||||||
const gScore = {};
|
const gScore = {};
|
||||||
|
|||||||
35
src/main.js
35
src/main.js
@@ -2,7 +2,7 @@ import * as THREE from 'three';
|
|||||||
import { MapControls } from 'three/addons/controls/MapControls.js';
|
import { MapControls } from 'three/addons/controls/MapControls.js';
|
||||||
import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
|
import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js';
|
||||||
|
|
||||||
// Import our new Phase 1 Managers
|
// Import our Classes
|
||||||
import { InputManager } from './InputManager.js';
|
import { InputManager } from './InputManager.js';
|
||||||
import { RouteManager } from './RouteManager.js';
|
import { RouteManager } from './RouteManager.js';
|
||||||
|
|
||||||
@@ -33,32 +33,36 @@ let inputManager, routeManager;
|
|||||||
function init() {
|
function init() {
|
||||||
setupScene();
|
setupScene();
|
||||||
|
|
||||||
// -- PHASE 1 INITIALIZATION --
|
// -- INITIALIZATION --
|
||||||
// Initialize Managers
|
// 1. Create Route Manager
|
||||||
inputManager = new InputManager(camera, renderer.domElement, scene);
|
|
||||||
routeManager = new RouteManager(scene, SETTINGS);
|
routeManager = new RouteManager(scene, SETTINGS);
|
||||||
|
|
||||||
// Wire up Input to Route Logic
|
// 2. Create Input Manager (Pass controls so we can disable them during drag)
|
||||||
|
inputManager = new InputManager(camera, renderer.domElement, scene, controls);
|
||||||
inputManager.init();
|
inputManager.init();
|
||||||
|
|
||||||
|
// 3. Wire Events
|
||||||
|
|
||||||
|
// Handle Click (Add Node)
|
||||||
inputManager.onClick = (point, object) => {
|
inputManager.onClick = (point, object) => {
|
||||||
// If we clicked the ground, we add a node to the route
|
|
||||||
if (object.name === "GROUND") {
|
if (object.name === "GROUND") {
|
||||||
routeManager.addNodeByWorldPosition(point);
|
routeManager.addNodeByWorldPosition(point);
|
||||||
}
|
}
|
||||||
// If we clicked a marker, we could eventually select it for dragging (Phase 3)
|
|
||||||
else if (object.userData.isMarker) {
|
|
||||||
console.log("Clicked Marker:", object.userData.nodeId);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load Data
|
// Handle Drag (Move Node)
|
||||||
|
inputManager.onDrag = (markerObject, newPoint) => {
|
||||||
|
routeManager.dragNode(markerObject, newPoint);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. Load Data
|
||||||
Promise.all([
|
Promise.all([
|
||||||
fetch(SETTINGS.files.visual).then(r => r.json()),
|
fetch(SETTINGS.files.visual).then(r => r.json()),
|
||||||
fetch(SETTINGS.files.routing).then(r => r.json())
|
fetch(SETTINGS.files.routing).then(r => r.json())
|
||||||
]).then(([visual, routing]) => {
|
]).then(([visual, routing]) => {
|
||||||
console.log("Data loaded.");
|
console.log("Data loaded.");
|
||||||
renderCity(visual);
|
renderCity(visual);
|
||||||
routeManager.initGraph(routing); // Pass data to RouteManager
|
routeManager.initGraph(routing);
|
||||||
});
|
});
|
||||||
|
|
||||||
animate();
|
animate();
|
||||||
@@ -80,9 +84,9 @@ function setupScene() {
|
|||||||
renderer.shadowMap.enabled = true;
|
renderer.shadowMap.enabled = true;
|
||||||
document.body.appendChild(renderer.domElement);
|
document.body.appendChild(renderer.domElement);
|
||||||
|
|
||||||
// Lights
|
|
||||||
const ambient = new THREE.HemisphereLight(0xffffff, 0x555555, 0.7);
|
const ambient = new THREE.HemisphereLight(0xffffff, 0x555555, 0.7);
|
||||||
scene.add(ambient);
|
scene.add(ambient);
|
||||||
|
|
||||||
const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
|
const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
|
||||||
dirLight.position.set(500, 1000, 500);
|
dirLight.position.set(500, 1000, 500);
|
||||||
dirLight.castShadow = true;
|
dirLight.castShadow = true;
|
||||||
@@ -93,7 +97,6 @@ function setupScene() {
|
|||||||
dirLight.shadow.camera.bottom = -1500;
|
dirLight.shadow.camera.bottom = -1500;
|
||||||
scene.add(dirLight);
|
scene.add(dirLight);
|
||||||
|
|
||||||
// Ground Plane
|
|
||||||
const plane = new THREE.Mesh(
|
const plane = new THREE.Mesh(
|
||||||
new THREE.PlaneGeometry(10000, 10000),
|
new THREE.PlaneGeometry(10000, 10000),
|
||||||
new THREE.MeshLambertMaterial({ color: SETTINGS.colors.ground })
|
new THREE.MeshLambertMaterial({ color: SETTINGS.colors.ground })
|
||||||
@@ -104,7 +107,6 @@ function setupScene() {
|
|||||||
plane.receiveShadow = true;
|
plane.receiveShadow = true;
|
||||||
scene.add(plane);
|
scene.add(plane);
|
||||||
|
|
||||||
// Controls
|
|
||||||
controls = new MapControls(camera, renderer.domElement);
|
controls = new MapControls(camera, renderer.domElement);
|
||||||
controls.dampingFactor = 0.05;
|
controls.dampingFactor = 0.05;
|
||||||
controls.enableDamping = true;
|
controls.enableDamping = true;
|
||||||
@@ -118,10 +120,9 @@ function setupScene() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 3. Visual Rendering (Static City)
|
// 3. Visual Rendering
|
||||||
// ==========================================
|
// ==========================================
|
||||||
function renderCity(data) {
|
function renderCity(data) {
|
||||||
// This logic is unchanged from before, just strictly for static geometry
|
|
||||||
const createLayer = (items, color, height, lift, isExtruded) => {
|
const createLayer = (items, color, height, lift, isExtruded) => {
|
||||||
if (!items || !items.length) return;
|
if (!items || !items.length) return;
|
||||||
const geometries = [];
|
const geometries = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user