@drawcall/charta 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/dist/assets/loader.d.ts +21 -0
- package/dist/assets/loader.d.ts.map +1 -0
- package/dist/assets/loader.js +113 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +27 -0
- package/dist/grammar.d.ts +29 -0
- package/dist/grammar.d.ts.map +1 -0
- package/dist/grammar.js +119 -0
- package/dist/grass/index.d.ts +25 -0
- package/dist/grass/index.d.ts.map +1 -0
- package/dist/grass/index.js +177 -0
- package/dist/grass/material.d.ts +10 -0
- package/dist/grass/material.d.ts.map +1 -0
- package/dist/grass/material.js +80 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/interpreter.d.ts +47 -0
- package/dist/interpreter.d.ts.map +1 -0
- package/dist/interpreter.js +226 -0
- package/dist/locations.d.ts +16 -0
- package/dist/locations.d.ts.map +1 -0
- package/dist/locations.js +58 -0
- package/dist/parser.d.ts +9 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +47 -0
- package/dist/pillars/index.d.ts +7 -0
- package/dist/pillars/index.d.ts.map +1 -0
- package/dist/pillars/index.js +154 -0
- package/dist/pillars/material.d.ts +3 -0
- package/dist/pillars/material.d.ts.map +1 -0
- package/dist/pillars/material.js +43 -0
- package/dist/place/index.d.ts +37 -0
- package/dist/place/index.d.ts.map +1 -0
- package/dist/place/index.js +216 -0
- package/dist/tiles/geometry.d.ts +46 -0
- package/dist/tiles/geometry.d.ts.map +1 -0
- package/dist/tiles/geometry.js +463 -0
- package/dist/tiles/index.d.ts +18 -0
- package/dist/tiles/index.d.ts.map +1 -0
- package/dist/tiles/index.js +121 -0
- package/dist/tiles/material.d.ts +6 -0
- package/dist/tiles/material.d.ts.map +1 -0
- package/dist/tiles/material.js +88 -0
- package/dist/utils/instanced-mesh-group.d.ts +17 -0
- package/dist/utils/instanced-mesh-group.d.ts.map +1 -0
- package/dist/utils/instanced-mesh-group.js +59 -0
- package/dist/utils/random.d.ts +4 -0
- package/dist/utils/random.d.ts.map +1 -0
- package/dist/utils/random.js +19 -0
- package/dist/utils/texture.d.ts +3 -0
- package/dist/utils/texture.d.ts.map +1 -0
- package/dist/utils/texture.js +30 -0
- package/dist/walls/index.d.ts +87 -0
- package/dist/walls/index.d.ts.map +1 -0
- package/dist/walls/index.js +376 -0
- package/dist/walls/material.d.ts +3 -0
- package/dist/walls/material.d.ts.map +1 -0
- package/dist/walls/material.js +67 -0
- package/dist/water/index.d.ts +10 -0
- package/dist/water/index.d.ts.map +1 -0
- package/dist/water/index.js +46 -0
- package/dist/water/material.d.ts +5 -0
- package/dist/water/material.d.ts.map +1 -0
- package/dist/water/material.js +46 -0
- package/dist/water/texture.d.ts +15 -0
- package/dist/water/texture.d.ts.map +1 -0
- package/dist/water/texture.js +201 -0
- package/package.json +39 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import { BufferGeometry, Float32BufferAttribute, Uint16BufferAttribute, } from 'three';
|
|
2
|
+
import { isLayerConnectedToWall } from '../walls/index.js';
|
|
3
|
+
const MAX_AUTO_CONNECT_HEIGHT_DIFF_RATIO = 1 / 4;
|
|
4
|
+
const MAX_CONNECTION_HEIGHT_DIFF_RATIO = 8;
|
|
5
|
+
function isBetterConnection(newConnection, oldConnection, tileSize) {
|
|
6
|
+
const getDistance = (c) => Math.abs(c[0].y - c[1].y);
|
|
7
|
+
const distNew = Math.max(tileSize, getDistance(newConnection));
|
|
8
|
+
const distOld = Math.max(tileSize, getDistance(oldConnection));
|
|
9
|
+
if (distNew !== distOld)
|
|
10
|
+
return distNew < distOld;
|
|
11
|
+
const sameTypeNew = newConnection[0].type === newConnection[1].type;
|
|
12
|
+
const sameTypeOld = oldConnection[0].type === oldConnection[1].type;
|
|
13
|
+
if (sameTypeNew !== sameTypeOld)
|
|
14
|
+
return sameTypeNew;
|
|
15
|
+
return getDistance(newConnection) < getDistance(oldConnection);
|
|
16
|
+
}
|
|
17
|
+
function isBlockedByWall(t1, t2, walls, tileSize) {
|
|
18
|
+
const closestWall1 = getClosestConnectedWall(t1, walls, tileSize);
|
|
19
|
+
const closestWall2 = getClosestConnectedWall(t2, walls, tileSize);
|
|
20
|
+
if (closestWall1 != null && closestWall2 != null) {
|
|
21
|
+
//both not null
|
|
22
|
+
if (closestWall1 != closestWall2) {
|
|
23
|
+
//if not connected to the same wall -> blocked
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
//block if not connected to the same part
|
|
27
|
+
return getClosestWallPart(closestWall1, t1) != getClosestWallPart(closestWall2, t2);
|
|
28
|
+
}
|
|
29
|
+
if (closestWall1 === closestWall2) {
|
|
30
|
+
//both null -> not blocked
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
//exactly one is null
|
|
34
|
+
const closestWall = closestWall1 ?? closestWall2;
|
|
35
|
+
//check if for the wall either t1 or t2 is connected to that the other tile is also rather connected to that part
|
|
36
|
+
if (getClosestWallPart(closestWall, t1) != getClosestWallPart(closestWall, t2)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
//we differentiate between top and bottom connected walls since top connected walls tend to be human-made and therefore we block based on the texture id
|
|
40
|
+
if (getClosestWallPart(closestWall, t1) === 'bottomY') {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (t1.textureId != t2.textureId) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
function getClosestConnectedWall(tile, walls, tileSize) {
|
|
49
|
+
let distance = Infinity;
|
|
50
|
+
let result;
|
|
51
|
+
for (const wall of walls) {
|
|
52
|
+
if (!isLayerConnectedToWall(wall.topY, wall.bottomY, tile.y, tileSize)) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
let newDistance = Math.min(Math.abs(tile.y - wall.topY), Math.abs(tile.y - wall.bottomY));
|
|
56
|
+
if (newDistance < distance) {
|
|
57
|
+
result = wall;
|
|
58
|
+
distance = newDistance;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
function getClosestWallPart(wall, tile) {
|
|
64
|
+
return Math.abs(wall.bottomY - tile.y) < Math.abs(wall.topY - tile.y) ? 'bottomY' : 'topY';
|
|
65
|
+
}
|
|
66
|
+
function isBlockingWall(wall, tile, tileSize) {
|
|
67
|
+
return isLayerConnectedToWall(wall.topY, wall.bottomY, tile.y, tileSize);
|
|
68
|
+
}
|
|
69
|
+
function getRelevantWallIndices(tile, vertexX, vertexZ, cellSizeX, cellSizeZ) {
|
|
70
|
+
const centerX = computeMeter('x', tile.cellIndexX, cellSizeX);
|
|
71
|
+
const centerZ = computeMeter('z', tile.cellIndexZ, cellSizeZ);
|
|
72
|
+
const dx = vertexX - centerX;
|
|
73
|
+
const dz = vertexZ - centerZ;
|
|
74
|
+
const eps = 1e-3;
|
|
75
|
+
const indices = [];
|
|
76
|
+
if (dz < -cellSizeZ * 0.25 + eps)
|
|
77
|
+
indices.push(0); // Top
|
|
78
|
+
if (dx > cellSizeX * 0.25 - eps)
|
|
79
|
+
indices.push(1); // Right
|
|
80
|
+
if (dz > cellSizeZ * 0.25 - eps)
|
|
81
|
+
indices.push(2); // Bottom
|
|
82
|
+
if (dx < -cellSizeX * 0.25 + eps)
|
|
83
|
+
indices.push(3); // Left
|
|
84
|
+
return indices;
|
|
85
|
+
}
|
|
86
|
+
function computeConnections(cell1, cell2, walls, tileSize) {
|
|
87
|
+
const connectedTiles = new Set();
|
|
88
|
+
const result = [];
|
|
89
|
+
while (true) {
|
|
90
|
+
let bestPotentialConnection;
|
|
91
|
+
for (const tile1 of cell1) {
|
|
92
|
+
for (const tile2 of cell2) {
|
|
93
|
+
if (result.some(([existingT1, existingT2]) => existingT1 === tile1 && existingT2 === tile2)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (isBlockedByWall(tile1, tile2, walls, tileSize)) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const heightDiff = Math.abs(tile1.y - tile2.y);
|
|
100
|
+
if (heightDiff > tileSize * MAX_AUTO_CONNECT_HEIGHT_DIFF_RATIO || tile1.type !== tile2.type) {
|
|
101
|
+
if (connectedTiles.has(tile1) || connectedTiles.has(tile2)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const newConnection = [tile1, tile2];
|
|
106
|
+
if (bestPotentialConnection != null && !isBetterConnection(newConnection, bestPotentialConnection, tileSize)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
bestPotentialConnection = [tile1, tile2];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (bestPotentialConnection == null ||
|
|
113
|
+
Math.abs(bestPotentialConnection[0].y - bestPotentialConnection[1].y) >=
|
|
114
|
+
MAX_CONNECTION_HEIGHT_DIFF_RATIO * tileSize) {
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
connectedTiles.add(bestPotentialConnection[0]);
|
|
118
|
+
connectedTiles.add(bestPotentialConnection[1]);
|
|
119
|
+
result.push(bestPotentialConnection);
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
function computeConnectedTiles(originTile, connections, filter) {
|
|
124
|
+
// Build Adjacency List for O(1) lookup
|
|
125
|
+
const adjacency = new Map();
|
|
126
|
+
for (const [t1, t2] of connections) {
|
|
127
|
+
if (!adjacency.has(t1))
|
|
128
|
+
adjacency.set(t1, []);
|
|
129
|
+
if (!adjacency.has(t2))
|
|
130
|
+
adjacency.set(t2, []);
|
|
131
|
+
adjacency.get(t1).push(t2);
|
|
132
|
+
adjacency.get(t2).push(t1);
|
|
133
|
+
}
|
|
134
|
+
const visited = new Set([originTile]);
|
|
135
|
+
const queue = [originTile];
|
|
136
|
+
while (queue.length > 0) {
|
|
137
|
+
const current = queue.shift();
|
|
138
|
+
const neighbors = adjacency.get(current);
|
|
139
|
+
if (neighbors == null)
|
|
140
|
+
continue;
|
|
141
|
+
for (const neighbor of neighbors) {
|
|
142
|
+
if (visited.has(neighbor)) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (filter?.(neighbor) === false) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
visited.add(neighbor);
|
|
149
|
+
queue.push(neighbor);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return Array.from(visited);
|
|
153
|
+
}
|
|
154
|
+
const directions = {
|
|
155
|
+
Top: 0,
|
|
156
|
+
TopRight: 1,
|
|
157
|
+
Right: 2,
|
|
158
|
+
BottomRight: 3,
|
|
159
|
+
Bottom: 4,
|
|
160
|
+
BottomLeft: 5,
|
|
161
|
+
Left: 6,
|
|
162
|
+
TopLeft: 7,
|
|
163
|
+
};
|
|
164
|
+
const directionXConnectionIndexOffsetMap = {
|
|
165
|
+
[directions.Top]: [],
|
|
166
|
+
[directions.TopRight]: [
|
|
167
|
+
[0, -1],
|
|
168
|
+
[0, 0],
|
|
169
|
+
],
|
|
170
|
+
[directions.Right]: [[0, 0]],
|
|
171
|
+
[directions.BottomRight]: [
|
|
172
|
+
[0, 0],
|
|
173
|
+
[0, 1],
|
|
174
|
+
],
|
|
175
|
+
[directions.Bottom]: [],
|
|
176
|
+
[directions.BottomLeft]: [
|
|
177
|
+
[-1, 0],
|
|
178
|
+
[-1, 1],
|
|
179
|
+
],
|
|
180
|
+
[directions.Left]: [[-1, 0]],
|
|
181
|
+
[directions.TopLeft]: [
|
|
182
|
+
[-1, -1],
|
|
183
|
+
[-1, 0],
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
const directionZConnectionIndexOffsetMap = {
|
|
187
|
+
[directions.Top]: [[0, -1]],
|
|
188
|
+
[directions.TopRight]: [
|
|
189
|
+
[0, -1],
|
|
190
|
+
[1, -1],
|
|
191
|
+
],
|
|
192
|
+
[directions.Right]: [],
|
|
193
|
+
[directions.BottomRight]: [
|
|
194
|
+
[0, 0],
|
|
195
|
+
[1, 0],
|
|
196
|
+
],
|
|
197
|
+
[directions.Bottom]: [[0, 0]],
|
|
198
|
+
[directions.BottomLeft]: [
|
|
199
|
+
[0, 0],
|
|
200
|
+
[-1, 0],
|
|
201
|
+
],
|
|
202
|
+
[directions.Left]: [],
|
|
203
|
+
[directions.TopLeft]: [
|
|
204
|
+
[0, -1],
|
|
205
|
+
[-1, -1],
|
|
206
|
+
],
|
|
207
|
+
};
|
|
208
|
+
const directionXOffsetMap = {
|
|
209
|
+
[directions.Top]: 0,
|
|
210
|
+
[directions.TopRight]: 0.5,
|
|
211
|
+
[directions.Right]: 0.5,
|
|
212
|
+
[directions.BottomRight]: 0.5,
|
|
213
|
+
[directions.Bottom]: 0,
|
|
214
|
+
[directions.BottomLeft]: -0.5,
|
|
215
|
+
[directions.Left]: -0.5,
|
|
216
|
+
[directions.TopLeft]: -0.5,
|
|
217
|
+
};
|
|
218
|
+
const directionZOffsetMap = {
|
|
219
|
+
[directions.Top]: -0.5,
|
|
220
|
+
[directions.TopRight]: -0.5,
|
|
221
|
+
[directions.Right]: 0,
|
|
222
|
+
[directions.BottomRight]: 0.5,
|
|
223
|
+
[directions.Bottom]: 0.5,
|
|
224
|
+
[directions.BottomLeft]: 0.5,
|
|
225
|
+
[directions.Left]: 0,
|
|
226
|
+
[directions.TopLeft]: -0.5,
|
|
227
|
+
};
|
|
228
|
+
const wallDirections = ['top', 'right', 'bottom', 'left'];
|
|
229
|
+
function computeMeter(type, cellIndex, cellSize, direction) {
|
|
230
|
+
const offset = direction == null ? 0 : (type === 'x' ? directionXOffsetMap : directionZOffsetMap)[direction];
|
|
231
|
+
return cellSize * (cellIndex + 0.5 + offset);
|
|
232
|
+
}
|
|
233
|
+
export class TilesGeometry extends BufferGeometry {
|
|
234
|
+
tiles;
|
|
235
|
+
walls;
|
|
236
|
+
mapSizeX;
|
|
237
|
+
mapSizeZ;
|
|
238
|
+
cellSizeX;
|
|
239
|
+
cellSizeZ;
|
|
240
|
+
vertices;
|
|
241
|
+
positions = [];
|
|
242
|
+
uvs = [];
|
|
243
|
+
textureStrengths = [];
|
|
244
|
+
idCounter = 0;
|
|
245
|
+
xConnections;
|
|
246
|
+
zConnections;
|
|
247
|
+
constructor(tiles, walls, mapSizeX, mapSizeZ) {
|
|
248
|
+
super();
|
|
249
|
+
this.tiles = tiles;
|
|
250
|
+
this.walls = walls;
|
|
251
|
+
this.mapSizeX = mapSizeX;
|
|
252
|
+
this.mapSizeZ = mapSizeZ;
|
|
253
|
+
this.cellSizeZ = mapSizeZ / tiles.length;
|
|
254
|
+
this.cellSizeX = mapSizeX / tiles[0].length;
|
|
255
|
+
this.zConnections = new Array(tiles.length - 1)
|
|
256
|
+
.fill(undefined)
|
|
257
|
+
.map((_, z) => new Array(tiles[0].length)
|
|
258
|
+
.fill(undefined)
|
|
259
|
+
.map((_, x) => computeConnections(tiles[z][x], tiles[z + 1][x], [...(walls[z]?.[x]?.bottom ?? []), ...(walls[z + 1]?.[x]?.top ?? [])], this.cellSizeZ)));
|
|
260
|
+
this.xConnections = new Array(tiles.length)
|
|
261
|
+
.fill(undefined)
|
|
262
|
+
.map((_, z) => new Array(tiles[0].length - 1)
|
|
263
|
+
.fill(undefined)
|
|
264
|
+
.map((_, x) => computeConnections(tiles[z][x], tiles[z][x + 1], [...(walls[z]?.[x]?.right ?? []), ...(walls[z]?.[x + 1]?.left ?? [])], this.cellSizeX)));
|
|
265
|
+
this.vertices = new Array(tiles.length * 2 + 1)
|
|
266
|
+
.fill(undefined)
|
|
267
|
+
.map(() => new Array(tiles[0].length * 2 + 1).fill(undefined).map(() => []));
|
|
268
|
+
const indices = [];
|
|
269
|
+
for (let tileIZ = 0; tileIZ < tiles.length; tileIZ++) {
|
|
270
|
+
const tileRow = tiles[tileIZ];
|
|
271
|
+
for (let tileIX = 0; tileIX < tileRow.length; tileIX++) {
|
|
272
|
+
const tileStack = tileRow[tileIX];
|
|
273
|
+
for (let tileIY = 0; tileIY < tileStack.length; tileIY++) {
|
|
274
|
+
const centerVertexId = this.addVertex(tileIX, tileIZ, undefined, tileStack[tileIY]);
|
|
275
|
+
for (let i = 0; i < 8; i += 2) {
|
|
276
|
+
const a = centerVertexId;
|
|
277
|
+
const b = this.getConnectionVertexId(tileIX, tileIY, tileIZ, (i + 0));
|
|
278
|
+
const c = this.getConnectionVertexId(tileIX, tileIY, tileIZ, (i + 1));
|
|
279
|
+
const d = this.getConnectionVertexId(tileIX, tileIY, tileIZ, ((i + 2) % 8));
|
|
280
|
+
// First triangle
|
|
281
|
+
indices.push(a, c, b);
|
|
282
|
+
// Second triangle
|
|
283
|
+
indices.push(a, d, c);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Set geometry attributes
|
|
289
|
+
this.setAttribute('position', new Float32BufferAttribute(this.positions, 3));
|
|
290
|
+
this.setAttribute('uv', new Float32BufferAttribute(this.uvs, 2));
|
|
291
|
+
this.setAttribute('textureStrength', new Float32BufferAttribute(this.textureStrengths, 16));
|
|
292
|
+
this.setIndex(new Uint16BufferAttribute(indices, 1));
|
|
293
|
+
this.computeVertexNormals();
|
|
294
|
+
this.computeTangents();
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* @param xIndex tile grid x index
|
|
298
|
+
* @param zIndex tile grid z index
|
|
299
|
+
* @param stackIndex index in the tile stack
|
|
300
|
+
* @param offsetX world unit offset from tile center X
|
|
301
|
+
* @param offsetZ world unit offset from tile center Z
|
|
302
|
+
* @param normalTarget optional target vector that will receive the interpolated face normal at the queried point
|
|
303
|
+
*/
|
|
304
|
+
getHeight(xIndex, zIndex, stackIndex, offsetX, offsetZ, normalTarget) {
|
|
305
|
+
if (xIndex < 0 || xIndex >= this.tiles[0].length || zIndex < 0 || zIndex >= this.tiles.length) {
|
|
306
|
+
throw new Error(`Tile index out of bounds: (${xIndex}, ${zIndex}). Grid size: ${this.tiles[0].length}x${this.tiles.length}`);
|
|
307
|
+
}
|
|
308
|
+
const vertices = this.getVertices(xIndex, zIndex);
|
|
309
|
+
const vertex1 = vertices[stackIndex];
|
|
310
|
+
if (vertex1 == null) {
|
|
311
|
+
throw new Error(`No vertex found at stack index ${stackIndex} for tile (${xIndex}, ${zIndex}).`);
|
|
312
|
+
}
|
|
313
|
+
const centerX = computeMeter('x', xIndex, this.cellSizeX) - this.mapSizeX / 2;
|
|
314
|
+
const centerZ = computeMeter('z', zIndex, this.cellSizeZ) - this.mapSizeZ / 2;
|
|
315
|
+
const worldX = centerX + offsetX;
|
|
316
|
+
const worldZ = centerZ + offsetZ;
|
|
317
|
+
let tileOffsetX = offsetX;
|
|
318
|
+
let tileOffsetZ = offsetZ;
|
|
319
|
+
const l1Norm = Math.abs(tileOffsetX) + Math.abs(tileOffsetZ);
|
|
320
|
+
if (l1Norm > 0) {
|
|
321
|
+
tileOffsetX /= l1Norm;
|
|
322
|
+
tileOffsetZ /= l1Norm;
|
|
323
|
+
}
|
|
324
|
+
let triangleIndex = Math.abs(tileOffsetX) > Math.abs(tileOffsetZ) ? 1 : 0;
|
|
325
|
+
if (tileOffsetZ > 0) {
|
|
326
|
+
triangleIndex = 3 - triangleIndex;
|
|
327
|
+
}
|
|
328
|
+
if (tileOffsetX < 0) {
|
|
329
|
+
triangleIndex = 7 - triangleIndex;
|
|
330
|
+
}
|
|
331
|
+
const directionVertex2 = triangleIndex;
|
|
332
|
+
const directionVertex3 = ((triangleIndex + 1) % 8);
|
|
333
|
+
const tile = vertex1.relatedTiles[0];
|
|
334
|
+
const vertex2 = this.getVertices(xIndex, zIndex, directionVertex2).find((vertex) => vertex.relatedTiles.includes(tile));
|
|
335
|
+
const vertex3 = this.getVertices(xIndex, zIndex, directionVertex3).find((vertex) => vertex.relatedTiles.includes(tile));
|
|
336
|
+
if (vertex2 == null || vertex3 == null) {
|
|
337
|
+
throw new Error(`Failed to find vertices for triangle at tile (${xIndex}, ${zIndex}) stack ${stackIndex} direction ${triangleIndex}.`);
|
|
338
|
+
}
|
|
339
|
+
const [x1, y1_pos, z1] = vertex1.position;
|
|
340
|
+
const [x2, y2_pos, z2] = vertex2.position;
|
|
341
|
+
const [x3, y3_pos, z3] = vertex3.position;
|
|
342
|
+
const denom = (z2 - z3) * (x1 - x3) + (x3 - x2) * (z1 - z3);
|
|
343
|
+
let w1 = ((z2 - z3) * (worldX - x3) + (x3 - x2) * (worldZ - z3)) / denom;
|
|
344
|
+
let w2 = ((z3 - z1) * (worldX - x3) + (x1 - x3) * (worldZ - z3)) / denom;
|
|
345
|
+
// Handle potential precision issues or 0 denom (degenerate triangles shouldn't happen but safe to check?)
|
|
346
|
+
if (!Number.isFinite(w1))
|
|
347
|
+
w1 = 0;
|
|
348
|
+
if (!Number.isFinite(w2))
|
|
349
|
+
w2 = 0;
|
|
350
|
+
const w3 = 1 - w1 - w2;
|
|
351
|
+
if (normalTarget != null) {
|
|
352
|
+
const ux = x2 - x1;
|
|
353
|
+
const uy = y2_pos - y1_pos;
|
|
354
|
+
const uz = z2 - z1;
|
|
355
|
+
const vx = x3 - x1;
|
|
356
|
+
const vy = y3_pos - y1_pos;
|
|
357
|
+
const vz = z3 - z1;
|
|
358
|
+
const nx = uy * vz - uz * vy;
|
|
359
|
+
const ny = uz * vx - ux * vz;
|
|
360
|
+
const nz = ux * vy - uy * vx;
|
|
361
|
+
const lenSq = nx * nx + ny * ny + nz * nz;
|
|
362
|
+
if (lenSq > 0) {
|
|
363
|
+
normalTarget.set(nx, ny, nz).normalize().negate();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return w1 * y1_pos + w2 * y2_pos + w3 * y3_pos;
|
|
367
|
+
}
|
|
368
|
+
addVertex(cellIndexX, cellIndexZ, direction, tile, connections) {
|
|
369
|
+
const id = this.idCounter++;
|
|
370
|
+
const xMeter = computeMeter('x', cellIndexX, this.cellSizeX, direction);
|
|
371
|
+
const zMeter = computeMeter('z', cellIndexZ, this.cellSizeZ, direction);
|
|
372
|
+
let filteredRelatedTiles;
|
|
373
|
+
let relatedTiles;
|
|
374
|
+
if (connections == null) {
|
|
375
|
+
filteredRelatedTiles = [tile];
|
|
376
|
+
relatedTiles = [tile];
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
relatedTiles = computeConnectedTiles(tile, connections);
|
|
380
|
+
const removeCoordinateSet = new Set();
|
|
381
|
+
let blockingTile;
|
|
382
|
+
for (const relatedTile of relatedTiles) {
|
|
383
|
+
const tileWalls = this.walls[relatedTile.cellIndexZ]?.[relatedTile.cellIndexX];
|
|
384
|
+
if (!tileWalls)
|
|
385
|
+
continue;
|
|
386
|
+
const relevantIndices = getRelevantWallIndices(relatedTile, xMeter, zMeter, this.cellSizeX, this.cellSizeZ);
|
|
387
|
+
let causesBlocking = false;
|
|
388
|
+
for (const i of relevantIndices) {
|
|
389
|
+
const dir = wallDirections[i];
|
|
390
|
+
const wallSize = i % 2 === 0 ? this.cellSizeZ : this.cellSizeX;
|
|
391
|
+
const hasBlockingWall = tileWalls[dir].some((wall) => isBlockingWall(wall, relatedTile, wallSize));
|
|
392
|
+
if (hasBlockingWall) {
|
|
393
|
+
causesBlocking = true;
|
|
394
|
+
// Remove neighbor in direction i
|
|
395
|
+
const nx = relatedTile.cellIndexX + directionXOffsetMap[(i * 2)] * 2;
|
|
396
|
+
const nz = relatedTile.cellIndexZ + directionZOffsetMap[(i * 2)] * 2;
|
|
397
|
+
removeCoordinateSet.add(`${nx},${nz}`);
|
|
398
|
+
// Check secondary blocking (diagonal)
|
|
399
|
+
const nextI = (i + 1) % 4;
|
|
400
|
+
const nextDir = wallDirections[nextI];
|
|
401
|
+
const nextWallSize = nextI % 2 === 0 ? this.cellSizeZ : this.cellSizeX;
|
|
402
|
+
const hasNextBlockingWall = tileWalls[nextDir].some((wall) => isBlockingWall(wall, relatedTile, nextWallSize));
|
|
403
|
+
if (hasNextBlockingWall) {
|
|
404
|
+
const diagDir = (i * 2 + 1);
|
|
405
|
+
const ndx = relatedTile.cellIndexX + directionXOffsetMap[diagDir] * 2;
|
|
406
|
+
const ndz = relatedTile.cellIndexZ + directionZOffsetMap[diagDir] * 2;
|
|
407
|
+
removeCoordinateSet.add(`${ndx},${ndz}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (causesBlocking && blockingTile == null) {
|
|
412
|
+
blockingTile = relatedTile;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
filteredRelatedTiles = computeConnectedTiles(blockingTile ?? tile, connections, (newTile) => !removeCoordinateSet.has(`${newTile.cellIndexX},${newTile.cellIndexZ}`));
|
|
416
|
+
}
|
|
417
|
+
const yMeter = filteredRelatedTiles.length === 0
|
|
418
|
+
? tile.y
|
|
419
|
+
: filteredRelatedTiles.reduce((prev, t) => prev + t.y, 0) / filteredRelatedTiles.length;
|
|
420
|
+
const textureStrength = new Array(16).fill(0);
|
|
421
|
+
const strength = relatedTiles.length === 1 ? 1 : 0.5;
|
|
422
|
+
for (const { textureId } of relatedTiles) {
|
|
423
|
+
textureStrength[textureId] = strength;
|
|
424
|
+
}
|
|
425
|
+
this.textureStrengths.push(...textureStrength);
|
|
426
|
+
const position = [xMeter - this.mapSizeX * 0.5, yMeter, zMeter - this.mapSizeZ * 0.5];
|
|
427
|
+
this.positions.push(...position);
|
|
428
|
+
const uv = [xMeter, zMeter];
|
|
429
|
+
this.uvs.push(...uv);
|
|
430
|
+
this.getVertices(cellIndexX, cellIndexZ, direction).push({
|
|
431
|
+
position,
|
|
432
|
+
textureStrength,
|
|
433
|
+
uv,
|
|
434
|
+
relatedTiles,
|
|
435
|
+
id,
|
|
436
|
+
});
|
|
437
|
+
return id;
|
|
438
|
+
}
|
|
439
|
+
getVertices(cellIndexX, cellIndexZ, direction) {
|
|
440
|
+
let vertexIndexX = cellIndexX * 2 + 1;
|
|
441
|
+
let vertexIndexZ = cellIndexZ * 2 + 1;
|
|
442
|
+
if (direction != null) {
|
|
443
|
+
vertexIndexX += directionXOffsetMap[direction] * 2;
|
|
444
|
+
vertexIndexZ += directionZOffsetMap[direction] * 2;
|
|
445
|
+
}
|
|
446
|
+
return this.vertices[vertexIndexZ][vertexIndexX];
|
|
447
|
+
}
|
|
448
|
+
getConnectionVertexId(cellIndexX, cellIndeY, cellIndexZ, direction) {
|
|
449
|
+
const tile = this.tiles[cellIndexZ][cellIndexX][cellIndeY];
|
|
450
|
+
let vertexId = this.getVertices(cellIndexX, cellIndexZ, direction).find(({ relatedTiles: connectedTiles }) => connectedTiles.includes(tile))?.id;
|
|
451
|
+
if (vertexId == null) {
|
|
452
|
+
const connections = [];
|
|
453
|
+
for (const [xOffset, zOffset] of directionXConnectionIndexOffsetMap[direction]) {
|
|
454
|
+
connections.push(...(this.xConnections[zOffset + cellIndexZ]?.[xOffset + cellIndexX] ?? []));
|
|
455
|
+
}
|
|
456
|
+
for (const [xOffset, zOffset] of directionZConnectionIndexOffsetMap[direction]) {
|
|
457
|
+
connections.push(...(this.zConnections[zOffset + cellIndexZ]?.[xOffset + cellIndexX] ?? []));
|
|
458
|
+
}
|
|
459
|
+
vertexId = this.addVertex(cellIndexX, cellIndexZ, direction, tile, connections);
|
|
460
|
+
}
|
|
461
|
+
return vertexId;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Material, Mesh } from 'three';
|
|
2
|
+
import { Interpreter } from '../interpreter.js';
|
|
3
|
+
import { coerce } from 'zod';
|
|
4
|
+
export declare const groundSchema: import("zod").ZodObject<{
|
|
5
|
+
texture: import("zod").ZodString;
|
|
6
|
+
y: coerce.ZodCoercedNumber<unknown>;
|
|
7
|
+
}, import("zod/v4/core").$strip>;
|
|
8
|
+
export declare const ceilingSchema: import("zod").ZodObject<{
|
|
9
|
+
texture: import("zod").ZodString;
|
|
10
|
+
y: coerce.ZodCoercedNumber<unknown>;
|
|
11
|
+
}, import("zod/v4/core").$strip>;
|
|
12
|
+
export type TilesMeshOptions = {
|
|
13
|
+
materialSharpness?: number;
|
|
14
|
+
};
|
|
15
|
+
export declare class TilesMesh extends Mesh {
|
|
16
|
+
constructor(interpreter: Interpreter, material?: Material, options?: TilesMeshOptions);
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tiles/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,QAAQ,EACR,IAAI,EAOL,MAAM,OAAO,CAAA;AACd,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,MAAM,EAAkB,MAAM,KAAK,CAAA;AAO5C,eAAO,MAAM,YAAY;;;gCAAoD,CAAA;AAC7E,eAAO,MAAM,aAAa;;;gCAAoD,CAAA;AAE9E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B,CAAA;AAyCD,qBAAa,SAAU,SAAQ,IAAI;gBACrB,WAAW,EAAE,WAAW,EAAE,QAAQ,GAAE,QAAkC,EAAE,OAAO,GAAE,gBAAqB;CA6GnH"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Mesh, Texture, MeshBasicMaterial, } from 'three';
|
|
2
|
+
import { coerce, object, string } from 'zod';
|
|
3
|
+
import { TilesGeometry } from './geometry.js';
|
|
4
|
+
import { buildTilesMaterial } from './material.js';
|
|
5
|
+
import { ChartaError } from '../errors.js';
|
|
6
|
+
import { wallSchema } from '../walls/index.js';
|
|
7
|
+
import { buildTextureArrayFromAssets } from '../utils/texture.js';
|
|
8
|
+
export const groundSchema = object({ texture: string(), y: coerce.number() });
|
|
9
|
+
export const ceilingSchema = object({ texture: string(), y: coerce.number() });
|
|
10
|
+
function resolveHeights(position, items, interpreter, reverse) {
|
|
11
|
+
items = [...items];
|
|
12
|
+
if (reverse) {
|
|
13
|
+
items.reverse();
|
|
14
|
+
}
|
|
15
|
+
let lastY;
|
|
16
|
+
const result = [...items].map((item) => {
|
|
17
|
+
if (item.layerY != null) {
|
|
18
|
+
lastY = item.layerY;
|
|
19
|
+
return item.layerY;
|
|
20
|
+
}
|
|
21
|
+
if (item.explicitWallY != null) {
|
|
22
|
+
return item.explicitWallY;
|
|
23
|
+
}
|
|
24
|
+
if (lastY == null) {
|
|
25
|
+
interpreter.reportError(new ChartaError(`Wall at ${position.join('/')}: ${reverse ? 'missing topY and no susequent layer' : 'missing bottomY and no preceding layer'}`, interpreter.getSource(), item.loc));
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
return lastY;
|
|
29
|
+
});
|
|
30
|
+
if (reverse) {
|
|
31
|
+
result.reverse();
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
export class TilesMesh extends Mesh {
|
|
36
|
+
constructor(interpreter, material = new MeshBasicMaterial(), options = {}) {
|
|
37
|
+
super();
|
|
38
|
+
const rows = interpreter.getRows();
|
|
39
|
+
const cols = interpreter.getCols();
|
|
40
|
+
// Helper to manage texture indices on the fly
|
|
41
|
+
const textureIndex = new Map();
|
|
42
|
+
const getTextureIndex = (name, loc) => {
|
|
43
|
+
const texture = interpreter.getAsset(Texture, `${name}BaseColorTexture`, loc);
|
|
44
|
+
if (!texture)
|
|
45
|
+
return 0;
|
|
46
|
+
let idx = textureIndex.get(texture);
|
|
47
|
+
if (idx == null) {
|
|
48
|
+
idx = textureIndex.size;
|
|
49
|
+
textureIndex.set(texture, idx);
|
|
50
|
+
}
|
|
51
|
+
return idx;
|
|
52
|
+
};
|
|
53
|
+
// Initialize grid structures
|
|
54
|
+
const tiles = Array.from({ length: rows }, () => Array.from({ length: cols }, () => []));
|
|
55
|
+
const walls = Array.from({ length: rows }, () => Array.from({ length: cols }, () => ({
|
|
56
|
+
top: [],
|
|
57
|
+
bottom: [],
|
|
58
|
+
left: [],
|
|
59
|
+
right: [],
|
|
60
|
+
})));
|
|
61
|
+
for (let z = 0; z < rows; z++) {
|
|
62
|
+
for (let x = 0; x < cols; x++) {
|
|
63
|
+
// 1. Collect all entries for this cell
|
|
64
|
+
const calls = Array.from(interpreter.getCalls([z, x], {
|
|
65
|
+
ground: groundSchema,
|
|
66
|
+
ceiling: ceilingSchema,
|
|
67
|
+
wall: wallSchema,
|
|
68
|
+
}));
|
|
69
|
+
// 2. Compute wall heights using the helper
|
|
70
|
+
const bottomYs = resolveHeights([z, x], calls.map(([_1, parsed, _2, loc]) => ({
|
|
71
|
+
loc,
|
|
72
|
+
layerY: 'y' in parsed ? parsed.y : undefined,
|
|
73
|
+
explicitWallY: 'bottomY' in parsed ? parsed.bottomY : undefined,
|
|
74
|
+
})), interpreter, false);
|
|
75
|
+
const topYs = resolveHeights([z, x], calls.map(([_1, parsed, _2, loc]) => ({
|
|
76
|
+
loc,
|
|
77
|
+
layerY: 'y' in parsed ? parsed.y : undefined,
|
|
78
|
+
explicitWallY: 'topY' in parsed ? parsed.topY : undefined,
|
|
79
|
+
})), interpreter, true);
|
|
80
|
+
// 3. Populate data structures
|
|
81
|
+
for (let i = 0; i < calls.length; i++) {
|
|
82
|
+
const [name, parsed, _, loc] = calls[i];
|
|
83
|
+
if (name === 'wall') {
|
|
84
|
+
let bottomY = bottomYs[i];
|
|
85
|
+
let topY = topYs[i];
|
|
86
|
+
if (bottomY === undefined || topY === undefined)
|
|
87
|
+
continue;
|
|
88
|
+
// Normalize heights so bottom is numerically lower
|
|
89
|
+
if (topY < bottomY) {
|
|
90
|
+
;
|
|
91
|
+
[bottomY, topY] = [topY, bottomY];
|
|
92
|
+
}
|
|
93
|
+
const dir = parsed.dir;
|
|
94
|
+
if (walls[z][x][dir]) {
|
|
95
|
+
walls[z][x][dir].push({ bottomY, topY });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// Ground or Ceiling
|
|
100
|
+
tiles[z][x].push({
|
|
101
|
+
type: name,
|
|
102
|
+
y: parsed.y,
|
|
103
|
+
textureId: getTextureIndex(parsed.texture, loc),
|
|
104
|
+
cellIndexX: x,
|
|
105
|
+
cellIndexZ: z,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const cellSize = interpreter.getCellSize();
|
|
112
|
+
const geometry = new TilesGeometry(tiles, walls, cols * cellSize, rows * cellSize);
|
|
113
|
+
this.geometry = geometry;
|
|
114
|
+
interpreter.setAsset('tilesGeometry', geometry);
|
|
115
|
+
// Build texture array from interpreter assets using encountered material names in insertion order
|
|
116
|
+
const texturesInOrder = Array.from(textureIndex.keys()).sort((a, b) => textureIndex.get(a) - textureIndex.get(b));
|
|
117
|
+
if (texturesInOrder.length > 0) {
|
|
118
|
+
this.material = buildTilesMaterial(material, buildTextureArrayFromAssets(texturesInOrder));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { DataArrayTexture, Material } from "three";
|
|
2
|
+
/**
|
|
3
|
+
* Modifies an existing Three.js material to support texture array blending based on textureStrength
|
|
4
|
+
*/
|
|
5
|
+
export declare function buildTilesMaterial<T extends Material>(material: T, textureArray: DataArrayTexture): T;
|
|
6
|
+
//# sourceMappingURL=material.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"material.d.ts","sourceRoot":"","sources":["../../src/tiles/material.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEnD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,QAAQ,EACnD,QAAQ,EAAE,CAAC,EACX,YAAY,EAAE,gBAAgB,GAC7B,CAAC,CAoGH"}
|