@aethervtt/shard-grid 0.0.0-stage → 0.1.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 +21 -0
- package/README.md +2 -2
- package/dist/grid.d.ts +36 -0
- package/dist/grid.js +240 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +54 -0
- package/dist/math.d.ts +61 -0
- package/dist/math.js +225 -0
- package/dist/plugin.d.ts +5 -0
- package/dist/plugin.js +29 -0
- package/package.json +39 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Patrick Dias
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @aethervtt/shard-grid
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Part of [Shard](https://github.com/aethervtt/shard), a WebGPU-first 2D/3D game engine in TypeScript. See the repository for documentation.
|
package/dist/grid.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type AssetRef, type Entity, type World } from '@aethervtt/shard-core';
|
|
2
|
+
export declare const GRID_KINDS: readonly ['square', 'hex'];
|
|
3
|
+
export declare const HEX_ORIENTATIONS: readonly ['pointy', 'flat'];
|
|
4
|
+
export declare const Grid: import("@aethervtt/shard-core").ComponentDef<{
|
|
5
|
+
readonly kind: import("@aethervtt/shard-core").FieldType<"hex" | "square", "u8">;
|
|
6
|
+
readonly orientation: import("@aethervtt/shard-core").FieldType<"flat" | "pointy", "u8">;
|
|
7
|
+
readonly size: import("@aethervtt/shard-core").FieldType<number, "f32">;
|
|
8
|
+
readonly offset: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec2, "f32">;
|
|
9
|
+
readonly color: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Color, "f32">;
|
|
10
|
+
readonly opacity: import("@aethervtt/shard-core").FieldType<number, "f32">;
|
|
11
|
+
readonly lineWidth: import("@aethervtt/shard-core").FieldType<number, "f32">;
|
|
12
|
+
readonly extent: import("@aethervtt/shard-core").FieldType<import("@aethervtt/shard-core").Vec2, "f32">;
|
|
13
|
+
}>;
|
|
14
|
+
export declare const GridLines: import("@aethervtt/shard-render").MaterialType;
|
|
15
|
+
export declare const GRID_SHADERS: Record<string, string>;
|
|
16
|
+
/** The quad and material a Grid draws with. */
|
|
17
|
+
export declare const GridQuad: import("@aethervtt/shard-core").ComponentDef<{
|
|
18
|
+
readonly grid: import("@aethervtt/shard-core").FieldType<number | null, "f64">;
|
|
19
|
+
}>;
|
|
20
|
+
interface GridRecord {
|
|
21
|
+
quad: Entity;
|
|
22
|
+
material: AssetRef<'Material'>;
|
|
23
|
+
}
|
|
24
|
+
export interface GridStateValue {
|
|
25
|
+
grids: Map<Entity, GridRecord>;
|
|
26
|
+
quad: AssetRef<'Mesh'> | undefined;
|
|
27
|
+
/** Grids removed since the last sync. */
|
|
28
|
+
removed: Entity[];
|
|
29
|
+
}
|
|
30
|
+
export declare const GridState: import("@aethervtt/shard-core").ResourceDef<GridStateValue>;
|
|
31
|
+
/** Keeps each Grid's quad and material in step with it; drops them when the Grid goes. */
|
|
32
|
+
export declare const syncGrids: import("@aethervtt/shard-core").SystemDef<{
|
|
33
|
+
q: import("@aethervtt/shard-core").Query;
|
|
34
|
+
}>;
|
|
35
|
+
export declare function observeGridRemovals(world: World): void;
|
|
36
|
+
export {};
|
package/dist/grid.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChildOf,
|
|
3
|
+
defineComponent,
|
|
4
|
+
defineResource,
|
|
5
|
+
defineSystem,
|
|
6
|
+
onRemove,
|
|
7
|
+
t
|
|
8
|
+
} from "@aethervtt/shard-core";
|
|
9
|
+
import { plane } from "@aethervtt/shard-mesh";
|
|
10
|
+
import {
|
|
11
|
+
defineMaterial,
|
|
12
|
+
GpuAssetsResource,
|
|
13
|
+
GROUND_BANDS,
|
|
14
|
+
GroundLayer,
|
|
15
|
+
MaterialAsset,
|
|
16
|
+
Materials,
|
|
17
|
+
Mesh3d,
|
|
18
|
+
Meshes,
|
|
19
|
+
MeshMaterial,
|
|
20
|
+
NotShadowCaster
|
|
21
|
+
} from "@aethervtt/shard-render";
|
|
22
|
+
import { Transform } from "@aethervtt/shard-transform";
|
|
23
|
+
const GRID_KINDS = ["square", "hex"];
|
|
24
|
+
const HEX_ORIENTATIONS = ["pointy", "flat"];
|
|
25
|
+
const Grid = defineComponent(
|
|
26
|
+
"grid/Grid",
|
|
27
|
+
{
|
|
28
|
+
kind: t.enum(GRID_KINDS, { description: "Square cells, or hexes." }),
|
|
29
|
+
orientation: t.enum(HEX_ORIENTATIONS, {
|
|
30
|
+
description: "Hex grids: pointy-top (rows of hexes) or flat-top (columns)."
|
|
31
|
+
}),
|
|
32
|
+
size: t.f32({
|
|
33
|
+
default: 1.5,
|
|
34
|
+
min: 1e-4,
|
|
35
|
+
unit: "m",
|
|
36
|
+
description: "Square: cell edge. Hex: distance between adjacent cell centres. Visual: a host converts it with its one fixed scale; game distance never sets it."
|
|
37
|
+
}),
|
|
38
|
+
offset: t.vec2({
|
|
39
|
+
description: "Where cell (0, 0) starts (square) or is centred (hex), world (x, z)."
|
|
40
|
+
}),
|
|
41
|
+
color: t.color({ default: [0, 0, 0, 1], description: "Line color (linear)." }),
|
|
42
|
+
opacity: t.f32({ default: 0.5, min: 0, max: 1, description: "Line opacity." }),
|
|
43
|
+
lineWidth: t.f32({
|
|
44
|
+
default: 1,
|
|
45
|
+
min: 0,
|
|
46
|
+
unit: "px",
|
|
47
|
+
description: "Line width in CSS pixels, at every zoom and display density."
|
|
48
|
+
}),
|
|
49
|
+
extent: t.vec2({
|
|
50
|
+
default: [100, 100],
|
|
51
|
+
description: "Size of the drawn area, (x, z) in world units, centred on the entity."
|
|
52
|
+
})
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
description: "A square or hex grid drawn as one quad in the grid band (20) at the entity's height. Game distance (per cell, unit, diagonal rule) isn't here: it measures (grid/math distance), it never draws.",
|
|
56
|
+
requires: [Transform]
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
const GridLines = defineMaterial("grid/GridLines", {
|
|
60
|
+
extends: "none",
|
|
61
|
+
blend: "premultiplied",
|
|
62
|
+
// Lines over the table, not a thing on it: picks reach the tiles and floor under them.
|
|
63
|
+
pickable: false,
|
|
64
|
+
fields: {
|
|
65
|
+
color: t.color({ default: [0, 0, 0, 1], description: "Line color (linear), alpha included." }),
|
|
66
|
+
lineWidth: t.f32({ default: 1, min: 0, description: "CSS pixels." }),
|
|
67
|
+
size: t.f32({ default: 1, min: 1e-4, description: "World units." }),
|
|
68
|
+
offset: t.vec2({ description: "World (x, z)." }),
|
|
69
|
+
mode: t.f32({ description: "0 square, 1 pointy hex, 2 flat hex." })
|
|
70
|
+
},
|
|
71
|
+
shader: "grid::lines",
|
|
72
|
+
description: "Analytic grid lines of constant CSS-pixel width (0057). One per Grid."
|
|
73
|
+
});
|
|
74
|
+
const GRID_SHADERS = {
|
|
75
|
+
"grid::lines": `
|
|
76
|
+
import shard::pbr::types::VertexOutput;
|
|
77
|
+
import shard::view::view;
|
|
78
|
+
import material::grid_lines::GridLines;
|
|
79
|
+
|
|
80
|
+
const SQRT3: f32 = 1.7320508;
|
|
81
|
+
|
|
82
|
+
/** World distance to the nearest edge of a pointy-top hex tiling, and that edge's normal. */
|
|
83
|
+
fn hex_edge(p: vec2f, s: f32) -> vec3f {
|
|
84
|
+
let r = (2.0 * p.y) / (SQRT3 * s);
|
|
85
|
+
let q = p.x / s - r / 2.0;
|
|
86
|
+
let x = q;
|
|
87
|
+
let z = r;
|
|
88
|
+
let y = -x - z;
|
|
89
|
+
var rx = round(x);
|
|
90
|
+
var ry = round(y);
|
|
91
|
+
var rz = round(z);
|
|
92
|
+
let dx = abs(rx - x);
|
|
93
|
+
let dy = abs(ry - y);
|
|
94
|
+
let dz = abs(rz - z);
|
|
95
|
+
if (dx > dy && dx > dz) { rx = -ry - rz; } else if (dy > dz) { ry = -rx - rz; } else { rz = -rx - ry; }
|
|
96
|
+
let c = vec2f(s * (rx + rz / 2.0), s * (SQRT3 / 2.0) * rz);
|
|
97
|
+
let d = p - c;
|
|
98
|
+
let a = abs(d);
|
|
99
|
+
let sg = select(vec2f(-1.0), vec2f(1.0), d >= vec2f(0.0));
|
|
100
|
+
// Edge normals of a pointy-top hex: 0\xB0 and \xB160\xB0 (inradius s / 2).
|
|
101
|
+
let slant = dot(a, vec2f(0.5, SQRT3 / 2.0));
|
|
102
|
+
if (a.x >= slant) { return vec3f(s * 0.5 - a.x, sg.x, 0.0); }
|
|
103
|
+
return vec3f(s * 0.5 - slant, sg.x * 0.5, sg.y * SQRT3 / 2.0);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Pixels from the line: the world distance over world units per pixel along the normal. */
|
|
107
|
+
fn pixels(d: f32, n: vec2f, p: vec2f) -> f32 {
|
|
108
|
+
let g = vec2f(dot(dpdx(p), n), dot(dpdy(p), n));
|
|
109
|
+
return d / max(length(g), 1e-8);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
override fn shade(in: VertexOutput) -> vec4f {
|
|
113
|
+
let p = in.world_position.xz - GridLines.offset;
|
|
114
|
+
let s = GridLines.size;
|
|
115
|
+
var d_px: f32;
|
|
116
|
+
var spacing: f32;
|
|
117
|
+
if (GridLines.mode < 0.5) {
|
|
118
|
+
let f = abs(fract(p / s + 0.5) - 0.5) * s;
|
|
119
|
+
let dx = pixels(f.x, vec2f(1.0, 0.0), p);
|
|
120
|
+
let dz = pixels(f.y, vec2f(0.0, 1.0), p);
|
|
121
|
+
d_px = min(dx, dz);
|
|
122
|
+
spacing = min(pixels(s, vec2f(1.0, 0.0), p), pixels(s, vec2f(0.0, 1.0), p));
|
|
123
|
+
} else {
|
|
124
|
+
// Flat-top hexes are pointy-top ones with x and z swapped.
|
|
125
|
+
let flat = GridLines.mode > 1.5;
|
|
126
|
+
let q = select(p, p.yx, flat);
|
|
127
|
+
let e = hex_edge(q, s);
|
|
128
|
+
let n = select(e.yz, e.zy, flat);
|
|
129
|
+
d_px = pixels(e.x, n, p);
|
|
130
|
+
spacing = pixels(s, n, p);
|
|
131
|
+
}
|
|
132
|
+
let width = GridLines.lineWidth * view.pixelScale.x;
|
|
133
|
+
var a = clamp(width * 0.5 + 0.5 - d_px, 0.0, 1.0);
|
|
134
|
+
a *= smoothstep(3.0, 6.0, spacing) * GridLines.color.a;
|
|
135
|
+
if (a <= 0.0) { discard; }
|
|
136
|
+
// Display-referred, like any overlay: the scene's exposure doesn't dim it.
|
|
137
|
+
return vec4f(GridLines.color.rgb * a / view.exposure, a);
|
|
138
|
+
}`
|
|
139
|
+
};
|
|
140
|
+
const GridQuad = defineComponent(
|
|
141
|
+
"grid/GridQuad",
|
|
142
|
+
{ grid: t.entity({ readonly: true, description: "The Grid this quad draws." }) },
|
|
143
|
+
{
|
|
144
|
+
description: "A Grid's quad: spawned and kept by the grid plugin.",
|
|
145
|
+
serialize: false,
|
|
146
|
+
save: false
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
const GridState = defineResource("grid/State", {
|
|
150
|
+
description: "Each Grid entity's quad and material.",
|
|
151
|
+
init: () => ({ grids: /* @__PURE__ */ new Map(), quad: void 0, removed: [] })
|
|
152
|
+
});
|
|
153
|
+
const MODES = { square: 0, pointy: 1, flat: 2 };
|
|
154
|
+
function lineValues(v) {
|
|
155
|
+
return {
|
|
156
|
+
color: [v.color[0], v.color[1], v.color[2], v.color[3] * v.opacity],
|
|
157
|
+
lineWidth: v.lineWidth,
|
|
158
|
+
size: v.size,
|
|
159
|
+
offset: [v.offset[0], v.offset[1]],
|
|
160
|
+
mode: v.kind === "square" ? MODES.square : MODES[v.orientation]
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const syncGrids = defineSystem({
|
|
164
|
+
name: "grid/sync",
|
|
165
|
+
description: "Keeps each Grid's quad (size, band) and line material in step with the Grid.",
|
|
166
|
+
setup: (world) => ({ q: world.query({ with: [Grid] }) }),
|
|
167
|
+
run: ({ q }, world, ctx) => {
|
|
168
|
+
const state = world.resource(GridState);
|
|
169
|
+
if (state.removed.length > 0) {
|
|
170
|
+
for (const e of state.removed) dropGrid(world, state, e);
|
|
171
|
+
state.removed.length = 0;
|
|
172
|
+
}
|
|
173
|
+
const since = ctx.lastRunTick;
|
|
174
|
+
for (const table of q.tables) {
|
|
175
|
+
const grounded = table.has(GroundLayer);
|
|
176
|
+
if (table.lastChanged(Grid) <= since && !(grounded && table.lastChanged(GroundLayer) > since))
|
|
177
|
+
continue;
|
|
178
|
+
const ticks = table.changedTicks(Grid);
|
|
179
|
+
const groundTicks = grounded ? table.changedTicks(GroundLayer) : void 0;
|
|
180
|
+
for (let row = 0; row < table.count; row++) {
|
|
181
|
+
if (ticks[row] <= since && !(groundTicks && groundTicks[row] > since)) continue;
|
|
182
|
+
updateGrid(world, state, table.entities[row]);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
function updateGrid(world, state, e) {
|
|
188
|
+
const v = world.get(e, Grid);
|
|
189
|
+
const band = world.tryGet(e, GroundLayer) ?? { band: GROUND_BANDS.grid, order: 0 };
|
|
190
|
+
let rec = state.grids.get(e);
|
|
191
|
+
const materials = world.resource(Materials);
|
|
192
|
+
if (!rec) {
|
|
193
|
+
state.quad ??= world.resource(Meshes).add(plane({ size: 1 }), "grid:quad");
|
|
194
|
+
const material = materials.add(
|
|
195
|
+
new MaterialAsset(lineValues(v), GridLines),
|
|
196
|
+
`grid:lines/${e}`
|
|
197
|
+
);
|
|
198
|
+
const quad = world.spawn(
|
|
199
|
+
[Mesh3d, { mesh: state.quad }],
|
|
200
|
+
[MeshMaterial, { material }],
|
|
201
|
+
[GroundLayer, band],
|
|
202
|
+
[GridQuad, { grid: e }],
|
|
203
|
+
NotShadowCaster,
|
|
204
|
+
Transform,
|
|
205
|
+
[ChildOf, { parent: e }]
|
|
206
|
+
);
|
|
207
|
+
rec = { quad, material };
|
|
208
|
+
state.grids.set(e, rec);
|
|
209
|
+
} else {
|
|
210
|
+
materials.get(rec.material)?.set(lineValues(v));
|
|
211
|
+
world.set(rec.quad, GroundLayer, band);
|
|
212
|
+
}
|
|
213
|
+
world.set(rec.quad, Transform, { scale: [v.extent[0], 1, v.extent[1]] });
|
|
214
|
+
}
|
|
215
|
+
function dropGrid(world, state, e) {
|
|
216
|
+
const rec = state.grids.get(e);
|
|
217
|
+
if (!rec) return;
|
|
218
|
+
state.grids.delete(e);
|
|
219
|
+
if (world.isAlive(rec.quad)) world.despawn(rec.quad);
|
|
220
|
+
const materials = world.resource(Materials);
|
|
221
|
+
const material = materials.get(rec.material);
|
|
222
|
+
if (material) world.tryResource(GpuAssetsResource)?.releaseMaterial(material);
|
|
223
|
+
materials.delete(rec.material.guid);
|
|
224
|
+
}
|
|
225
|
+
function observeGridRemovals(world) {
|
|
226
|
+
world.observe(onRemove(Grid), ({ entity, world: w }) => {
|
|
227
|
+
w.tryResource(GridState)?.removed.push(entity);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
export {
|
|
231
|
+
GRID_KINDS,
|
|
232
|
+
GRID_SHADERS,
|
|
233
|
+
Grid,
|
|
234
|
+
GridLines,
|
|
235
|
+
GridQuad,
|
|
236
|
+
GridState,
|
|
237
|
+
HEX_ORIENTATIONS,
|
|
238
|
+
observeGridRemovals,
|
|
239
|
+
syncGrids
|
|
240
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { GRID_KINDS, GRID_SHADERS, Grid, GridLines, GridQuad, GridState, type GridStateValue, HEX_ORIENTATIONS, syncGrids, } from './grid.js';
|
|
2
|
+
export { axialToCube, type Cell, cellAt, cellCenter, cellPolygon, cubeToAxial, type DiagonalRule, distance, edgeMidpoints, type GridGeometry, type GridKind, gridVertex, type HexOrientation, hexDistance, neighbors, pathDistance, pointToAxial, roundAxial, tokenCenter, wallAnchor, } from './math.js';
|
|
3
|
+
export { gridPlugin } from './plugin.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GRID_KINDS,
|
|
3
|
+
GRID_SHADERS,
|
|
4
|
+
Grid,
|
|
5
|
+
GridLines,
|
|
6
|
+
GridQuad,
|
|
7
|
+
GridState,
|
|
8
|
+
HEX_ORIENTATIONS,
|
|
9
|
+
syncGrids
|
|
10
|
+
} from "./grid.js";
|
|
11
|
+
import {
|
|
12
|
+
axialToCube,
|
|
13
|
+
cellAt,
|
|
14
|
+
cellCenter,
|
|
15
|
+
cellPolygon,
|
|
16
|
+
cubeToAxial,
|
|
17
|
+
distance,
|
|
18
|
+
edgeMidpoints,
|
|
19
|
+
gridVertex,
|
|
20
|
+
hexDistance,
|
|
21
|
+
neighbors,
|
|
22
|
+
pathDistance,
|
|
23
|
+
pointToAxial,
|
|
24
|
+
roundAxial,
|
|
25
|
+
tokenCenter,
|
|
26
|
+
wallAnchor
|
|
27
|
+
} from "./math.js";
|
|
28
|
+
import { gridPlugin } from "./plugin.js";
|
|
29
|
+
export {
|
|
30
|
+
GRID_KINDS,
|
|
31
|
+
GRID_SHADERS,
|
|
32
|
+
Grid,
|
|
33
|
+
GridLines,
|
|
34
|
+
GridQuad,
|
|
35
|
+
GridState,
|
|
36
|
+
HEX_ORIENTATIONS,
|
|
37
|
+
axialToCube,
|
|
38
|
+
cellAt,
|
|
39
|
+
cellCenter,
|
|
40
|
+
cellPolygon,
|
|
41
|
+
cubeToAxial,
|
|
42
|
+
distance,
|
|
43
|
+
edgeMidpoints,
|
|
44
|
+
gridPlugin,
|
|
45
|
+
gridVertex,
|
|
46
|
+
hexDistance,
|
|
47
|
+
neighbors,
|
|
48
|
+
pathDistance,
|
|
49
|
+
pointToAxial,
|
|
50
|
+
roundAxial,
|
|
51
|
+
syncGrids,
|
|
52
|
+
tokenCenter,
|
|
53
|
+
wallAnchor
|
|
54
|
+
};
|
package/dist/math.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type GridKind = 'square' | 'hex';
|
|
2
|
+
export type HexOrientation = 'pointy' | 'flat';
|
|
3
|
+
/** Aether's diagonal rules: true length, every step one cell, or 1-2-1-2 alternating. */
|
|
4
|
+
export type DiagonalRule = 'euclidean' | 'equal' | 'alternating';
|
|
5
|
+
/** What cell math needs of a grid: its shape, cell size and origin, in world units. */
|
|
6
|
+
export interface GridGeometry {
|
|
7
|
+
kind: GridKind;
|
|
8
|
+
orientation: HexOrientation;
|
|
9
|
+
/** Square: cell edge. Hex: distance between adjacent cell centres. */
|
|
10
|
+
size: number;
|
|
11
|
+
/** Where cell (0, 0) starts (square) or is centred (hex), as (x, z). */
|
|
12
|
+
offset: readonly [number, number];
|
|
13
|
+
}
|
|
14
|
+
/** A cell: (column, row) for square grids, axial (q, r) for hex grids. */
|
|
15
|
+
export type Cell = [number, number];
|
|
16
|
+
/** Axial hex coordinates to cube (x, y, z), with x + y + z = 0. */
|
|
17
|
+
export declare function axialToCube(q: number, r: number): [number, number, number];
|
|
18
|
+
/** Cube hex coordinates to axial (q, r). */
|
|
19
|
+
export declare function cubeToAxial(x: number, _y: number, z: number): Cell;
|
|
20
|
+
/** Rounds fractional axial coordinates to the nearest hex, through cube space. */
|
|
21
|
+
export declare function roundAxial(q: number, r: number): Cell;
|
|
22
|
+
/** Fractional axial coordinates of a world point. */
|
|
23
|
+
export declare function pointToAxial(grid: GridGeometry, x: number, z: number): Cell;
|
|
24
|
+
/** The cell containing a world point (x, z). */
|
|
25
|
+
export declare function cellAt(grid: GridGeometry, x: number, z: number): Cell;
|
|
26
|
+
/** A cell's centre, as world (x, z). */
|
|
27
|
+
export declare function cellCenter(grid: GridGeometry, cell: readonly [number, number]): [number, number];
|
|
28
|
+
/** A cell's outline: its corners as world (x, z), in order around it. */
|
|
29
|
+
export declare function cellPolygon(grid: GridGeometry, cell: readonly [number, number]): [number, number][];
|
|
30
|
+
/** A cell's neighbours: 8 for square grids (diagonals included), 6 for hex grids. */
|
|
31
|
+
export declare function neighbors(grid: GridGeometry, cell: readonly [number, number]): Cell[];
|
|
32
|
+
/** Hex steps between two axial cells (orientation doesn't matter). */
|
|
33
|
+
export declare function hexDistance(a: readonly [number, number], b: readonly [number, number]): number;
|
|
34
|
+
/**
|
|
35
|
+
* The distance from world point `a` to `b`, in cells, by Aether's rules: hex grids count hex
|
|
36
|
+
* steps; square grids measure the true length (`euclidean`), the longer axis (`equal`), or count
|
|
37
|
+
* diagonals 1, 2, 1, 2 (`alternating`, starting cheap).
|
|
38
|
+
*/
|
|
39
|
+
export declare function distance(grid: GridGeometry, a: readonly [number, number], b: readonly [number, number], diagonal: DiagonalRule): number;
|
|
40
|
+
/**
|
|
41
|
+
* The distance along a path of world points, per leg and in total, in cells. With `alternating`,
|
|
42
|
+
* the 1-2-1-2 phase carries across waypoints; diagonal steps round to whole cells, straight
|
|
43
|
+
* movement stays fractional (Aether's `pathCells`).
|
|
44
|
+
*/
|
|
45
|
+
export declare function pathDistance(grid: GridGeometry, points: readonly (readonly [number, number])[], diagonal: DiagonalRule): {
|
|
46
|
+
legs: number[];
|
|
47
|
+
total: number;
|
|
48
|
+
};
|
|
49
|
+
type Point = [number, number];
|
|
50
|
+
/** The grid corner nearest (x, z): a square's intersection, or a hex's vertex. */
|
|
51
|
+
export declare function gridVertex(grid: GridGeometry, x: number, z: number): Point;
|
|
52
|
+
/** The midpoints of the edges of the cell containing (x, z). */
|
|
53
|
+
export declare function edgeMidpoints(grid: GridGeometry, x: number, z: number): Point[];
|
|
54
|
+
/** Where a wall's end snaps: the nearest cell corner or edge midpoint. */
|
|
55
|
+
export declare function wallAnchor(grid: GridGeometry, x: number, z: number): Point;
|
|
56
|
+
/**
|
|
57
|
+
* Where a token's centre snaps: odd square footprints (in cells) to a cell centre, even ones to an
|
|
58
|
+
* intersection; hex tokens always to a cell centre.
|
|
59
|
+
*/
|
|
60
|
+
export declare function tokenCenter(grid: GridGeometry, x: number, z: number, footprint?: number): Point;
|
|
61
|
+
export {};
|
package/dist/math.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
const SQRT3 = Math.sqrt(3);
|
|
2
|
+
function axialToCube(q, r) {
|
|
3
|
+
return [q, -q - r, r];
|
|
4
|
+
}
|
|
5
|
+
function cubeToAxial(x, _y, z) {
|
|
6
|
+
return [x, z];
|
|
7
|
+
}
|
|
8
|
+
function roundAxial(q, r) {
|
|
9
|
+
const x = q;
|
|
10
|
+
const z = r;
|
|
11
|
+
const y = -x - z;
|
|
12
|
+
let rx = Math.round(x);
|
|
13
|
+
let ry = Math.round(y);
|
|
14
|
+
let rz = Math.round(z);
|
|
15
|
+
const dx = Math.abs(rx - x);
|
|
16
|
+
const dy = Math.abs(ry - y);
|
|
17
|
+
const dz = Math.abs(rz - z);
|
|
18
|
+
if (dx > dy && dx > dz) rx = -ry - rz;
|
|
19
|
+
else if (dy > dz) ry = -rx - rz;
|
|
20
|
+
else rz = -rx - ry;
|
|
21
|
+
return [rx + 0, rz + 0];
|
|
22
|
+
}
|
|
23
|
+
function pointToAxial(grid, x, z) {
|
|
24
|
+
const px = x - grid.offset[0];
|
|
25
|
+
const pz = z - grid.offset[1];
|
|
26
|
+
if (grid.orientation === "flat") {
|
|
27
|
+
const q = 2 * px / (SQRT3 * grid.size);
|
|
28
|
+
return [q, pz / grid.size - q / 2];
|
|
29
|
+
}
|
|
30
|
+
const r = 2 * pz / (SQRT3 * grid.size);
|
|
31
|
+
return [px / grid.size - r / 2, r];
|
|
32
|
+
}
|
|
33
|
+
function cellAt(grid, x, z) {
|
|
34
|
+
if (grid.kind === "hex") {
|
|
35
|
+
const [q, r] = pointToAxial(grid, x, z);
|
|
36
|
+
return roundAxial(q, r);
|
|
37
|
+
}
|
|
38
|
+
return [
|
|
39
|
+
Math.floor((x - grid.offset[0]) / grid.size),
|
|
40
|
+
Math.floor((z - grid.offset[1]) / grid.size)
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
function cellCenter(grid, cell) {
|
|
44
|
+
const [a, b] = cell;
|
|
45
|
+
if (grid.kind === "hex") {
|
|
46
|
+
if (grid.orientation === "flat") {
|
|
47
|
+
return [
|
|
48
|
+
grid.offset[0] + grid.size * (SQRT3 / 2) * a,
|
|
49
|
+
grid.offset[1] + grid.size * (b + a / 2)
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
return [grid.offset[0] + grid.size * (a + b / 2), grid.offset[1] + grid.size * (SQRT3 / 2) * b];
|
|
53
|
+
}
|
|
54
|
+
return [grid.offset[0] + (a + 0.5) * grid.size, grid.offset[1] + (b + 0.5) * grid.size];
|
|
55
|
+
}
|
|
56
|
+
function cellPolygon(grid, cell) {
|
|
57
|
+
const [cx, cz] = cellCenter(grid, cell);
|
|
58
|
+
if (grid.kind === "square") {
|
|
59
|
+
const h = grid.size / 2;
|
|
60
|
+
return [
|
|
61
|
+
[cx - h, cz - h],
|
|
62
|
+
[cx + h, cz - h],
|
|
63
|
+
[cx + h, cz + h],
|
|
64
|
+
[cx - h, cz + h]
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
const radius = grid.size / SQRT3;
|
|
68
|
+
const start = grid.orientation === "pointy" ? -30 : 0;
|
|
69
|
+
const out = [];
|
|
70
|
+
for (let i = 0; i < 6; i++) {
|
|
71
|
+
const a = (start + i * 60) * Math.PI / 180;
|
|
72
|
+
out.push([cx + radius * Math.cos(a), cz + radius * Math.sin(a)]);
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
const SQUARE_NEIGHBORS = [
|
|
77
|
+
[1, 0],
|
|
78
|
+
[1, 1],
|
|
79
|
+
[0, 1],
|
|
80
|
+
[-1, 1],
|
|
81
|
+
[-1, 0],
|
|
82
|
+
[-1, -1],
|
|
83
|
+
[0, -1],
|
|
84
|
+
[1, -1]
|
|
85
|
+
];
|
|
86
|
+
const HEX_NEIGHBORS = [
|
|
87
|
+
[1, 0],
|
|
88
|
+
[1, -1],
|
|
89
|
+
[0, -1],
|
|
90
|
+
[-1, 0],
|
|
91
|
+
[-1, 1],
|
|
92
|
+
[0, 1]
|
|
93
|
+
];
|
|
94
|
+
function neighbors(grid, cell) {
|
|
95
|
+
const list = grid.kind === "hex" ? HEX_NEIGHBORS : SQUARE_NEIGHBORS;
|
|
96
|
+
return list.map(([dx, dy]) => [cell[0] + dx, cell[1] + dy]);
|
|
97
|
+
}
|
|
98
|
+
function hexDistance(a, b) {
|
|
99
|
+
const dq = a[0] - b[0];
|
|
100
|
+
const dr = a[1] - b[1];
|
|
101
|
+
return (Math.abs(dq) + Math.abs(dr) + Math.abs(dq + dr)) / 2;
|
|
102
|
+
}
|
|
103
|
+
function distance(grid, a, b, diagonal) {
|
|
104
|
+
return pathDistance(grid, [a, b], diagonal).total;
|
|
105
|
+
}
|
|
106
|
+
function pathDistance(grid, points, diagonal) {
|
|
107
|
+
const legs = [];
|
|
108
|
+
let total = 0;
|
|
109
|
+
let expensiveNext = false;
|
|
110
|
+
for (let i = 0; i + 1 < points.length; i++) {
|
|
111
|
+
const a = points[i];
|
|
112
|
+
const b = points[i + 1];
|
|
113
|
+
let value;
|
|
114
|
+
if (grid.kind === "hex") {
|
|
115
|
+
value = hexDistance(cellAt(grid, a[0], a[1]), cellAt(grid, b[0], b[1]));
|
|
116
|
+
} else {
|
|
117
|
+
const dx = Math.abs(b[0] - a[0]) / grid.size;
|
|
118
|
+
const dz = Math.abs(b[1] - a[1]) / grid.size;
|
|
119
|
+
if (diagonal === "equal") value = Math.max(dx, dz);
|
|
120
|
+
else if (diagonal === "alternating") {
|
|
121
|
+
const diagonals = Math.round(Math.min(dx, dz));
|
|
122
|
+
const straight = Math.abs(dx - dz);
|
|
123
|
+
value = straight + Math.floor(diagonals / 2) * 3;
|
|
124
|
+
if (diagonals % 2 === 1) {
|
|
125
|
+
value += expensiveNext ? 2 : 1;
|
|
126
|
+
expensiveNext = !expensiveNext;
|
|
127
|
+
}
|
|
128
|
+
} else value = Math.sqrt(dx * dx + dz * dz);
|
|
129
|
+
}
|
|
130
|
+
legs.push(value);
|
|
131
|
+
total += value;
|
|
132
|
+
}
|
|
133
|
+
return { legs, total };
|
|
134
|
+
}
|
|
135
|
+
function hexCorners(grid, cx, cz, out) {
|
|
136
|
+
const radius = grid.size / SQRT3;
|
|
137
|
+
const start = grid.orientation === "pointy" ? -30 : 0;
|
|
138
|
+
for (let i = 0; i < 6; i++) {
|
|
139
|
+
const a = (start + i * 60) * Math.PI / 180;
|
|
140
|
+
out.push([cx + radius * Math.cos(a), cz + radius * Math.sin(a)]);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function nearbyHexes(grid, x, z) {
|
|
144
|
+
const cell = cellAt(grid, x, z);
|
|
145
|
+
return [cell, ...neighbors(grid, cell)].map((c) => cellCenter(grid, c));
|
|
146
|
+
}
|
|
147
|
+
function nearest(x, z, choices) {
|
|
148
|
+
let best = choices[0] ?? [x, z];
|
|
149
|
+
let d = Infinity;
|
|
150
|
+
for (const c of choices) {
|
|
151
|
+
const e = (c[0] - x) ** 2 + (c[1] - z) ** 2;
|
|
152
|
+
if (e < d) {
|
|
153
|
+
d = e;
|
|
154
|
+
best = c;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return [best[0], best[1]];
|
|
158
|
+
}
|
|
159
|
+
function gridVertex(grid, x, z) {
|
|
160
|
+
if (grid.kind === "square") {
|
|
161
|
+
return [
|
|
162
|
+
grid.offset[0] + Math.round((x - grid.offset[0]) / grid.size) * grid.size,
|
|
163
|
+
grid.offset[1] + Math.round((z - grid.offset[1]) / grid.size) * grid.size
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
const corners = [];
|
|
167
|
+
for (const [cx, cz] of nearbyHexes(grid, x, z)) hexCorners(grid, cx, cz, corners);
|
|
168
|
+
return nearest(x, z, corners);
|
|
169
|
+
}
|
|
170
|
+
function edgeMidpoints(grid, x, z) {
|
|
171
|
+
const [cx, cz] = cellCenter(grid, cellAt(grid, x, z));
|
|
172
|
+
if (grid.kind === "square") {
|
|
173
|
+
const h = grid.size / 2;
|
|
174
|
+
return [
|
|
175
|
+
[cx, cz - h],
|
|
176
|
+
[cx + h, cz],
|
|
177
|
+
[cx, cz + h],
|
|
178
|
+
[cx - h, cz]
|
|
179
|
+
];
|
|
180
|
+
}
|
|
181
|
+
const corners = [];
|
|
182
|
+
hexCorners(grid, cx, cz, corners);
|
|
183
|
+
return corners.map((a, i) => {
|
|
184
|
+
const b = corners[(i + 1) % 6];
|
|
185
|
+
return [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function wallAnchor(grid, x, z) {
|
|
189
|
+
if (grid.kind === "square")
|
|
190
|
+
return nearest(x, z, [gridVertex(grid, x, z), ...edgeMidpoints(grid, x, z)]);
|
|
191
|
+
const features = [];
|
|
192
|
+
for (const [cx, cz] of nearbyHexes(grid, x, z)) {
|
|
193
|
+
const corners = [];
|
|
194
|
+
hexCorners(grid, cx, cz, corners);
|
|
195
|
+
features.push(...corners);
|
|
196
|
+
for (let i = 0; i < 6; i++) {
|
|
197
|
+
const a = corners[i];
|
|
198
|
+
const b = corners[(i + 1) % 6];
|
|
199
|
+
features.push([(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return nearest(x, z, features);
|
|
203
|
+
}
|
|
204
|
+
function tokenCenter(grid, x, z, footprint = 1) {
|
|
205
|
+
if (grid.kind === "hex" || Math.max(1, Math.round(footprint)) % 2 === 1)
|
|
206
|
+
return cellCenter(grid, cellAt(grid, x, z));
|
|
207
|
+
return gridVertex(grid, x, z);
|
|
208
|
+
}
|
|
209
|
+
export {
|
|
210
|
+
axialToCube,
|
|
211
|
+
cellAt,
|
|
212
|
+
cellCenter,
|
|
213
|
+
cellPolygon,
|
|
214
|
+
cubeToAxial,
|
|
215
|
+
distance,
|
|
216
|
+
edgeMidpoints,
|
|
217
|
+
gridVertex,
|
|
218
|
+
hexDistance,
|
|
219
|
+
neighbors,
|
|
220
|
+
pathDistance,
|
|
221
|
+
pointToAxial,
|
|
222
|
+
roundAxial,
|
|
223
|
+
tokenCenter,
|
|
224
|
+
wallAnchor
|
|
225
|
+
};
|
package/dist/plugin.d.ts
ADDED
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { PostUpdate } from "@aethervtt/shard-core";
|
|
2
|
+
import { registerShaders, Shaders } from "@aethervtt/shard-render";
|
|
3
|
+
import { definePlugin } from "@aethervtt/shard-runtime";
|
|
4
|
+
import { TransformSystems } from "@aethervtt/shard-transform";
|
|
5
|
+
import {
|
|
6
|
+
GRID_SHADERS,
|
|
7
|
+
Grid,
|
|
8
|
+
GridLines,
|
|
9
|
+
GridQuad,
|
|
10
|
+
GridState,
|
|
11
|
+
observeGridRemovals,
|
|
12
|
+
syncGrids
|
|
13
|
+
} from "./grid.js";
|
|
14
|
+
const gridPlugin = definePlugin({
|
|
15
|
+
name: "grid",
|
|
16
|
+
dependencies: ["render/forward", "core/transform"],
|
|
17
|
+
provides: [Grid, GridQuad, GridLines, GridState],
|
|
18
|
+
build(app) {
|
|
19
|
+
app.world.initResource(GridState);
|
|
20
|
+
observeGridRemovals(app.world);
|
|
21
|
+
app.addSystems(PostUpdate, syncGrids.before(TransformSystems));
|
|
22
|
+
},
|
|
23
|
+
ready(app) {
|
|
24
|
+
registerShaders(app.world.resource(Shaders), GRID_SHADERS);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
export {
|
|
28
|
+
gridPlugin
|
|
29
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aethervtt/shard-grid",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./math": {
|
|
13
|
+
"types": "./dist/math.d.ts",
|
|
14
|
+
"default": "./dist/math.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"LICENSE",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@aethervtt/shard-core": "0.1.0",
|
|
24
|
+
"@aethervtt/shard-mesh": "0.1.0",
|
|
25
|
+
"@aethervtt/shard-render": "0.1.0",
|
|
26
|
+
"@aethervtt/shard-runtime": "0.1.0",
|
|
27
|
+
"@aethervtt/shard-transform": "0.1.0"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/aethervtt/shard.git",
|
|
32
|
+
"directory": "packages/grid"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public",
|
|
36
|
+
"provenance": true
|
|
37
|
+
},
|
|
38
|
+
"shard": {
|
|
39
|
+
"buildSha": "8e14c9896f82a3554b156f1ffdd68f2cc3e7d78a"
|
|
40
|
+
}
|
|
41
|
+
}
|