1 /** 2 Copyright: Copyright (c) 2016-2018 Andrey Penechko. 3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0). 4 Authors: Andrey Penechko. 5 */ 6 module railroad.rail.railtool; 7 8 import voxelman.log; 9 import voxelman.container.buffer : Buffer; 10 import voxelman.graphics; 11 import voxelman.core.config; 12 import voxelman.core.packets; 13 14 import voxelman.blockentity.blockentityman; 15 import voxelman.blockentity.plugin; 16 import voxelman.edit.plugin; 17 import voxelman.graphics.plugin; 18 import voxelman.net.plugin; 19 import voxelman.world.blockentity; 20 import voxelman.world.clientworld; 21 import voxelman.worldinteraction.plugin; 22 23 import voxelman.geometry; 24 import voxelman.math; 25 import voxelman.world.storage; 26 import voxelman.edit.tools.itool; 27 28 import railroad.plugin; 29 import railroad.rail.mesh; 30 import railroad.rail.packets; 31 import railroad.rail.utils; 32 33 34 final class RailTool : ITool 35 { 36 ClientWorld clientWorld; 37 BlockEntityManager blockEntityManager; 38 NetClientPlugin connection; 39 WorldInteractionPlugin worldInteraction; 40 41 BlockWorldPos startingPos; 42 RailSegment segment; 43 BlockWorldPos cursorPos; 44 45 RailOrientation cursorOrientation; 46 DiagonalRailSide diagonalRailSide; 47 uint curLength; 48 RailPos minPos; 49 RailPos maxPos; 50 51 enum EditState 52 { 53 none, 54 placing, 55 removing 56 } 57 EditState state; 58 59 this(ClientWorld clientWorld, BlockEntityManager blockEntityManager, 60 NetClientPlugin connection, WorldInteractionPlugin worldInteraction) 61 { 62 this.clientWorld = clientWorld; 63 this.blockEntityManager = blockEntityManager; 64 this.connection = connection; 65 this.worldInteraction = worldInteraction; 66 name = "entity.place_rail"; 67 } 68 69 override void onUpdate() 70 { 71 updateCursorStartingPos(); 72 } 73 74 override void onRender(GraphicsPlugin graphics) { 75 if (worldInteraction.cameraInSolidBlock) return; 76 if (!worldInteraction.cursorHit) return; 77 78 final switch(state) { 79 case EditState.none: 80 drawLine(cursorPos, cursorPos, graphics, Colors.white); 81 break; 82 case EditState.placing: 83 drawLine(startingPos, cursorPos, graphics, Colors.green); 84 break; 85 case EditState.removing: 86 drawLine(startingPos, cursorPos, graphics, Colors.red); 87 break; 88 } 89 } 90 91 void drawLine(BlockWorldPos start, BlockWorldPos end, 92 GraphicsPlugin graphics, Colors color) 93 { 94 CubeSide side0; 95 CubeSide side1; 96 97 // little hack to fix preview crossing caused by special combination of offsets 98 // happens when two sides with the same sign are emitted. 99 bool flipEndOffset = false; 100 101 final switch(cursorOrientation) { 102 case RailOrientation.x: 103 auto railPos1 = RailPos(start); 104 auto railPos2 = RailPos(end); 105 short minX = min(railPos1.x, railPos2.x); 106 short maxX = max(railPos1.x, railPos2.x); 107 minPos.vector = svec4(minX, railPos1.y, railPos1.z, railPos1.w); 108 maxPos.vector = svec4(maxX, railPos1.y, railPos1.z, railPos1.w); 109 curLength = (maxX - minX) + 1; 110 side0 = CubeSide.xneg; 111 side1 = CubeSide.xpos; 112 break; 113 case RailOrientation.z: 114 auto railPos1 = RailPos(start); 115 auto railPos2 = RailPos(end); 116 short minZ = min(railPos1.z, railPos2.z); 117 short maxZ = max(railPos1.z, railPos2.z); 118 minPos.vector = svec4(railPos1.x, railPos1.y, minZ, railPos1.w); 119 maxPos.vector = svec4(railPos1.x, railPos1.y, maxZ, railPos1.w); 120 curLength = (maxZ - minZ) + 1; 121 side0 = CubeSide.zneg; 122 side1 = CubeSide.zpos; 123 break; 124 case RailOrientation.xzSameSign: 125 minPos = RailPos(start); 126 vec2 origin = vec2(minPos.xz); 127 vec2 cursor = vec2(end.xz) / RAIL_TILE_SIZE - origin - vec2(0,1); // relative to the start of selection 128 vec2 dividingAxisVector = vec2(1, -1); 129 130 // ^ H / 131 // +---+---& 2 H | P - values 132 // | X ^ /| H | 133 //^ | X P | H | 1 134 // ^|X / ^|H | 135 // + &1 + | 0.5 136 // L|^ / X|^ | +---> X 137 //L | P X | ^ | 0 | 138 // |/ ^ X | ^ | | 139 // &---+---+ ^ |-0.5 v Z 140 // /0 L ^ ^ 141 // L ^ ^ 142 // & - sample vectors of (0, 0), (0.5, -0.5) and (1, -1) give results of 0, 1, 2 143 // Points marked as 'P' divide diagonal of rail tile in 4 equal pieces 144 // and they have dot results of 0.5 and 1.5 145 // Cursor locations between P-points should result in rail pieces marked as 'X' 146 // Locations between 0 and 0.5 are L pieces, between 1.5 and 2 are H pieces. 147 // dot(dividingAxisVector, cursor) of vec2(1, -1) will give result of 2 148 // we substract 0.5 to bring 0 to the first P piece. Second P piece has a value of 1. 149 float projection = dot(dividingAxisVector, cursor) - 0.5f; 150 151 // section inside current tile has index of 0 152 int cursorSection = cast(int)floor(projection); 153 154 // use length of 0 when no selection 155 if (state == EditState.none) cursorSection = 0; 156 157 DiagonalRailSide startSide = cast(DiagonalRailSide)(dot(vec2(1, 1), cursor) < 0); 158 159 DiagonalRailSide endSide = startSide; 160 if (cursorSection % 2 != 0) endSide = cast(DiagonalRailSide)(!startSide); 161 162 ivec2 endTile = addDiagonalManhattan(ivec2(minPos.xz), cursorSection, cursorOrientation, startSide); 163 maxPos.vector = svec4(cast(short)endTile.x, minPos.y, cast(short)endTile.y, minPos.w); 164 165 if (cursorSection < 0) 166 { 167 swap(minPos, maxPos); 168 cursorSection = -cursorSection; 169 if (cursorSection % 2 != 0) 170 { 171 swap(startSide, endSide); 172 } 173 } 174 175 diagonalRailSide = startSide; 176 177 // uses manhattan distance 178 curLength = cursorSection + 1; 179 180 side0 = [CubeSide.zpos, CubeSide.xneg][startSide]; 181 side1 = [CubeSide.xpos, CubeSide.zneg][endSide]; 182 flipEndOffset = startSide == endSide; 183 break; 184 case RailOrientation.xzOppSign: 185 minPos = RailPos(start); 186 vec2 origin = vec2(minPos.xz); 187 vec2 cursor = vec2(end.xz) / RAIL_TILE_SIZE - origin; // relative to the start of selection 188 vec2 dividingAxisVector = vec2(1, 1); 189 float projection = dot(dividingAxisVector, cursor) - 0.5f; 190 191 // section inside current tile has index of 0 192 int cursorSection = cast(int)floor(projection); 193 194 // use length of 0 when no selection 195 if (state == EditState.none) cursorSection = 0; 196 197 DiagonalRailSide startSide = cast(DiagonalRailSide)(dot(vec2(-1, 1), cursor) < 0); 198 199 DiagonalRailSide endSide = startSide; 200 if (cursorSection % 2 != 0) endSide = cast(DiagonalRailSide)(!startSide); 201 202 ivec2 endTile = addDiagonalManhattan(ivec2(minPos.xz), cursorSection, cursorOrientation, startSide); 203 maxPos.vector = svec4(cast(short)endTile.x, minPos.y, cast(short)endTile.y, minPos.w); 204 205 if (cursorSection < 0) 206 { 207 swap(minPos, maxPos); 208 cursorSection = -cursorSection; 209 if (cursorSection % 2 != 0) 210 { 211 swap(startSide, endSide); 212 } 213 } 214 215 diagonalRailSide = startSide; 216 217 // uses manhattan distance 218 curLength = cursorSection + 1; 219 220 side0 = [CubeSide.xneg, CubeSide.zneg][startSide]; 221 side1 = [CubeSide.zpos, CubeSide.xpos][endSide]; 222 break; 223 } 224 graphics.debugBatch.triBuffer.putRailPreview(minPos, maxPos, side0, side1, flipEndOffset, color); 225 } 226 227 override void onShowDebug() { 228 //import voxelman.text.textformatter; 229 //igTextf("Orientation: %s", cursorOrientation); 230 } 231 232 override void onMainActionPress() { 233 if (state != EditState.none) return; 234 if (!worldInteraction.cursorHit) return; 235 state = EditState.removing; 236 startingPos = cursorPos; 237 } 238 239 override void onMainActionRelease() { 240 if (state != EditState.removing) return; 241 state = EditState.none; 242 243 if (worldInteraction.cursorHit) 244 { 245 connection.send(EditRailLinePacket(minPos, curLength, cursorOrientation, diagonalRailSide, RailEditOp.remove)); 246 } 247 } 248 249 private void updateCursorStartingPos() { 250 if (worldInteraction.cameraInSolidBlock) return; 251 252 cursorPos = worldInteraction.sideBlockPos; 253 254 RailData railOnGround = getRailAt(RailPos(worldInteraction.blockPos), 255 blockEntityManager.getId("rail"), 256 clientWorld.worldAccess, clientWorld.entityAccess); 257 258 if (!railOnGround.empty) 259 { 260 cursorPos = worldInteraction.blockPos; 261 } 262 } 263 264 override void onSecondaryActionPress() { 265 if (state != EditState.none) return; 266 if (!worldInteraction.cursorHit) return; 267 state = EditState.placing; 268 startingPos = cursorPos; 269 } 270 271 override void onSecondaryActionRelease() { 272 if (state != EditState.placing) return; 273 state = EditState.none; 274 275 if (worldInteraction.cursorHit) 276 { 277 connection.send(EditRailLinePacket(minPos, curLength, cursorOrientation, diagonalRailSide, RailEditOp.add)); 278 } 279 } 280 281 override void onRotateAction() { 282 cursorOrientation = cast(RailOrientation)((cursorOrientation + 1) % 4); 283 } 284 }