Publish Forma Engine 0.3.0 source with documentation and CI
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
import {
|
||||
entity,
|
||||
uid,
|
||||
type Vec3,
|
||||
type Geometry,
|
||||
validateGeometry,
|
||||
} from "./schema.ts";
|
||||
const cross = (a: number[], b: number[], c: number[]) =>
|
||||
(b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
|
||||
export function extrude(profile: number[][], depth = 1): Geometry {
|
||||
if (
|
||||
!Array.isArray(profile) ||
|
||||
profile.length < 3 ||
|
||||
profile.length > 256 ||
|
||||
!Number.isFinite(depth) ||
|
||||
depth <= 0 ||
|
||||
!profile.every((p) => p.length === 2 && p.every(Number.isFinite))
|
||||
)
|
||||
throw Error("Нужен простой замкнутый профиль из 3–256 точек");
|
||||
const points = profile.map((p) => [...p]);
|
||||
let area = points.reduce(
|
||||
(s, p, i) =>
|
||||
s +
|
||||
p[0] * points[(i + 1) % points.length][1] -
|
||||
points[(i + 1) % points.length][0] * p[1],
|
||||
0,
|
||||
);
|
||||
if (Math.abs(area) < 1e-8) throw Error("Нулевая площадь");
|
||||
if (area < 0) points.reverse();
|
||||
const n = points.length;
|
||||
for (let i = 0; i < n; i++)
|
||||
for (let j = i + 2; j < n; j++) {
|
||||
if (i === 0 && j === n - 1) continue;
|
||||
const a = points[i],
|
||||
b = points[(i + 1) % n],
|
||||
c = points[j],
|
||||
d = points[(j + 1) % n];
|
||||
if (
|
||||
cross(a, b, c) * cross(a, b, d) < 0 &&
|
||||
cross(c, d, a) * cross(c, d, b) < 0
|
||||
)
|
||||
throw Error("Профиль пересекает себя");
|
||||
}
|
||||
const indices: number[] = [],
|
||||
left = points.map((_, i) => i);
|
||||
let guard = 0;
|
||||
while (left.length > 3) {
|
||||
let clipped = false;
|
||||
for (let j = 0; j < left.length; j++) {
|
||||
const a = left[(j + left.length - 1) % left.length],
|
||||
b = left[j],
|
||||
c = left[(j + 1) % left.length];
|
||||
if (cross(points[a], points[b], points[c]) <= 1e-8) continue;
|
||||
if (
|
||||
left.some(
|
||||
(k) =>
|
||||
k !== a &&
|
||||
k !== b &&
|
||||
k !== c &&
|
||||
cross(points[a], points[b], points[k]) >= 0 &&
|
||||
cross(points[b], points[c], points[k]) >= 0 &&
|
||||
cross(points[c], points[a], points[k]) >= 0,
|
||||
)
|
||||
)
|
||||
continue;
|
||||
indices.push(a, b, c, a + n, c + n, b + n);
|
||||
left.splice(j, 1);
|
||||
clipped = true;
|
||||
break;
|
||||
}
|
||||
if (!clipped || guard++ > 256)
|
||||
throw Error("Невозможно триангулировать профиль");
|
||||
}
|
||||
indices.push(
|
||||
left[0],
|
||||
left[1],
|
||||
left[2],
|
||||
left[0] + n,
|
||||
left[2] + n,
|
||||
left[1] + n,
|
||||
);
|
||||
const positions: number[] = [];
|
||||
for (const y of [0, depth])
|
||||
for (const [x, z] of points) positions.push(x, y, z);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const j = (i + 1) % n;
|
||||
indices.push(i, i + n, j + n, i, j + n, j);
|
||||
}
|
||||
const g = { positions, indices };
|
||||
validateGeometry(g);
|
||||
return g;
|
||||
}
|
||||
export function lathe(profile: number[][], segments = 24): Geometry {
|
||||
if (
|
||||
!Number.isInteger(segments) ||
|
||||
segments < 3 ||
|
||||
segments > 128 ||
|
||||
!Array.isArray(profile) ||
|
||||
profile.length < 2 ||
|
||||
profile.length > 256 ||
|
||||
!profile.every(
|
||||
(p) => p.length === 2 && p.every(Number.isFinite) && p[0] >= 0,
|
||||
)
|
||||
)
|
||||
throw Error("Нужен профиль [радиус, высота] и 3–128 сегментов");
|
||||
const positions: number[] = [],
|
||||
indices: number[] = [];
|
||||
for (const [r, y] of profile)
|
||||
for (let j = 0; j < segments; j++) {
|
||||
const a = (j / segments) * Math.PI * 2;
|
||||
positions.push(Math.cos(a) * r, y, Math.sin(a) * r);
|
||||
}
|
||||
for (let i = 0; i < profile.length - 1; i++)
|
||||
for (let j = 0; j < segments; j++) {
|
||||
const a = i * segments + j,
|
||||
b = i * segments + ((j + 1) % segments),
|
||||
c = a + segments,
|
||||
d = b + segments;
|
||||
indices.push(a, c, b, b, c, d);
|
||||
}
|
||||
const g = { positions, indices };
|
||||
validateGeometry(g);
|
||||
return g;
|
||||
}
|
||||
export function transformed(
|
||||
g: Geometry,
|
||||
offset: Vec3 = [0, 0, 0],
|
||||
scale: Vec3 = [1, 1, 1],
|
||||
): Geometry {
|
||||
const positions = g.positions.map((v, i) => v * scale[i % 3] + offset[i % 3]),
|
||||
indices = [...g.indices];
|
||||
if (scale[0] * scale[1] * scale[2] < 0)
|
||||
for (let i = 0; i < indices.length; i += 3)
|
||||
[indices[i + 1], indices[i + 2]] = [indices[i + 2], indices[i + 1]];
|
||||
return { positions, indices };
|
||||
}
|
||||
export function arena({
|
||||
width = 22,
|
||||
depth = 18,
|
||||
seed = 42,
|
||||
obstacles = 8,
|
||||
}: any = {}) {
|
||||
width = Math.max(8, Math.min(80, width));
|
||||
depth = Math.max(8, Math.min(80, depth));
|
||||
obstacles = Math.max(0, Math.min(80, Math.round(obstacles)));
|
||||
let v = seed | 0;
|
||||
const random = () => {
|
||||
v = (Math.imul(v, 1664525) + 1013904223) | 0;
|
||||
return (v >>> 0) / 4294967296;
|
||||
};
|
||||
const root = entity("Сад · уровень"),
|
||||
nodes = [root];
|
||||
const add = (
|
||||
name: string,
|
||||
pos: Vec3,
|
||||
size: Vec3,
|
||||
color: string,
|
||||
type = "box",
|
||||
collision = true,
|
||||
) => {
|
||||
const n = entity(
|
||||
name,
|
||||
{
|
||||
mesh: { type, size },
|
||||
material: { color, roughness: 0.92 },
|
||||
...(collision
|
||||
? { collider: { shape: "box", size }, rigidbody: { type: "fixed" } }
|
||||
: {}),
|
||||
},
|
||||
pos,
|
||||
);
|
||||
n.parentId = root.id;
|
||||
nodes.push(n);
|
||||
return n;
|
||||
};
|
||||
add("Каменное основание", [0, -0.5, 0], [width, 1, depth], "#b6aa91");
|
||||
add(
|
||||
"Светлый песок",
|
||||
[0, 0.025, 0],
|
||||
[width - 0.5, 0.05, depth - 0.5],
|
||||
"#d1c9ae",
|
||||
"box",
|
||||
false,
|
||||
);
|
||||
add("Северная стена", [0, 0.6, depth / 2], [width, 1.2, 0.45], "#b0a48a");
|
||||
add("Южная стена", [0, 0.6, -depth / 2], [width, 1.2, 0.45], "#b0a48a");
|
||||
add("Западная стена", [-width / 2, 0.6, 0], [0.45, 1.2, depth], "#b0a48a");
|
||||
add("Восточная стена", [width / 2, 0.6, 0], [0.45, 1.2, depth], "#b0a48a");
|
||||
for (let z = -Math.floor(depth / 2) + 1; z < depth / 2; z += 1.1)
|
||||
add(
|
||||
"Плитка тропы",
|
||||
[0, 0.07, z],
|
||||
[1.7, 0.09, 0.9],
|
||||
"#b6b49c",
|
||||
"box",
|
||||
false,
|
||||
);
|
||||
for (const x of [-width / 2 + 1.2, width / 2 - 1.2])
|
||||
for (const z of [-depth / 2 + 1.2, depth / 2 - 1.2]) {
|
||||
add("Основание колонны", [x, 0.15, z], [1.4, 0.3, 1.4], "#a9a187");
|
||||
add("Колонна", [x, 1.6, z], [0.7, 2.7, 0.7], "#c5bca2", "cylinder");
|
||||
add("Капитель", [x, 3, z], [1.2, 0.3, 1.2], "#cfc5ab");
|
||||
}
|
||||
for (let i = 0; i < obstacles; i++) {
|
||||
const x = (i % 2 ? -1 : 1) * (2 + random() * (width / 2 - 4)),
|
||||
z = (random() - 0.5) * (depth - 5);
|
||||
if (i % 3 === 0) {
|
||||
add("Ствол", [x, 0.7, z], [0.25, 1.4, 0.25], "#80765c", "cylinder");
|
||||
add("Крона", [x, 1.7, z], [2, 1.7, 2], "#859887", "icosphere", false);
|
||||
} else {
|
||||
const n = add(
|
||||
"Обломок",
|
||||
[x, 0.35, z],
|
||||
[0.9 + random(), 0.7, 0.8 + random()],
|
||||
"#999b84",
|
||||
);
|
||||
n.transform.rotation[1] = random() * 2;
|
||||
}
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
export function character({
|
||||
height = 1.8,
|
||||
color = "#7d9b8a",
|
||||
name = "Персонаж · модель",
|
||||
}: any = {}) {
|
||||
const root = entity(name),
|
||||
nodes = [root],
|
||||
scale = height / 1.8;
|
||||
const add = (name: string, p: Vec3, size: Vec3, c: string, type = "box") => {
|
||||
const n = entity(
|
||||
name,
|
||||
{
|
||||
mesh: { type, size: size.map((v) => v * scale) },
|
||||
material: { color: c, roughness: 0.8 },
|
||||
},
|
||||
p.map((v) => v * scale) as Vec3,
|
||||
);
|
||||
n.parentId = root.id;
|
||||
nodes.push(n);
|
||||
};
|
||||
add("Торс", [0, 1.1, 0], [0.6, 0.65, 0.35], color);
|
||||
add("Голова", [0, 1.65, 0], [0.4, 0.4, 0.4], "#d5b994", "sphere");
|
||||
add("Капюшон", [0, 1.8, -0.04], [0.5, 0.25, 0.48], color, "icosphere");
|
||||
for (const sign of [-1, 1]) {
|
||||
add("Нога", [sign * 0.16, 0.37, 0], [0.24, 0.7, 0.25], "#61685f");
|
||||
add("Сапог", [sign * 0.16, 0.09, 0.09], [0.28, 0.18, 0.42], "#55594f");
|
||||
add("Рука", [sign * 0.42, 1.05, 0], [0.18, 0.65, 0.2], color);
|
||||
}
|
||||
add("Клинок", [0.53, 0.98, 0.28], [0.08, 0.85, 0.12], "#c4cac0");
|
||||
return nodes;
|
||||
}
|
||||
Reference in New Issue
Block a user