sprite-machine 0.1.0 → 0.2.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/README.md +113 -64
- package/bin/sprite-machine.mjs +2 -10
- package/package.json +11 -4
- package/src/atlas.js +136 -142
- package/src/carve.js +18 -51
- package/src/colorize.js +14 -29
- package/src/constants.js +7 -12
- package/src/diag.js +13 -20
- package/src/faces.js +9 -24
- package/src/gltf.js +22 -41
- package/src/index.js +8 -11
- package/src/ingest.js +18 -35
- package/src/layers.js +55 -0
- package/src/model.js +55 -59
- package/src/node.js +23 -17
- package/src/pipeline.js +124 -9
- package/src/png-chunks.js +26 -46
- package/src/png-encode.js +12 -22
- package/src/regions.js +31 -48
- package/src/skin.js +37 -66
- package/src/t-junction.js +21 -39
- package/src/three.js +60 -0
- package/src/views.js +47 -86
- package/src/wedge-mesh.js +120 -169
- package/src/weld.js +56 -0
- package/src/mesh-util.js +0 -65
package/src/skin.js
CHANGED
|
@@ -1,48 +1,25 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// mesh-util.js turns the bytes into the DataTexture the material samples.
|
|
1
|
+
// The skin: the model's color as a texture, so the mesher can merge faces on
|
|
2
|
+
// occupancy alone (regions.js). mesh-util.js turns the bytes into a
|
|
3
|
+
// DataTexture.
|
|
5
4
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// by
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// outward: a fragment on the region's edge that rounds to the neighbouring
|
|
13
|
-
// texel still reads its own color, and an importer with bilinear filtering
|
|
14
|
-
// on gets no bleed. A region of one color — every one-color wall, every
|
|
15
|
-
// solid cube's face — gets no chart: its triangles point at the SWATCH
|
|
16
|
-
// STRIP, one 1×1 chart per distinct color (the palette, plus anything the
|
|
17
|
-
// faces actually hold), sampled at the texel's center — one texel read at
|
|
18
|
-
// its middle needs no gutter. A wedge's slope is one material by the gate,
|
|
19
|
-
// so it points at a swatch too. The skin is therefore only the regions that
|
|
20
|
-
// cross a color, plus the strip — a fraction of "every exposed face" — and
|
|
21
|
-
// its size is bounded by the multi-color regions' boxes, never by the grid.
|
|
5
|
+
// A region with more than one color gets a chart: its bounding box at one
|
|
6
|
+
// texel per cell, padded by a one-texel gutter. Texels no piece covers (the
|
|
7
|
+
// gutter, holes, the box outside a diagonal edge) take the nearest piece's
|
|
8
|
+
// color by a breadth-first flood, so edge fragments and bilinear filtering
|
|
9
|
+
// read the region's own colors. A one-color region, and every wedge slope,
|
|
10
|
+
// samples a 1×1 swatch at its texel center. There is one swatch per color.
|
|
22
11
|
//
|
|
23
|
-
// Packing
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// while the packed height, rounded up to a power of two, would exceed it —
|
|
28
|
-
// no ceiling (a skin past a device's texture limit is a sprite the carve
|
|
29
|
-
// could not have rebuilt live either). Power-of-two sides are not required
|
|
30
|
-
// by three or by WebGL2; exporters and older engines are happier with them,
|
|
31
|
-
// and it costs nothing here.
|
|
12
|
+
// Packing: padded charts sorted by height then width, descending, on
|
|
13
|
+
// left-to-right shelves, then the swatches. The width starts at the smallest
|
|
14
|
+
// power of two that fits the widest chart (at least 16) and doubles while the
|
|
15
|
+
// packed height, rounded up to a power of two, exceeds it.
|
|
32
16
|
//
|
|
33
|
-
// Orientation
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
// (uvOfLattice). There is no per-face flip table, so nothing can drift; if a
|
|
37
|
-
// face ever renders mirrored the bug is in the corner-to-UV read, not the
|
|
38
|
-
// bake. The texture's row 0 is v = 0 (DataTexture's flipY is false) — leave
|
|
39
|
-
// it there.
|
|
17
|
+
// Orientation: chart texel (i, j) is the cell at tangent (a + i, b + j) of the
|
|
18
|
+
// region's box, i along FACE_GEO[face].A and j along .B. A vertex's UV is the
|
|
19
|
+
// same affine map of its lattice position (uvOfLattice). Texture row 0 is v = 0.
|
|
40
20
|
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
// (the Helium bug, wedge-mesh.test.mjs) cannot touch it. Keep it that way:
|
|
44
|
-
// no canvas in this file, ever.
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
21
|
+
// The RGBA bytes are written directly. Do not use a canvas here: privacy
|
|
22
|
+
// browsers perturb getImageData.
|
|
46
23
|
|
|
47
24
|
import { FACE_GEO } from './faces.js';
|
|
48
25
|
import { unpackRGBA } from './ingest.js';
|
|
@@ -50,11 +27,11 @@ import { AXIS_INDEX } from './views.js';
|
|
|
50
27
|
|
|
51
28
|
/**
|
|
52
29
|
* @typedef {{u0:number, v0:number, w:number, h:number}} Chart
|
|
53
|
-
* a
|
|
30
|
+
* a chart's texel rect, excluding the gutter.
|
|
54
31
|
* @typedef {{width:number, height:number, data:Uint8Array,
|
|
55
32
|
* charts:(Chart|null)[], swatch:Map<number, {u:number, v:number}>}} Skin
|
|
56
|
-
* charts[i] is regions[i]'s chart
|
|
57
|
-
*
|
|
33
|
+
* charts[i] is regions[i]'s chart, or null for a one-color region. swatch
|
|
34
|
+
* maps a packed color to its texel.
|
|
58
35
|
*/
|
|
59
36
|
|
|
60
37
|
const GUTTER = 1;
|
|
@@ -66,9 +43,8 @@ const pow2ceil = (n) => {
|
|
|
66
43
|
return p;
|
|
67
44
|
};
|
|
68
45
|
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
// width by construction. Returns the packed height and each item's origin.
|
|
46
|
+
// Pack `items` ({pw, ph}, padded sizes) left to right on shelves of `width`.
|
|
47
|
+
// Every item fits the width. Returns the packed height and each item's origin.
|
|
72
48
|
function shelfPack(items, width) {
|
|
73
49
|
const at = new Array(items.length);
|
|
74
50
|
let x = 0,
|
|
@@ -87,10 +63,9 @@ function shelfPack(items, width) {
|
|
|
87
63
|
return { height: y + shelf, at };
|
|
88
64
|
}
|
|
89
65
|
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
// the pieces seed in row order, the four neighbours in a fixed order).
|
|
66
|
+
// A region's padded box as texel colors. Uncovered texels take the nearest
|
|
67
|
+
// piece texel's color by a multi-source breadth-first flood. Seeds go in row
|
|
68
|
+
// order and neighbors in a fixed order, so the result is deterministic.
|
|
94
69
|
function floodBox(region) {
|
|
95
70
|
const { w, h, texels, present } = region;
|
|
96
71
|
const W = w + 2 * GUTTER;
|
|
@@ -126,13 +101,12 @@ function floodBox(region) {
|
|
|
126
101
|
* Bake the skin for a mesh: a chart per multi-color region, a swatch per color.
|
|
127
102
|
* @param {import('./regions.js').Region2D[]} regions the base regions, in emit order
|
|
128
103
|
* @param {number[]} colors colors to give a swatch (the build's palette);
|
|
129
|
-
* every
|
|
130
|
-
* a relaxed or dominant color the palette snap left off it
|
|
104
|
+
* every color the faces and regions hold is added too
|
|
131
105
|
* @param {Map<number, number>} faceColor colorize's per-face colors, keyed idx*6 + f
|
|
132
106
|
* @returns {Skin}
|
|
133
107
|
*/
|
|
134
108
|
export function bakeSkin(regions, colors, faceColor) {
|
|
135
|
-
// 1.
|
|
109
|
+
// 1. Regions with more than one color get a chart.
|
|
136
110
|
/** @type {{i:number, region:import('./regions.js').Region2D}[]} */
|
|
137
111
|
const bodies = [];
|
|
138
112
|
/** @type {(Chart|null)[]} */
|
|
@@ -141,8 +115,8 @@ export function bakeSkin(regions, colors, faceColor) {
|
|
|
141
115
|
if (region.uniform === null) bodies.push({ i, region });
|
|
142
116
|
});
|
|
143
117
|
|
|
144
|
-
// 2.
|
|
145
|
-
// regions' pieces
|
|
118
|
+
// 2. Swatch colors: the palette, then every color in the faces and the
|
|
119
|
+
// regions' pieces. A cap's wedge color appears only in a region.
|
|
146
120
|
const seen = new Set();
|
|
147
121
|
/** @type {number[]} */
|
|
148
122
|
const swatchColors = [];
|
|
@@ -158,9 +132,8 @@ export function bakeSkin(regions, colors, faceColor) {
|
|
|
158
132
|
for (let i = 0; i < region.present.length; i++)
|
|
159
133
|
if (region.present[i]) addColor(region.texels[i]);
|
|
160
134
|
|
|
161
|
-
// 3. Pack
|
|
162
|
-
// order
|
|
163
|
-
// swatches, 1×1 and unpadded, after them.
|
|
135
|
+
// 3. Pack padded charts by height then width, descending, ties in region
|
|
136
|
+
// order. The 1×1 unpadded swatches follow.
|
|
164
137
|
/** @type {{pw:number, ph:number, body?:{i:number, region:import('./regions.js').Region2D}, color?:number}[]} */
|
|
165
138
|
const items = bodies.map((body) => ({
|
|
166
139
|
body,
|
|
@@ -181,7 +154,7 @@ export function bakeSkin(regions, colors, faceColor) {
|
|
|
181
154
|
height = pow2ceil(packed.height);
|
|
182
155
|
}
|
|
183
156
|
|
|
184
|
-
// 4. Bake. Unused texels stay transparent black
|
|
157
|
+
// 4. Bake. Unused texels stay transparent black.
|
|
185
158
|
const data = new Uint8Array(width * height * 4);
|
|
186
159
|
const put = (x, y, c) => {
|
|
187
160
|
const o = (y * width + x) * 4;
|
|
@@ -211,11 +184,9 @@ export function bakeSkin(regions, colors, faceColor) {
|
|
|
211
184
|
}
|
|
212
185
|
|
|
213
186
|
/**
|
|
214
|
-
* The texel coordinates of a point on a charted region's plane:
|
|
215
|
-
*
|
|
216
|
-
* origin
|
|
217
|
-
* the T-junction repair inserted along an edge lands on the texel line
|
|
218
|
-
* between two cells. Divide by the skin's width and height for the UV.
|
|
187
|
+
* The texel coordinates of a point on a charted region's plane: its offset
|
|
188
|
+
* along the face's tangent axes from the region's box origin, added to the
|
|
189
|
+
* chart's origin. Divide by the skin's width and height for the UV.
|
|
219
190
|
* @param {Chart} chart
|
|
220
191
|
* @param {{face:string, a:number, b:number}} region
|
|
221
192
|
* @param {number[]} p a point [x, y, z] on the region's plane, in voxel units
|
|
@@ -230,7 +201,7 @@ export function uvOfLattice(chart, region, p) {
|
|
|
230
201
|
}
|
|
231
202
|
|
|
232
203
|
/**
|
|
233
|
-
* The texel
|
|
204
|
+
* The texel center of a color's swatch, in texel coordinates.
|
|
234
205
|
* @param {Skin} skin
|
|
235
206
|
* @param {number} packed a packed RGBA color the skin holds a swatch for
|
|
236
207
|
* @returns {[number, number]}
|
package/src/t-junction.js
CHANGED
|
@@ -1,38 +1,21 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
1
|
// T-junction elimination for lattice meshes.
|
|
3
2
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// which breaks the watertight weld and clean glTF export.
|
|
3
|
+
// Merged faces leave T-junctions: a vertex in the middle of another triangle's
|
|
4
|
+
// edge. They make the surface non-manifold, which breaks the watertight weld
|
|
5
|
+
// and glTF export. Every vertex is on the integer lattice, so the repair is
|
|
6
|
+
// exact: split each triangle edge at every mesh vertex strictly inside it, then
|
|
7
|
+
// re-triangulate the convex result with its own vertices.
|
|
10
8
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// the (convex) result with lattice-only vertices. Pure integer geometry: no
|
|
15
|
-
// THREE, Node-testable.
|
|
9
|
+
// Only axis-aligned and 45° diagonal edges can carry interior vertices. Any
|
|
10
|
+
// other edge is a triangulation chord across a region or slope, where a valid
|
|
11
|
+
// surface has no vertex, so it is skipped.
|
|
16
12
|
//
|
|
17
|
-
//
|
|
18
|
-
// one (a region polygon's, a slope's ridge edge) and a 45° DIAGONAL (a slope
|
|
19
|
-
// block's staircase edge, a region's cut beside it — both long since the
|
|
20
|
-
// merges of Sep 7 2026). Any other segment is a triangulation chord across a
|
|
21
|
-
// region's or a slope's interior, where a valid surface never has a vertex,
|
|
22
|
-
// so it is left alone.
|
|
23
|
-
//
|
|
24
|
-
// A triangle is an opaque record beyond its three vertices: every other field
|
|
25
|
-
// (the normal, and the mesher's paint — a chart and its rect, or a swatch
|
|
26
|
-
// color) is copied onto each piece a split produces, so the repair never has
|
|
27
|
-
// to know what rides on a triangle. UVs are NOT carried through here: they
|
|
28
|
-
// are a function of position (skin.js uvOfLattice), read after the repair.
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
13
|
+
// UVs are computed from position after the repair (skin.js uvOfLattice).
|
|
30
14
|
|
|
31
15
|
const key = (p) => p[0] + ',' + p[1] + ',' + p[2];
|
|
32
16
|
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
// axes stepping by the same magnitude).
|
|
17
|
+
// Vertices in `vset` strictly inside segment p->q, ordered p->q. Returns []
|
|
18
|
+
// unless p->q is axis-aligned or a 45° diagonal.
|
|
36
19
|
function interiorPointsOnEdge(p, q, vset) {
|
|
37
20
|
const d = [q[0] - p[0], q[1] - p[1], q[2] - p[2]];
|
|
38
21
|
const axes = [];
|
|
@@ -49,10 +32,10 @@ function interiorPointsOnEdge(p, q, vset) {
|
|
|
49
32
|
return out;
|
|
50
33
|
}
|
|
51
34
|
|
|
52
|
-
// Triangulate a convex polygon
|
|
53
|
-
// vertices
|
|
54
|
-
// corner whose
|
|
55
|
-
// would span a
|
|
35
|
+
// Triangulate a convex polygon, wound CCW about `normal`, using only its own
|
|
36
|
+
// vertices. Collinear boundary points are allowed. Only a strictly convex
|
|
37
|
+
// corner whose edge prev->next contains no other vertex is clipped. Otherwise
|
|
38
|
+
// the ear would span a split edge and reintroduce a T-junction.
|
|
56
39
|
function triangulateConvex(ring, normal, emit) {
|
|
57
40
|
const turn = (o, a, b) => {
|
|
58
41
|
const ux = a[0] - o[0],
|
|
@@ -67,7 +50,7 @@ function triangulateConvex(ring, normal, emit) {
|
|
|
67
50
|
(ux * vy - uy * vx) * normal[2]
|
|
68
51
|
);
|
|
69
52
|
};
|
|
70
|
-
// v strictly
|
|
53
|
+
// v lies strictly inside segment p->q.
|
|
71
54
|
const onSeg = (p, q, v) => {
|
|
72
55
|
const dx = q[0] - p[0],
|
|
73
56
|
dy = q[1] - p[1],
|
|
@@ -104,8 +87,8 @@ function triangulateConvex(ring, normal, emit) {
|
|
|
104
87
|
break;
|
|
105
88
|
}
|
|
106
89
|
if (!clipped) {
|
|
107
|
-
//
|
|
108
|
-
//
|
|
90
|
+
// Fallback: fan from a strictly convex vertex. Ear clipping should always
|
|
91
|
+
// progress on a convex polygon.
|
|
109
92
|
let ai = 0;
|
|
110
93
|
for (let i = 0; i < poly.length; i++) {
|
|
111
94
|
const prev = poly[(i - 1 + poly.length) % poly.length];
|
|
@@ -130,11 +113,10 @@ function triangulateConvex(ring, normal, emit) {
|
|
|
130
113
|
/**
|
|
131
114
|
* @template {{a:number[], b:number[], c:number[], normal:number[]}} T
|
|
132
115
|
* @param {T[]} tris
|
|
133
|
-
* triangles with
|
|
116
|
+
* triangles with integer lattice vertices, wound CCW about `normal`.
|
|
134
117
|
* @returns {T[]}
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
* the three vertices.
|
|
118
|
+
* the same surface with no T-junctions. Pieces of a split triangle keep its
|
|
119
|
+
* other fields.
|
|
138
120
|
*/
|
|
139
121
|
export function eliminateTJunctions(tris) {
|
|
140
122
|
const vset = new Set();
|
package/src/three.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Three adapter (sprite-machine/three): the mesher's record as three objects.
|
|
2
|
+
// The only entry that imports three, an optional peer.
|
|
3
|
+
|
|
4
|
+
import * as THREE from 'three';
|
|
5
|
+
import { unpackRGBA } from './ingest.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The skin as a DataTexture. The class defaults match the bake: nearest
|
|
9
|
+
* filtering, no mipmaps, and flipY false so texel row 0 is v = 0. The bytes
|
|
10
|
+
* are sRGB.
|
|
11
|
+
* @param {import('./skin.js').Skin} skin
|
|
12
|
+
* @returns {THREE.DataTexture}
|
|
13
|
+
*/
|
|
14
|
+
export function skinTexture(skin) {
|
|
15
|
+
const tex = new THREE.DataTexture(skin.data, skin.width, skin.height, THREE.RGBAFormat);
|
|
16
|
+
tex.colorSpace = THREE.SRGBColorSpace;
|
|
17
|
+
tex.needsUpdate = true;
|
|
18
|
+
return tex;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The geometry as an indexed BufferGeometry with its bounds computed. The
|
|
23
|
+
* arrays are copied.
|
|
24
|
+
* @param {import('./wedge-mesh.js').Geometry} geometry
|
|
25
|
+
* @returns {THREE.BufferGeometry}
|
|
26
|
+
*/
|
|
27
|
+
export function toGeometry(geometry) {
|
|
28
|
+
const geo = new THREE.BufferGeometry();
|
|
29
|
+
geo.setAttribute('position', new THREE.Float32BufferAttribute(geometry.position, 3));
|
|
30
|
+
geo.setAttribute('normal', new THREE.Float32BufferAttribute(geometry.normal, 3));
|
|
31
|
+
geo.setAttribute('uv', new THREE.Float32BufferAttribute(geometry.uv, 2));
|
|
32
|
+
geo.setIndex(new THREE.Uint32BufferAttribute(geometry.index, 1));
|
|
33
|
+
geo.computeBoundingBox();
|
|
34
|
+
geo.computeBoundingSphere();
|
|
35
|
+
return geo;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The model as a flat-shaded Mesh with shadows on. The material takes the
|
|
40
|
+
* skin as `map`, or with no skin the packed sRGB `color`.
|
|
41
|
+
* @param {import('./wedge-mesh.js').Built} model
|
|
42
|
+
* @returns {THREE.Mesh}
|
|
43
|
+
*/
|
|
44
|
+
export function toMesh(model) {
|
|
45
|
+
const map = model.skin ? skinTexture(model.skin) : null;
|
|
46
|
+
const mat = new THREE.MeshStandardMaterial({
|
|
47
|
+
flatShading: true,
|
|
48
|
+
metalness: 0,
|
|
49
|
+
roughness: 1,
|
|
50
|
+
...(map ? { map } : {}),
|
|
51
|
+
});
|
|
52
|
+
if (!map && model.color != null) {
|
|
53
|
+
const { r, g, b } = unpackRGBA(model.color);
|
|
54
|
+
mat.color.setRGB(r / 255, g / 255, b / 255, THREE.SRGBColorSpace);
|
|
55
|
+
}
|
|
56
|
+
const mesh = new THREE.Mesh(toGeometry(model.geometry), mat);
|
|
57
|
+
mesh.castShadow = true;
|
|
58
|
+
mesh.receiveShadow = true;
|
|
59
|
+
return mesh;
|
|
60
|
+
}
|
package/src/views.js
CHANGED
|
@@ -1,30 +1,20 @@
|
|
|
1
|
-
//
|
|
2
|
-
// View conventions and projection mappings.
|
|
1
|
+
// View conventions and projections.
|
|
3
2
|
//
|
|
4
|
-
// World frame:
|
|
5
|
-
// A
|
|
6
|
-
//
|
|
7
|
-
// - axis: the world axis it looks ALONG (the depth / extrusion axis).
|
|
8
|
-
// - project(x, y, z, dims) -> {u, v}: which pixel of the view a voxel maps to.
|
|
9
|
-
// u,v are image coords with (0,0) = top-left, v growing DOWN.
|
|
3
|
+
// World frame: +x right, +y up, +z toward the front (camera).
|
|
4
|
+
// A view is an orthographic render of one face. project(x, y, z, dims) -> {u, v}
|
|
5
|
+
// gives the view pixel for a voxel, with (0, 0) at top-left and v growing down.
|
|
10
6
|
//
|
|
11
|
-
//
|
|
12
|
-
// FRONT/BACK :
|
|
13
|
-
// LEFT/RIGHT :
|
|
14
|
-
// TOP/BOTTOM :
|
|
7
|
+
// View image sizes in grid dims:
|
|
8
|
+
// FRONT/BACK : nx wide, ny tall (X/Y plane)
|
|
9
|
+
// LEFT/RIGHT : nz wide, ny tall (Z/Y plane)
|
|
10
|
+
// TOP/BOTTOM : nx wide, nz tall (X/Z plane)
|
|
15
11
|
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// pixel (col,row) lands where a human artist expects (art drawn upright and
|
|
19
|
-
// left-to-right as seen from outside the object).
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
12
|
+
// The mappings are the same for every model. Art drawn upright and left to
|
|
13
|
+
// right, as seen from outside the object, appears that way on the model.
|
|
21
14
|
|
|
22
15
|
/** @typedef {{nx:number, ny:number, nz:number}} Dims */
|
|
23
16
|
|
|
24
|
-
//
|
|
25
|
-
// FACE_TO_VIEW is consumed by colorize, VIEW_TO_FACE by the edge hints (which
|
|
26
|
-
// need a view's own face to know which way it looks); every pair is already
|
|
27
|
-
// implied by the face metadata below.
|
|
17
|
+
// View name -> the face key it colors, and the inverse.
|
|
28
18
|
export const VIEW_TO_FACE = {
|
|
29
19
|
right: 'nx',
|
|
30
20
|
left: 'px',
|
|
@@ -37,10 +27,8 @@ export const FACE_TO_VIEW = Object.fromEntries(
|
|
|
37
27
|
Object.entries(VIEW_TO_FACE).map(([k, v]) => [v, k])
|
|
38
28
|
);
|
|
39
29
|
|
|
40
|
-
// Outward unit
|
|
41
|
-
//
|
|
42
|
-
// all derived from this so they can't drift from the convention the 6-bit surface
|
|
43
|
-
// mask and the faceColor keying (idx*6+f) depend on.
|
|
30
|
+
// Outward unit normal per face key. FACE_AXIS, carve's NEIGHBORS and faces.js
|
|
31
|
+
// derive from it.
|
|
44
32
|
export const FACE_NORMAL = {
|
|
45
33
|
px: [1, 0, 0],
|
|
46
34
|
nx: [-1, 0, 0],
|
|
@@ -50,29 +38,26 @@ export const FACE_NORMAL = {
|
|
|
50
38
|
nz: [0, 0, -1],
|
|
51
39
|
};
|
|
52
40
|
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
// with FACE_NORMAL; carve.js re-exports it for the consumers that read it there.
|
|
41
|
+
// Face-key order. The 6-bit surface mask and the faceColor keys (idx*6 + f)
|
|
42
|
+
// index by position, so the order must not change. carve.js re-exports it.
|
|
56
43
|
export const FACE_KEYS = ['px', 'nx', 'py', 'ny', 'pz', 'nz'];
|
|
57
44
|
|
|
58
|
-
// Face key ->
|
|
45
|
+
// Face key -> index in FACE_KEYS.
|
|
59
46
|
export const FACE_INDEX = Object.fromEntries(FACE_KEYS.map((k, i) => [k, i]));
|
|
60
47
|
|
|
61
|
-
//
|
|
48
|
+
// Axis name -> index in an [x, y, z] triple.
|
|
62
49
|
export const AXIS_INDEX = { x: 0, y: 1, z: 2 };
|
|
63
50
|
|
|
64
51
|
// The face key whose outward normal points along world `axis` with `sign` (±1).
|
|
65
|
-
// Derived from FACE_NORMAL so an (axis, sign) pair can never drift from the normals.
|
|
66
52
|
export const faceKeyOf = (axis, sign) =>
|
|
67
53
|
FACE_KEYS.find((k) => FACE_NORMAL[k][AXIS_INDEX[axis]] === sign);
|
|
68
54
|
|
|
69
|
-
//
|
|
70
|
-
// FACE_NORMAL so it can't drift; used by colorize for mirror-fill.
|
|
55
|
+
// The world axis of each face's normal. colorize uses it for mirror-fill.
|
|
71
56
|
export const FACE_AXIS = Object.fromEntries(
|
|
72
57
|
Object.entries(FACE_NORMAL).map(([k, n]) => [k, n[0] ? 'x' : n[1] ? 'y' : 'z'])
|
|
73
58
|
);
|
|
74
59
|
|
|
75
|
-
// Opposite face
|
|
60
|
+
// Opposite face, for mirror-fill.
|
|
76
61
|
export const FACE_OPPOSITE = {
|
|
77
62
|
px: 'nx',
|
|
78
63
|
nx: 'px',
|
|
@@ -82,18 +67,11 @@ export const FACE_OPPOSITE = {
|
|
|
82
67
|
nz: 'pz',
|
|
83
68
|
};
|
|
84
69
|
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
// truth (a `step`/`from` field here would be a silent drift hazard).
|
|
89
|
-
//
|
|
90
|
-
// projectInto(x,y,z,d,out) writes integer image coords into the reused `out` (no
|
|
91
|
-
// per-voxel allocation in carve's hot triple loop); project() is the allocating
|
|
92
|
-
// convenience that delegates to it, so each view has exactly ONE formula. `imgW`/
|
|
93
|
-
// `imgH` give the expected view image size for a grid, so carve can place each view
|
|
94
|
-
// at native scale (padding, never stretching) and index it 1:1.
|
|
70
|
+
// Per view: the image size for a grid (imgW, imgH) and the pixel projection.
|
|
71
|
+
// projectInto writes into a reused `out` so carve's per-voxel loop does not
|
|
72
|
+
// allocate. project allocates and delegates to it.
|
|
95
73
|
export const VIEWS = {
|
|
96
|
-
// FRONT: looks toward -z
|
|
74
|
+
// FRONT: looks toward -z and sees the +z face. Image is X by Y.
|
|
97
75
|
front: {
|
|
98
76
|
imgW: (d) => d.nx,
|
|
99
77
|
imgH: (d) => d.ny,
|
|
@@ -102,7 +80,7 @@ export const VIEWS = {
|
|
|
102
80
|
return this.projectInto(x, y, z, d, { u: 0, v: 0 });
|
|
103
81
|
},
|
|
104
82
|
},
|
|
105
|
-
// BACK: looks toward +z
|
|
83
|
+
// BACK: looks toward +z and sees the -z face. Mirrored left to right from FRONT.
|
|
106
84
|
back: {
|
|
107
85
|
imgW: (d) => d.nx,
|
|
108
86
|
imgH: (d) => d.ny,
|
|
@@ -111,11 +89,9 @@ export const VIEWS = {
|
|
|
111
89
|
return this.projectInto(x, y, z, d, { u: 0, v: 0 });
|
|
112
90
|
},
|
|
113
91
|
},
|
|
114
|
-
// LEFT: the
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
// not by the world axis it happens to occupy. Image = Z by Y. u = nz-1-z puts
|
|
118
|
-
// the object's front (+z) at the left column, matching a nose-left profile.
|
|
92
|
+
// LEFT: the tile for the object's left side. It colors the +x face, which
|
|
93
|
+
// seen from +x reads as a left-side profile (see README). Image is Z by Y.
|
|
94
|
+
// u = nz-1-z puts the front (+z) in the left column.
|
|
119
95
|
left: {
|
|
120
96
|
imgW: (d) => d.nz,
|
|
121
97
|
imgH: (d) => d.ny,
|
|
@@ -124,8 +100,8 @@ export const VIEWS = {
|
|
|
124
100
|
return this.projectInto(x, y, z, d, { u: 0, v: 0 });
|
|
125
101
|
},
|
|
126
102
|
},
|
|
127
|
-
// RIGHT: the object's
|
|
128
|
-
//
|
|
103
|
+
// RIGHT: the object's right side. It colors the -x face. u = z puts the front
|
|
104
|
+
// in the right column.
|
|
129
105
|
right: {
|
|
130
106
|
imgW: (d) => d.nz,
|
|
131
107
|
imgH: (d) => d.ny,
|
|
@@ -134,9 +110,8 @@ export const VIEWS = {
|
|
|
134
110
|
return this.projectInto(x, y, z, d, { u: 0, v: 0 });
|
|
135
111
|
},
|
|
136
112
|
},
|
|
137
|
-
// TOP: looks toward -y
|
|
138
|
-
//
|
|
139
|
-
// the TOP row of the image (v=0), matching the FRONT view's top-is-v=0.
|
|
113
|
+
// TOP: looks toward -y and sees the +y face. Image is X by Z.
|
|
114
|
+
// v = nz-1-z puts the front (+z) on the top row.
|
|
140
115
|
top: {
|
|
141
116
|
imgW: (d) => d.nx,
|
|
142
117
|
imgH: (d) => d.nz,
|
|
@@ -145,10 +120,9 @@ export const VIEWS = {
|
|
|
145
120
|
return this.projectInto(x, y, z, d, { u: 0, v: 0 });
|
|
146
121
|
},
|
|
147
122
|
},
|
|
148
|
-
// BOTTOM: looks toward +y
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
// way the TOP and BOTTOM tiles register front-to-front on the same edge.
|
|
123
|
+
// BOTTOM: looks toward +y and sees the -y face. The object is rolled about its
|
|
124
|
+
// front-back axis, so the front stays on the top row as in TOP (v = nz-1-z)
|
|
125
|
+
// and only u flips (u = nx-1-x).
|
|
152
126
|
bottom: {
|
|
153
127
|
imgW: (d) => d.nx,
|
|
154
128
|
imgH: (d) => d.nz,
|
|
@@ -161,19 +135,11 @@ export const VIEWS = {
|
|
|
161
135
|
|
|
162
136
|
export const VIEW_NAMES = Object.keys(VIEWS);
|
|
163
137
|
|
|
164
|
-
//
|
|
165
|
-
// DEFAULT_ATLAS_LAYOUT (LEFT FRONT TOP / RIGHT BACK BOTTOM). A pinned convention
|
|
166
|
-
// (a test asserts it equals the layout) — there is no on-screen faces-preview
|
|
167
|
-
// grid; the editor switches faces with text tabs.
|
|
138
|
+
// View names in atlas row-major order, matching atlas.js DEFAULT_ATLAS_LAYOUT.
|
|
168
139
|
export const VIEW_DISPLAY_ORDER = ['left', 'front', 'top', 'right', 'back', 'bottom'];
|
|
169
140
|
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
// and the reference behind the README's per-face "Front points" column. Derived
|
|
173
|
-
// meaning: in LEFT, front (z=nz-1) maps to u=0, the left column; TOP and BOTTOM
|
|
174
|
-
// both put the front on their TOP edge (BOTTOM is the sideways flip of TOP);
|
|
175
|
-
// FRONT/BACK look straight down +z/-z, so their nose points out of / into the
|
|
176
|
-
// screen — there is no in-plane front edge (null).
|
|
141
|
+
// The edge of each view's tile that the object's front (+z) points toward.
|
|
142
|
+
// FRONT and BACK look along z, so they have none (null).
|
|
177
143
|
export const VIEW_FRONT_EDGE = {
|
|
178
144
|
right: 'right',
|
|
179
145
|
left: 'left',
|
|
@@ -183,7 +149,7 @@ export const VIEW_FRONT_EDGE = {
|
|
|
183
149
|
back: null,
|
|
184
150
|
};
|
|
185
151
|
|
|
186
|
-
// Each view's opposite
|
|
152
|
+
// Each view's opposite, the mirror-fill source for a view with no art.
|
|
187
153
|
export const VIEW_OPPOSITE = {
|
|
188
154
|
right: 'left',
|
|
189
155
|
left: 'right',
|
|
@@ -193,16 +159,13 @@ export const VIEW_OPPOSITE = {
|
|
|
193
159
|
bottom: 'top',
|
|
194
160
|
};
|
|
195
161
|
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
// HORIZONTALLY — left↔right and front↔back on the X/Z planes, and top↔bottom too
|
|
200
|
-
// because BOTTOM is the sideways (left/right) flip of TOP, not an end-over-end one.
|
|
201
|
-
// (mirrorImage's 'y' branch in derive.js is therefore unexercised in practice.)
|
|
162
|
+
// The image axis along which an opposite view's tile is flipped to display a
|
|
163
|
+
// mirror-derived face. Every pair mirrors horizontally, top and bottom included,
|
|
164
|
+
// because BOTTOM is TOP rolled sideways.
|
|
202
165
|
export const MIRROR_AXIS = 'x';
|
|
203
166
|
|
|
204
|
-
//
|
|
205
|
-
//
|
|
167
|
+
// The grid dims a view's image constrains: [axis for imgW, axis for imgH].
|
|
168
|
+
// Used by dimension reconciliation.
|
|
206
169
|
export const VIEW_AXES = {
|
|
207
170
|
front: ['nx', 'ny'],
|
|
208
171
|
back: ['nx', 'ny'],
|
|
@@ -212,12 +175,10 @@ export const VIEW_AXES = {
|
|
|
212
175
|
bottom: ['nx', 'nz'],
|
|
213
176
|
};
|
|
214
177
|
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
// (
|
|
218
|
-
//
|
|
219
|
-
// Consumed by the sheet resize (atlas.js resizeAtlas) to place each
|
|
220
|
-
// face's tile so every face sharing a world axis shifts identically.
|
|
178
|
+
// Per view: the axis its image columns (u) and rows (v) run along, and whether
|
|
179
|
+
// the image index runs against the world coordinate (flip). Probed from
|
|
180
|
+
// project() at load. atlas.js resizeAtlas uses it so faces that share an axis
|
|
181
|
+
// shift together.
|
|
221
182
|
const AXIS_ARG = { nx: 0, ny: 1, nz: 2 };
|
|
222
183
|
function probeFlip(spec, axisName, which) {
|
|
223
184
|
const d = { nx: 2, ny: 2, nz: 2 };
|