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 }