1 /** 2 Copyright: Copyright (c) 2017-2018 Andrey Penechko. 3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0). 4 Authors: Andrey Penechko. 5 */ 6 module railroad.wagon.wagontool; 7 8 import dlib.geometry.plane; 9 import voxelman.log; 10 import voxelman.math; 11 12 import voxelman.blockentity.blockentityman; 13 14 import voxelman.edit.plugin; 15 import voxelman.edit.tools.itool; 16 17 import voxelman.graphics.plugin; 18 import voxelman.net.plugin; 19 20 import voxelman.world.blockentity; 21 import voxelman.world.clientworld; 22 import voxelman.world.storage; 23 import voxelman.worldinteraction.plugin; 24 25 import railroad.rail.utils; 26 import railroad.wagon.packets; 27 import railroad.wagon.wagon; 28 import voxelman.world.mesh.utils : FaceSide, oppFaceSides; 29 30 final class WagonTool : ITool 31 { 32 ClientWorld clientWorld; 33 BlockEntityManager blockEntityManager; 34 NetClientPlugin connection; 35 WorldInteractionPlugin worldInteraction; 36 GraphicsPlugin graphics; 37 38 // need to check for emptiness 39 RailData data; 40 RailPos railPos; 41 bool placeAllowed; 42 vec3 preciseHitPos; 43 44 float wagonAxisDistance = 8; 45 float wagonRotation = 0; 46 47 this(ClientWorld clientWorld, BlockEntityManager blockEntityManager, 48 NetClientPlugin connection, WorldInteractionPlugin worldInteraction, 49 GraphicsPlugin graphics) 50 { 51 this.clientWorld = clientWorld; 52 this.blockEntityManager = blockEntityManager; 53 this.connection = connection; 54 this.worldInteraction = worldInteraction; 55 this.graphics = graphics; 56 name = "entity.wagon_tool"; 57 } 58 59 override void onUpdate() 60 { 61 auto plane = Plane(vec3(0,1,0), worldInteraction.hitPosition.y); 62 vec3 camPos = worldInteraction.cameraPos; 63 plane.intersectsLine(camPos, camPos+worldInteraction.cameraTraget, preciseHitPos); 64 preciseHitPos.y = worldInteraction.hitPosition.y; 65 graphics.debugText.putfln("hit %s", worldInteraction.hitPosition); 66 graphics.debugText.putfln("precise hit %s", preciseHitPos); 67 68 railPos = RailPos(worldInteraction.blockPos); 69 data = railAt(railPos); 70 } 71 72 private RailData railAt(RailPos pos) 73 { 74 ushort targetEntityId = blockEntityManager.getId("rail"); 75 return getRailAt(pos, targetEntityId, clientWorld.worldAccess, clientWorld.entityAccess); 76 } 77 78 // returns q the closest point to p on the line segment from a to b 79 vec3 project_point_to_line_segment(vec3 a, vec3 b, vec3 p) 80 { 81 // vector from a to b 82 vec3 AB = b - a; 83 84 // squared distance from a to b 85 float AB_squared = dot(AB, AB); 86 87 if(AB_squared == 0) 88 { 89 // a and b are the same point 90 return a; 91 } 92 else 93 { 94 // vector from a to p 95 vec3 Ap = p - a; 96 97 // from http://stackoverflow.com/questions/849211/ 98 // Consider the line extending the segment, parameterized as a + t (b - a) 99 // We find projection of point p onto the line. 100 // It falls where t = [(p-a) . (b-a)] / |b-a|^2 101 float t = dot(Ap, AB) / AB_squared; 102 103 if (t < 0.0f) 104 { 105 // "Before" a on the line, just return a 106 return a; 107 } 108 else if (t > 1.0f) 109 { 110 // "After" b on the line, just return b 111 return b; 112 } 113 else 114 { 115 // projection lines "inbetween" a and b on the line 116 return a + t * AB; 117 } 118 } 119 } 120 121 float point_time_on_line_segment(vec3 a, vec3 b, vec3 p) 122 { 123 vec3 AB = b - a; 124 float AB_squared = dot(AB, AB); 125 if(AB_squared == 0) return 0; 126 else return dot(p - a, AB) / AB_squared; 127 } 128 129 vec3 pointSize = vec3(0.5f,0.5f,0.5f); 130 vec3 pointOffset = vec3(0.25f,0.25f,0.25f); 131 132 // Assumes non-empty rail 133 // Virtual wagon is placed at user's cursor position and has 'wagonRotation' rotation. 134 // The goal is to find positions of wagon's ends to be placed. 135 // Algorithm: 136 // 1. For hovered rail tile find closest segment 137 // 2. For best candidate, for each end calc: 138 // - position and rail (vec3, RailPos, RailSegment, side of tile and segment end index) 139 // - error (diff between end's position and virtual wagon position) 140 // 3. Iteratively try to build wagon placement closest to virtual wagon 141 // 3.1. If we hit the end of segment when placing one of the ends, 142 // look into attached segments, replacing current selected segment with closer one 143 // Repeat until we can place two ends 144 vec3[2] findWagonPlacement() 145 { 146 assert(!data.empty); 147 148 vec3 railWorldPos = vec3(railPos.toBlockWorldPos.xyz); 149 // wagon center 150 vec3 wagonC = vec3(preciseHitPos); 151 152 vec3 vertOff = vec3(0,1,0); 153 vec3 wagonVector = vec3(cos(wagonRotation), 0, sin(wagonRotation)); 154 vec3 wagonA = vertOff + wagonC - wagonVector * wagonAxisDistance/2; 155 vec3 wagonB = vertOff + wagonC + wagonVector * wagonAxisDistance/2; 156 vec3[2] wagonPoints = [wagonA-vertOff, wagonB-vertOff]; 157 graphics.debugText.putfln(" wagA %s wagB %s", wagonPoints[0], wagonPoints[1]); 158 159 // Shadow wagon 160 graphics.debugBatch.putLine(wagonA, wagonB, Color4ub(0,0,0,64)); 161 graphics.debugBatch.putCube(wagonA-pointOffset, pointSize, Color4ub(0,0,0,64), true); 162 graphics.debugBatch.putCube(wagonB-pointOffset, pointSize, Color4ub(0,0,0,64), true); 163 graphics.debugText.putfln("wagon rotation %.1f", radtodeg(wagonRotation)); 164 165 vec3[2] resultPos; 166 vec3[2] resultInnerPos; 167 RailPos[2] resultRail = [railPos, railPos]; 168 RailSegment[2] resultSegment; 169 FaceSide[2] resultSides; 170 size_t[2] resultOuterIndex; 171 bool[2] canExtend = [true, true]; 172 173 // errors say how far is calculated position to the given one 174 float segmentError = float.infinity; 175 float[2] errors; 176 177 // Find best central segment 178 foreach(i, segment; data.getSegments) 179 { 180 FaceSide[2] sides = segmentInfos[segment].sides; 181 // Set first approximation 182 vec3 railA = railWorldPos + railTileConnectionPoints[sides[0]]-vec3(0f,0.5f,0f); 183 vec3 railB = railWorldPos + railTileConnectionPoints[sides[1]]-vec3(0f,0.5f,0f); 184 //graphics.debugText.putfln(" railA %s railB %s", railA, railB); 185 186 float errorAA = distancesqr(railA, wagonPoints[0]); 187 float errorBB = distancesqr(railB, wagonPoints[1]); 188 189 float errorAB = distancesqr(railA, wagonPoints[1]); 190 float errorBA = distancesqr(railB, wagonPoints[0]); 191 192 float[2] newErrors; 193 float newError; 194 195 float error0 = errorAA + errorBB; 196 float error1 = errorAB + errorBA; 197 198 if (error0 < error1) 199 { 200 newError = error0; 201 newErrors = [errorAA, errorBB]; 202 } 203 else 204 { 205 newError = error1; 206 newErrors = [errorAB, errorBA]; 207 } 208 209 //graphics.debugText.putfln(" AA %s AB %s BB %s BA %s", errorAA, errorAB, errorBB, errorBA); 210 //graphics.debugText.putfln(" error of %s is %.1f", segment, newError); 211 212 void setSegment(int idx0, int idx1) 213 { 214 resultPos[idx0] = railA; 215 resultPos[idx1] = railB; 216 resultInnerPos[idx0] = railB; 217 resultInnerPos[idx1] = railA; 218 resultSides[idx0] = sides[0]; 219 resultSides[idx1] = sides[1]; 220 errors[idx0] = newErrors[0]; 221 errors[idx1] = newErrors[1]; 222 segmentError = newError; 223 resultOuterIndex[idx0] = 0; 224 resultOuterIndex[idx1] = 1; 225 resultSegment = [segment, segment]; 226 } 227 228 if (newError < segmentError) 229 { 230 if (error0 < error1) setSegment(0, 1); 231 else setSegment(1, 0); 232 } 233 } 234 235 immutable float wagonLenSqr = wagonAxisDistance * wagonAxisDistance; 236 237 // Returns true if extension was successfull 238 // Returns false otherwise (i.e. no rail in given direction) 239 bool extendRails(size_t nextIdx) 240 { 241 RailPos adjacentPos = resultRail[nextIdx].posInDirection(resultSides[nextIdx]); 242 RailData adjacentData = railAt(adjacentPos); 243 FaceSide connectedViaSide = oppFaceSides[resultSides[nextIdx]]; 244 vec3 adjRailWorldPos = vec3(adjacentPos.toBlockWorldPos.xyz); 245 graphics.debugText.putfln(" - extend %s in %s at %s", nextIdx, resultSides[nextIdx], adjacentPos); 246 247 float pointError = float.infinity; 248 249 bool success = false; 250 251 // TODO smooth connection checking 252 foreach(adjSegment; adjacentData.getSegments) 253 { 254 graphics.debugText.putfln(" - segm side %s conn %s", adjSegment, segmentInfos[adjSegment].sideConnections[connectedViaSide]); 255 if (segmentInfos[adjSegment].sideConnections[connectedViaSide]) 256 { 257 size_t innerIndex = segmentInfos[adjSegment].sideIndicies[connectedViaSide]; 258 size_t outerIndex = 1 - innerIndex; 259 FaceSide[2] sides = segmentInfos[adjSegment].sides; 260 vec3 railInnerEnd = adjRailWorldPos + railTileConnectionPoints[sides[innerIndex]]-vec3(0f,0.5f,0f); 261 vec3 railOuterEnd = adjRailWorldPos + railTileConnectionPoints[sides[outerIndex]]-vec3(0f,0.5f,0f); 262 vec3 closestPoint = project_point_to_line_segment(railInnerEnd, railOuterEnd, wagonPoints[nextIdx]); 263 float endError = distancesqr(closestPoint, wagonPoints[nextIdx]); 264 265 void setPoint() 266 { 267 // set attributes of outer end of the rail 268 resultOuterIndex[nextIdx] = outerIndex; 269 graphics.debugText.putfln(" - set side %s", outerIndex); 270 resultInnerPos[nextIdx] = resultPos[nextIdx]; 271 resultPos[nextIdx] = railOuterEnd; 272 resultRail[nextIdx] = adjacentPos; 273 resultSides[nextIdx] = sides[outerIndex]; 274 segmentError = errors[1-nextIdx] + endError; 275 errors[nextIdx] = endError; 276 pointError = endError; 277 resultSegment[nextIdx] = adjSegment; 278 success = true; 279 } 280 281 if (endError < pointError) setPoint(); 282 } 283 } 284 285 graphics.debugText.putfln(" - success %s", success); 286 287 canExtend[nextIdx] = success; 288 289 return success; 290 } 291 292 // Reports true when wagon position is found 293 // Otherwise a new segment needs to be attached to one of the ends 294 bool tryPlaceWagon() 295 { 296 // Can potentially place 297 if (wagonLenSqr <= distancesqr(resultPos[0], resultPos[1])) 298 { 299 graphics.debugText.putfln(" - place success %s <= %s", wagonLenSqr, distancesqr(resultPos[0], resultPos[1])); 300 301 vec3 inner0 = resultInnerPos[0]; 302 vec3 outer0 = resultPos[0]; 303 vec3 inner1 = resultInnerPos[1]; 304 vec3 outer1 = resultPos[1]; 305 306 if (resultOuterIndex[0] == 0) swap(inner0, outer0); 307 if (resultOuterIndex[1] == 0) swap(inner1, outer1); 308 309 graphics.debugBatch.putCube(vertOff*1.5f + outer0-pointOffset, pointSize, Color4ub(0,100,100,255), true); 310 graphics.debugBatch.putCube(vertOff*1.5f + inner0-pointOffset, pointSize, Color4ub(0,200,200,255), true); 311 graphics.debugBatch.putCube(vertOff*2.5f + inner1-pointOffset, pointSize, Color4ub(100,100,0,255), true); 312 graphics.debugBatch.putCube(vertOff*2.5f + outer1-pointOffset, pointSize, Color4ub(200,200,0,255), true); 313 314 float pos0t = equation2(inner0.xz, outer0.xz, preciseHitPos.xz, wagonAxisDistance/2.0); 315 vec3 pos0 = lerp(inner0, outer0, pos0t); 316 317 graphics.debugText.putfln("lerp 0 %s ext %s", pos0t, canExtend[0]); 318 if ((pos0t < 0 || pos0t > 1) && canExtend[0]) 319 { 320 extendRails(0); 321 return false; 322 } 323 324 float pos1t = equation2(inner1.xz, outer1.xz, pos0.xz, wagonAxisDistance); 325 vec3 pos1 = lerp(inner1, outer1, pos1t); 326 327 graphics.debugText.putfln("lerp 1 %s ext %s", pos1t, canExtend[1]); 328 if ((pos1t < 0 || pos1t > 1) && canExtend[1]) 329 { 330 extendRails(1); 331 return false; 332 } 333 334 resultPos = [pos0, pos1]; 335 graphics.debugText.putfln("lerp 0 %s 1 %s", pos0t, pos1t); 336 graphics.debugText.putfln("final pos %s", resultPos); 337 return true; 338 } 339 else 340 { 341 // Extend points to adjacent segments as nesessary 342 size_t nextIdx; 343 // fail, need more space 344 // extend in direction closer to wagon center 345 graphics.debugText.putfln(" - place fail"); 346 if (distancesqr(wagonC, resultPos[0]) < distancesqr(wagonC, resultPos[1])) 347 { 348 if (!extendRails(0)) extendRails(1); 349 } 350 else 351 { 352 if (!extendRails(1)) extendRails(0); 353 } 354 return false; 355 } 356 } 357 358 size_t iters; 359 while(!tryPlaceWagon()) { 360 if (iters++ > 10) break; 361 } 362 363 // Best first segment 364 graphics.debugBatch.putLine(vertOff + resultPos[0], vertOff + resultPos[1], Color4ub(0,255,0,255)); 365 graphics.debugBatch.putCube(vertOff + resultPos[0]-pointOffset, pointSize, Color4ub(255,0,0,255), true); 366 graphics.debugBatch.putCube(vertOff + resultPos[1]-pointOffset, pointSize, Color4ub(0,255,0,255), true); 367 return resultPos; 368 } 369 370 override void onRender(GraphicsPlugin renderer) 371 { 372 void drawRailsAt(vec3 railWorldPos, RailSegment segment, Color4ub color) 373 { 374 FaceSide[2] sides = segmentInfos[segment].sides; 375 vec3 pos0 = railWorldPos + railTileConnectionPoints[sides[0]]; 376 vec3 pos1 = railWorldPos + railTileConnectionPoints[sides[1]]; 377 renderer.debugBatch.putLine(pos0, pos1, color); 378 } 379 380 void drawSegments() 381 { 382 vec3 railWorldPos = vec3(railPos.toBlockWorldPos.xyz); 383 384 // Segments of hovered rail tile 385 foreach(i, segment; data.getSegments) 386 { 387 enum layerDist = 0.25f; 388 drawRailsAt(railWorldPos + vec3(0, i*layerDist, 0), segment, colorsArray[i+2]); 389 390 FaceSide[2] sides = segmentInfos[segment].sides; 391 392 foreach(side; sides) 393 { 394 RailPos adjacentPos = railPos.posInDirection(side); 395 RailData adjacentData = railAt(adjacentPos); 396 397 // side with which adjacent segment connects to main segment 398 FaceSide connectedViaSide = oppFaceSides[side]; 399 400 // Process all segments that connect to main segment 401 foreach(adjSegment; adjacentData.getSegments) 402 if (segmentInfos[adjSegment].sideConnections[connectedViaSide]) 403 { 404 vec3 adjRailWorldPos = vec3(adjacentPos.toBlockWorldPos.xyz); 405 drawRailsAt(adjRailWorldPos + vec3(0, i*layerDist, 0), adjSegment, colorsArray[i+2]); 406 } 407 } 408 } 409 } 410 411 if (!data.empty) 412 { 413 graphics.debugText.putfln("hover %s", railPos); 414 vec3[2] ends = findWagonPlacement(); 415 float angle = -atan2(ends[1].z - ends[0].z, ends[1].x - ends[0].x); 416 auto quat = rotationQuaternion(vec3(0,1,0), angle); 417 vec3 wagonCenter = (ends[0] + ends[1]) * 0.5f + vec3(0,1,0); 418 enum wagonWidth = 3.0f; 419 enum wagonHeight = 4.0f; 420 float wagonLength = wagonAxisDistance + 4.0f; 421 422 vec3 wagonSize = vec3(wagonLength, wagonHeight, wagonWidth); 423 vec3 centerOffset = -vec3(wagonSize.x*0.5f, 0, wagonSize.z*0.5f); 424 425 import voxelman.geometry.cubeutils; 426 putFilledBlock(renderer.debugBatch.triBuffer, wagonCenter, wagonSize, -vec3(0.5,0.5,0.5), quat, Color4ub(128, 128, 128, 255)); 427 428 //drawSegments(); 429 } 430 } 431 432 override void onSecondaryActionRelease() 433 { 434 if (!data.empty) connection.send(CreateWagonPacket(railPos)); 435 } 436 437 override void onRotateAction() { 438 wagonRotation += 2*PI / 8; 439 wagonRotation %= 2*PI; 440 } 441 } 442 443 float equation2(vec2 p1, vec2 p2, vec2 circlePos, float radius) 444 { 445 return equation(p1 - circlePos, p2 - circlePos, radius); 446 } 447 448 // for a circle at 0,0. calculate its intersection with a line segment 449 // described with p1, p2 and return a position along the line as [0; 1] 450 float equation(vec2 p1, vec2 p2, float radius) 451 { 452 float dx = (p2.x - p1.x); 453 float dy = (p2.y - p1.y); 454 float a = dx*dx + dy*dy; 455 float b = 2*(dy*p1.y + dx*p1.x); 456 float c = p1.x*p1.x + p1.y*p1.y - radius*radius; 457 float d = b*b - 4*a*c; 458 float sqrt_d = sqrt(d); 459 float t1 = (-b + sqrt_d) / (2*a); 460 float t2 = (-b - sqrt_d) / (2*a); 461 if (std_abs(t2 - 0.5f) < std_abs(t1 - 0.5f)) return t2; 462 return t1; 463 //if (t1 < 0 || t1 > 1) return t2; 464 //return t1; 465 } 466 467 // t position on rail, R length between wagon points 468 // x₁y₁, x₂y₂ rail ends 469 // (1) x² + y² = R² 470 // (2) x = x₁ + t(x₂-x₁) 471 // (3) y = y₁ + t(y₂-y₁) 472 // We substitute (2) and (3) into (1) and get quadratic equation 473 // t²((x₂-x₁)² + (y₂-y₁)²) + t2((y₂-y₁)y₂ + (x₁-x₁)x₂) + x₁ + y₁ - R² = 0 474 // solve for t 475 476 // a = (x2 - x1)^2 + (y2 - y1)^2; 477 // b = 2 * ((y2 - y1)*y1 + (x2 - x1)*x1); 478 // c = x1^2 + y1^2 - R^2; 479 // d = b*b - 4*a*c;