Refactor project sequence handling and add whitebox brush geometry support
This commit is contained in:
@@ -1,544 +0,0 @@
|
||||
import { BufferAttribute, BufferGeometry } from "three";
|
||||
|
||||
import type { Vec2, Vec3 } from "../core/vector";
|
||||
import {
|
||||
BOX_EDGE_IDS,
|
||||
BOX_FACE_IDS,
|
||||
type BoxBrush,
|
||||
type BoxEdgeId,
|
||||
type BoxFaceId,
|
||||
type BoxVertexId,
|
||||
type FaceUvState
|
||||
} from "../document/brushes";
|
||||
import { transformProjectedFaceUv } from "./box-face-uvs";
|
||||
|
||||
const FACE_VERTEX_IDS: Record<BoxFaceId, readonly [BoxVertexId, BoxVertexId, BoxVertexId, BoxVertexId]> = {
|
||||
posX: ["posX_negY_posZ", "posX_negY_negZ", "posX_posY_negZ", "posX_posY_posZ"],
|
||||
negX: ["negX_negY_negZ", "negX_negY_posZ", "negX_posY_posZ", "negX_posY_negZ"],
|
||||
posY: ["negX_posY_posZ", "posX_posY_posZ", "posX_posY_negZ", "negX_posY_negZ"],
|
||||
negY: ["negX_negY_negZ", "posX_negY_negZ", "posX_negY_posZ", "negX_negY_posZ"],
|
||||
posZ: ["negX_negY_posZ", "posX_negY_posZ", "posX_posY_posZ", "negX_posY_posZ"],
|
||||
negZ: ["posX_negY_negZ", "negX_negY_negZ", "negX_posY_negZ", "posX_posY_negZ"]
|
||||
};
|
||||
|
||||
const EDGE_VERTEX_IDS: Record<BoxEdgeId, readonly [BoxVertexId, BoxVertexId]> = {
|
||||
edgeX_negY_negZ: ["negX_negY_negZ", "posX_negY_negZ"],
|
||||
edgeX_posY_negZ: ["negX_posY_negZ", "posX_posY_negZ"],
|
||||
edgeX_negY_posZ: ["negX_negY_posZ", "posX_negY_posZ"],
|
||||
edgeX_posY_posZ: ["negX_posY_posZ", "posX_posY_posZ"],
|
||||
edgeY_negX_negZ: ["negX_negY_negZ", "negX_posY_negZ"],
|
||||
edgeY_posX_negZ: ["posX_negY_negZ", "posX_posY_negZ"],
|
||||
edgeY_negX_posZ: ["negX_negY_posZ", "negX_posY_posZ"],
|
||||
edgeY_posX_posZ: ["posX_negY_posZ", "posX_posY_posZ"],
|
||||
edgeZ_negX_negY: ["negX_negY_negZ", "negX_negY_posZ"],
|
||||
edgeZ_posX_negY: ["posX_negY_negZ", "posX_negY_posZ"],
|
||||
edgeZ_negX_posY: ["negX_posY_negZ", "negX_posY_posZ"],
|
||||
edgeZ_posX_posY: ["posX_posY_negZ", "posX_posY_posZ"]
|
||||
};
|
||||
|
||||
const WATER_TOP_FACE_RENDER_SEGMENTS = 12;
|
||||
|
||||
export interface BoxBrushGeometryDiagnostic {
|
||||
code: string;
|
||||
message: string;
|
||||
faceId?: BoxFaceId;
|
||||
}
|
||||
|
||||
export interface DerivedBoxBrushFaceSurface {
|
||||
faceId: BoxFaceId;
|
||||
vertexIds: readonly [BoxVertexId, BoxVertexId, BoxVertexId, BoxVertexId];
|
||||
triangles: Array<readonly [number, number, number]>;
|
||||
normal: Vec3;
|
||||
}
|
||||
|
||||
export interface DerivedBoxBrushMeshData {
|
||||
geometry: BufferGeometry;
|
||||
faceSurfaces: DerivedBoxBrushFaceSurface[];
|
||||
edgeSegments: Array<{ edgeId: BoxEdgeId; start: Vec3; end: Vec3 }>;
|
||||
colliderVertices: Float32Array;
|
||||
colliderIndices: Uint32Array;
|
||||
localBounds: { min: Vec3; max: Vec3 };
|
||||
}
|
||||
|
||||
function cloneVec3(vector: Vec3): Vec3 {
|
||||
return { x: vector.x, y: vector.y, z: vector.z };
|
||||
}
|
||||
|
||||
function dotVec3(left: Vec3, right: Vec3): number {
|
||||
return left.x * right.x + left.y * right.y + left.z * right.z;
|
||||
}
|
||||
|
||||
function getVectorLength(vector: Vec3): number {
|
||||
return Math.sqrt(dotVec3(vector, vector));
|
||||
}
|
||||
|
||||
function normalizeVec3(vector: Vec3): Vec3 {
|
||||
const length = getVectorLength(vector);
|
||||
|
||||
if (length <= 1e-8) {
|
||||
return { x: 0, y: 0, z: 0 };
|
||||
}
|
||||
|
||||
return {
|
||||
x: vector.x / length,
|
||||
y: vector.y / length,
|
||||
z: vector.z / length
|
||||
};
|
||||
}
|
||||
|
||||
function computeNewellNormal(vertices: Vec3[]): Vec3 {
|
||||
let normal = { x: 0, y: 0, z: 0 };
|
||||
|
||||
for (let index = 0; index < vertices.length; index += 1) {
|
||||
const current = vertices[index];
|
||||
const next = vertices[(index + 1) % vertices.length];
|
||||
normal.x += (current.y - next.y) * (current.z + next.z);
|
||||
normal.y += (current.z - next.z) * (current.x + next.x);
|
||||
normal.z += (current.x - next.x) * (current.y + next.y);
|
||||
}
|
||||
|
||||
return normalizeVec3(normal);
|
||||
}
|
||||
|
||||
function chooseProjectionAxes(normal: Vec3): [keyof Vec3, keyof Vec3] {
|
||||
const absoluteNormal = {
|
||||
x: Math.abs(normal.x),
|
||||
y: Math.abs(normal.y),
|
||||
z: Math.abs(normal.z)
|
||||
};
|
||||
|
||||
if (absoluteNormal.x >= absoluteNormal.y && absoluteNormal.x >= absoluteNormal.z) {
|
||||
return ["y", "z"];
|
||||
}
|
||||
|
||||
if (absoluteNormal.y >= absoluteNormal.z) {
|
||||
return ["x", "z"];
|
||||
}
|
||||
|
||||
return ["x", "y"];
|
||||
}
|
||||
|
||||
function projectVerticesTo2d(vertices: Vec3[], normal: Vec3): Vec2[] {
|
||||
const [uAxis, vAxis] = chooseProjectionAxes(normal);
|
||||
return vertices.map((vertex) => ({
|
||||
x: vertex[uAxis],
|
||||
y: vertex[vAxis]
|
||||
}));
|
||||
}
|
||||
|
||||
function computeSignedArea(points: Vec2[]): number {
|
||||
let area = 0;
|
||||
|
||||
for (let index = 0; index < points.length; index += 1) {
|
||||
const current = points[index];
|
||||
const next = points[(index + 1) % points.length];
|
||||
area += current.x * next.y - next.x * current.y;
|
||||
}
|
||||
|
||||
return area * 0.5;
|
||||
}
|
||||
|
||||
function isPointInTriangle(point: Vec2, triangle: [Vec2, Vec2, Vec2], orientation: number): boolean {
|
||||
const [a, b, c] = triangle;
|
||||
const edges = [
|
||||
(b.x - a.x) * (point.y - a.y) - (b.y - a.y) * (point.x - a.x),
|
||||
(c.x - b.x) * (point.y - b.y) - (c.y - b.y) * (point.x - b.x),
|
||||
(a.x - c.x) * (point.y - c.y) - (a.y - c.y) * (point.x - c.x)
|
||||
];
|
||||
|
||||
return orientation > 0 ? edges.every((value) => value >= -1e-8) : edges.every((value) => value <= 1e-8);
|
||||
}
|
||||
|
||||
function triangulateQuad(vertices: Vec3[]): Array<readonly [number, number, number]> {
|
||||
const normal = computeNewellNormal(vertices);
|
||||
const projected = projectVerticesTo2d(vertices, normal);
|
||||
const orientation = computeSignedArea(projected);
|
||||
|
||||
if (Math.abs(orientation) <= 1e-8) {
|
||||
throw new Error("Face projection is degenerate.");
|
||||
}
|
||||
|
||||
const remaining = [0, 1, 2, 3];
|
||||
const triangles: Array<readonly [number, number, number]> = [];
|
||||
|
||||
while (remaining.length > 3) {
|
||||
let earFound = false;
|
||||
|
||||
for (let offset = 0; offset < remaining.length; offset += 1) {
|
||||
const previousIndex = remaining[(offset + remaining.length - 1) % remaining.length];
|
||||
const currentIndex = remaining[offset];
|
||||
const nextIndex = remaining[(offset + 1) % remaining.length];
|
||||
const previousPoint = projected[previousIndex];
|
||||
const currentPoint = projected[currentIndex];
|
||||
const nextPoint = projected[nextIndex];
|
||||
const cross =
|
||||
(currentPoint.x - previousPoint.x) * (nextPoint.y - previousPoint.y) -
|
||||
(currentPoint.y - previousPoint.y) * (nextPoint.x - previousPoint.x);
|
||||
|
||||
if ((orientation > 0 && cross <= 1e-8) || (orientation < 0 && cross >= -1e-8)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const candidateTriangle: [Vec2, Vec2, Vec2] = [previousPoint, currentPoint, nextPoint];
|
||||
const containsOtherPoint = remaining.some((candidateIndex) => {
|
||||
if (candidateIndex === previousIndex || candidateIndex === currentIndex || candidateIndex === nextIndex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isPointInTriangle(projected[candidateIndex], candidateTriangle, orientation);
|
||||
});
|
||||
|
||||
if (containsOtherPoint) {
|
||||
continue;
|
||||
}
|
||||
|
||||
triangles.push([previousIndex, currentIndex, nextIndex]);
|
||||
remaining.splice(offset, 1);
|
||||
earFound = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!earFound) {
|
||||
throw new Error("Face triangulation could not find a stable ear.");
|
||||
}
|
||||
}
|
||||
|
||||
triangles.push([remaining[0], remaining[1], remaining[2]]);
|
||||
return triangles;
|
||||
}
|
||||
|
||||
function projectLocalVertexToFaceUv(vertexPosition: Vec3, faceId: BoxFaceId, faceBounds: { min: Vec3; max: Vec3 }): Vec2 {
|
||||
switch (faceId) {
|
||||
case "posX":
|
||||
return {
|
||||
x: faceBounds.max.z - vertexPosition.z,
|
||||
y: vertexPosition.y - faceBounds.min.y
|
||||
};
|
||||
case "negX":
|
||||
return {
|
||||
x: vertexPosition.z - faceBounds.min.z,
|
||||
y: vertexPosition.y - faceBounds.min.y
|
||||
};
|
||||
case "posY":
|
||||
return {
|
||||
x: vertexPosition.x - faceBounds.min.x,
|
||||
y: faceBounds.max.z - vertexPosition.z
|
||||
};
|
||||
case "negY":
|
||||
return {
|
||||
x: vertexPosition.x - faceBounds.min.x,
|
||||
y: vertexPosition.z - faceBounds.min.z
|
||||
};
|
||||
case "posZ":
|
||||
return {
|
||||
x: vertexPosition.x - faceBounds.min.x,
|
||||
y: vertexPosition.y - faceBounds.min.y
|
||||
};
|
||||
case "negZ":
|
||||
return {
|
||||
x: faceBounds.max.x - vertexPosition.x,
|
||||
y: vertexPosition.y - faceBounds.min.y
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getFaceUvSize(faceId: BoxFaceId, faceBounds: { min: Vec3; max: Vec3 }): Vec2 {
|
||||
switch (faceId) {
|
||||
case "posX":
|
||||
case "negX":
|
||||
return {
|
||||
x: faceBounds.max.z - faceBounds.min.z,
|
||||
y: faceBounds.max.y - faceBounds.min.y
|
||||
};
|
||||
case "posY":
|
||||
case "negY":
|
||||
return {
|
||||
x: faceBounds.max.x - faceBounds.min.x,
|
||||
y: faceBounds.max.z - faceBounds.min.z
|
||||
};
|
||||
case "posZ":
|
||||
case "negZ":
|
||||
return {
|
||||
x: faceBounds.max.x - faceBounds.min.x,
|
||||
y: faceBounds.max.y - faceBounds.min.y
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function computeFaceBounds(vertices: Vec3[]): { min: Vec3; max: Vec3 } {
|
||||
const firstVertex = vertices[0];
|
||||
const min = { ...firstVertex };
|
||||
const max = { ...firstVertex };
|
||||
|
||||
for (const vertex of vertices.slice(1)) {
|
||||
min.x = Math.min(min.x, vertex.x);
|
||||
min.y = Math.min(min.y, vertex.y);
|
||||
min.z = Math.min(min.z, vertex.z);
|
||||
max.x = Math.max(max.x, vertex.x);
|
||||
max.y = Math.max(max.y, vertex.y);
|
||||
max.z = Math.max(max.z, vertex.z);
|
||||
}
|
||||
|
||||
return { min, max };
|
||||
}
|
||||
|
||||
function lerpNumber(start: number, end: number, amount: number) {
|
||||
return start + (end - start) * amount;
|
||||
}
|
||||
|
||||
function lerpVec3(start: Vec3, end: Vec3, amount: number): Vec3 {
|
||||
return {
|
||||
x: lerpNumber(start.x, end.x, amount),
|
||||
y: lerpNumber(start.y, end.y, amount),
|
||||
z: lerpNumber(start.z, end.z, amount)
|
||||
};
|
||||
}
|
||||
|
||||
function interpolateQuadSurfaceVertex(
|
||||
corners: readonly [Vec3, Vec3, Vec3, Vec3],
|
||||
u: number,
|
||||
v: number
|
||||
): Vec3 {
|
||||
const topEdge = lerpVec3(corners[0], corners[1], u);
|
||||
const bottomEdge = lerpVec3(corners[3], corners[2], u);
|
||||
|
||||
return lerpVec3(topEdge, bottomEdge, v);
|
||||
}
|
||||
|
||||
function pushRenderedFaceVertex(
|
||||
positions: number[],
|
||||
normals: number[],
|
||||
uvs: number[],
|
||||
faceUvs: number[],
|
||||
indices: number[],
|
||||
vertex: Vec3,
|
||||
normal: Vec3,
|
||||
faceId: BoxFaceId,
|
||||
faceBounds: { min: Vec3; max: Vec3 },
|
||||
uvSize: Vec2,
|
||||
uvState: FaceUvState
|
||||
) {
|
||||
const projectedUv = projectLocalVertexToFaceUv(vertex, faceId, faceBounds);
|
||||
const transformedUv = transformProjectedFaceUv(projectedUv, uvSize, uvState);
|
||||
|
||||
positions.push(vertex.x, vertex.y, vertex.z);
|
||||
normals.push(normal.x, normal.y, normal.z);
|
||||
uvs.push(transformedUv.x, transformedUv.y);
|
||||
faceUvs.push(
|
||||
uvSize.x <= 1e-8 ? 0.5 : projectedUv.x / uvSize.x,
|
||||
uvSize.y <= 1e-8 ? 0.5 : projectedUv.y / uvSize.y
|
||||
);
|
||||
indices.push(indices.length);
|
||||
}
|
||||
|
||||
export function getBoxBrushFaceVertexIds(faceId: BoxFaceId): readonly [BoxVertexId, BoxVertexId, BoxVertexId, BoxVertexId] {
|
||||
return FACE_VERTEX_IDS[faceId];
|
||||
}
|
||||
|
||||
export function getBoxBrushEdgeVertexIds(edgeId: BoxEdgeId): readonly [BoxVertexId, BoxVertexId] {
|
||||
return EDGE_VERTEX_IDS[edgeId];
|
||||
}
|
||||
|
||||
export function getBoxBrushLocalVertexPosition(brush: BoxBrush, vertexId: BoxVertexId): Vec3 {
|
||||
return cloneVec3(brush.geometry.vertices[vertexId]);
|
||||
}
|
||||
|
||||
export function buildBoxBrushDerivedMeshData(brush: BoxBrush): DerivedBoxBrushMeshData {
|
||||
const diagnostics = validateBoxBrushGeometry(brush);
|
||||
|
||||
if (diagnostics.length > 0) {
|
||||
throw new Error(diagnostics[0].message);
|
||||
}
|
||||
|
||||
const positions: number[] = [];
|
||||
const normals: number[] = [];
|
||||
const uvs: number[] = [];
|
||||
const faceUvs: number[] = [];
|
||||
const indices: number[] = [];
|
||||
const colliderVertices: number[] = [];
|
||||
const colliderIndices: number[] = [];
|
||||
const faceSurfaces: DerivedBoxBrushFaceSurface[] = [];
|
||||
const groups: Array<{ start: number; count: number; materialIndex: number }> = [];
|
||||
const vertexIndexMap = new Map<BoxVertexId, number>();
|
||||
|
||||
for (const vertexId of Object.keys(brush.geometry.vertices) as BoxVertexId[]) {
|
||||
const vertex = brush.geometry.vertices[vertexId];
|
||||
vertexIndexMap.set(vertexId, colliderVertices.length / 3);
|
||||
colliderVertices.push(vertex.x, vertex.y, vertex.z);
|
||||
}
|
||||
|
||||
for (const [materialIndex, faceId] of BOX_FACE_IDS.entries()) {
|
||||
const faceVertexIds = FACE_VERTEX_IDS[faceId];
|
||||
const faceVertices = faceVertexIds.map((vertexId) => getBoxBrushLocalVertexPosition(brush, vertexId));
|
||||
const triangles = triangulateQuad(faceVertices);
|
||||
const normal = computeNewellNormal(faceVertices);
|
||||
const faceBounds = computeFaceBounds(faceVertices);
|
||||
const uvSize = getFaceUvSize(faceId, faceBounds);
|
||||
const uvState = brush.faces[faceId].uv as FaceUvState;
|
||||
const indexStart = indices.length;
|
||||
|
||||
faceSurfaces.push({
|
||||
faceId,
|
||||
vertexIds: faceVertexIds,
|
||||
triangles,
|
||||
normal
|
||||
});
|
||||
|
||||
const useSubdividedWaterTopFace =
|
||||
brush.volume.mode === "water" &&
|
||||
faceId === "posY" &&
|
||||
brush.volume.water.surfaceDisplacementEnabled;
|
||||
|
||||
if (useSubdividedWaterTopFace) {
|
||||
const faceCorners = faceVertices as [Vec3, Vec3, Vec3, Vec3];
|
||||
|
||||
for (let row = 0; row < WATER_TOP_FACE_RENDER_SEGMENTS; row += 1) {
|
||||
const v0 = row / WATER_TOP_FACE_RENDER_SEGMENTS;
|
||||
const v1 = (row + 1) / WATER_TOP_FACE_RENDER_SEGMENTS;
|
||||
|
||||
for (let column = 0; column < WATER_TOP_FACE_RENDER_SEGMENTS; column += 1) {
|
||||
const u0 = column / WATER_TOP_FACE_RENDER_SEGMENTS;
|
||||
const u1 = (column + 1) / WATER_TOP_FACE_RENDER_SEGMENTS;
|
||||
const quadVertices: [Vec3, Vec3, Vec3, Vec3] = [
|
||||
interpolateQuadSurfaceVertex(faceCorners, u0, v0),
|
||||
interpolateQuadSurfaceVertex(faceCorners, u1, v0),
|
||||
interpolateQuadSurfaceVertex(faceCorners, u1, v1),
|
||||
interpolateQuadSurfaceVertex(faceCorners, u0, v1)
|
||||
];
|
||||
|
||||
for (const vertex of [quadVertices[0], quadVertices[1], quadVertices[2], quadVertices[0], quadVertices[2], quadVertices[3]]) {
|
||||
pushRenderedFaceVertex(
|
||||
positions,
|
||||
normals,
|
||||
uvs,
|
||||
faceUvs,
|
||||
indices,
|
||||
vertex,
|
||||
normal,
|
||||
faceId,
|
||||
faceBounds,
|
||||
uvSize,
|
||||
uvState
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const triangle of triangles) {
|
||||
for (const vertexOffset of triangle) {
|
||||
pushRenderedFaceVertex(
|
||||
positions,
|
||||
normals,
|
||||
uvs,
|
||||
faceUvs,
|
||||
indices,
|
||||
faceVertices[vertexOffset],
|
||||
normal,
|
||||
faceId,
|
||||
faceBounds,
|
||||
uvSize,
|
||||
uvState
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
groups.push({
|
||||
start: indexStart,
|
||||
count: indices.length - indexStart,
|
||||
materialIndex
|
||||
});
|
||||
|
||||
for (const triangle of triangles) {
|
||||
colliderIndices.push(
|
||||
vertexIndexMap.get(faceVertexIds[triangle[0]]) ?? 0,
|
||||
vertexIndexMap.get(faceVertexIds[triangle[1]]) ?? 0,
|
||||
vertexIndexMap.get(faceVertexIds[triangle[2]]) ?? 0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const geometry = new BufferGeometry();
|
||||
geometry.setAttribute("position", new BufferAttribute(new Float32Array(positions), 3));
|
||||
geometry.setAttribute("normal", new BufferAttribute(new Float32Array(normals), 3));
|
||||
geometry.setAttribute("uv", new BufferAttribute(new Float32Array(uvs), 2));
|
||||
geometry.setAttribute("faceUv", new BufferAttribute(new Float32Array(faceUvs), 2));
|
||||
geometry.setIndex(indices);
|
||||
|
||||
for (const group of groups) {
|
||||
geometry.addGroup(group.start, group.count, group.materialIndex);
|
||||
}
|
||||
|
||||
geometry.computeBoundingBox();
|
||||
geometry.computeBoundingSphere();
|
||||
|
||||
const firstVertex = brush.geometry.vertices.negX_negY_negZ;
|
||||
const localBounds = {
|
||||
min: cloneVec3(firstVertex),
|
||||
max: cloneVec3(firstVertex)
|
||||
};
|
||||
|
||||
for (const vertex of Object.values(brush.geometry.vertices)) {
|
||||
localBounds.min.x = Math.min(localBounds.min.x, vertex.x);
|
||||
localBounds.min.y = Math.min(localBounds.min.y, vertex.y);
|
||||
localBounds.min.z = Math.min(localBounds.min.z, vertex.z);
|
||||
localBounds.max.x = Math.max(localBounds.max.x, vertex.x);
|
||||
localBounds.max.y = Math.max(localBounds.max.y, vertex.y);
|
||||
localBounds.max.z = Math.max(localBounds.max.z, vertex.z);
|
||||
}
|
||||
|
||||
return {
|
||||
geometry,
|
||||
faceSurfaces,
|
||||
edgeSegments: BOX_EDGE_IDS.map((edgeId) => {
|
||||
const [startId, endId] = EDGE_VERTEX_IDS[edgeId];
|
||||
return {
|
||||
edgeId,
|
||||
start: getBoxBrushLocalVertexPosition(brush, startId),
|
||||
end: getBoxBrushLocalVertexPosition(brush, endId)
|
||||
};
|
||||
}),
|
||||
colliderVertices: new Float32Array(colliderVertices),
|
||||
colliderIndices: new Uint32Array(colliderIndices),
|
||||
localBounds
|
||||
};
|
||||
}
|
||||
|
||||
export function validateBoxBrushGeometry(brush: BoxBrush): BoxBrushGeometryDiagnostic[] {
|
||||
const diagnostics: BoxBrushGeometryDiagnostic[] = [];
|
||||
|
||||
for (const [vertexId, vertex] of Object.entries(brush.geometry.vertices) as Array<[BoxVertexId, Vec3]>) {
|
||||
if (!Number.isFinite(vertex.x) || !Number.isFinite(vertex.y) || !Number.isFinite(vertex.z)) {
|
||||
diagnostics.push({
|
||||
code: "invalid-box-geometry-vertex",
|
||||
message: `Whitebox vertex ${vertexId} must remain finite.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const faceId of BOX_FACE_IDS) {
|
||||
const faceVertices = FACE_VERTEX_IDS[faceId].map((vertexId) => brush.geometry.vertices[vertexId]);
|
||||
const normal = computeNewellNormal(faceVertices);
|
||||
|
||||
if (getVectorLength(normal) <= 1e-8) {
|
||||
diagnostics.push({
|
||||
code: "degenerate-box-face",
|
||||
message: `Whitebox face ${faceId} is degenerate and cannot be triangulated.`,
|
||||
faceId
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
triangulateQuad(faceVertices);
|
||||
} catch (error) {
|
||||
diagnostics.push({
|
||||
code: "invalid-box-face-triangulation",
|
||||
message: error instanceof Error ? `Whitebox face ${faceId} could not be triangulated: ${error.message}` : `Whitebox face ${faceId} could not be triangulated.`,
|
||||
faceId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return diagnostics;
|
||||
}
|
||||
316
src/geometry/whitebox-brush.ts
Normal file
316
src/geometry/whitebox-brush.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
import { Euler, MathUtils, Quaternion, Vector3 } from "three";
|
||||
|
||||
import type { Vec3 } from "../core/vector";
|
||||
import type {
|
||||
Brush,
|
||||
BrushGeometry,
|
||||
WhiteboxEdgeId,
|
||||
WhiteboxFaceId,
|
||||
WhiteboxVertexId
|
||||
} from "../document/brushes";
|
||||
import {
|
||||
getBrushEdgeVertexIds,
|
||||
getBrushFaceVertexIds,
|
||||
getBrushVertexIds
|
||||
} from "./whitebox-topology";
|
||||
|
||||
export interface WhiteboxBounds {
|
||||
min: Vec3;
|
||||
max: Vec3;
|
||||
}
|
||||
|
||||
export interface WhiteboxEdgeWorldSegment {
|
||||
id: WhiteboxEdgeId;
|
||||
start: Vec3;
|
||||
end: Vec3;
|
||||
center: Vec3;
|
||||
}
|
||||
|
||||
function cloneVec3(vector: Vec3): Vec3 {
|
||||
return {
|
||||
x: vector.x,
|
||||
y: vector.y,
|
||||
z: vector.z
|
||||
};
|
||||
}
|
||||
|
||||
function createBrushRotationEuler(brush: Brush): Euler {
|
||||
return new Euler(
|
||||
MathUtils.degToRad(brush.rotationDegrees.x),
|
||||
MathUtils.degToRad(brush.rotationDegrees.y),
|
||||
MathUtils.degToRad(brush.rotationDegrees.z),
|
||||
"XYZ"
|
||||
);
|
||||
}
|
||||
|
||||
export function getBrushLocalVertexPosition(
|
||||
brush: { geometry: BrushGeometry },
|
||||
vertexId: WhiteboxVertexId
|
||||
): Vec3 {
|
||||
const vertex = brush.geometry.vertices[vertexId];
|
||||
|
||||
if (vertex === undefined) {
|
||||
throw new Error(`Whitebox vertex ${vertexId} does not exist on this brush.`);
|
||||
}
|
||||
|
||||
return cloneVec3(vertex);
|
||||
}
|
||||
|
||||
export function transformBrushWorldVectorToLocal(
|
||||
brush: Brush,
|
||||
worldVector: Vec3
|
||||
): Vec3 {
|
||||
const rotation = createBrushRotationEuler(brush);
|
||||
const inverseRotation = new Quaternion().setFromEuler(rotation).invert();
|
||||
const localVector = new Vector3(
|
||||
worldVector.x,
|
||||
worldVector.y,
|
||||
worldVector.z
|
||||
).applyQuaternion(inverseRotation);
|
||||
|
||||
return {
|
||||
x: localVector.x,
|
||||
y: localVector.y,
|
||||
z: localVector.z
|
||||
};
|
||||
}
|
||||
|
||||
export function transformBrushWorldPointToLocal(
|
||||
brush: Brush,
|
||||
worldPoint: Vec3
|
||||
): Vec3 {
|
||||
const rotation = createBrushRotationEuler(brush);
|
||||
const inverseRotation = new Quaternion().setFromEuler(rotation).invert();
|
||||
const localPoint = new Vector3(
|
||||
worldPoint.x - brush.center.x,
|
||||
worldPoint.y - brush.center.y,
|
||||
worldPoint.z - brush.center.z
|
||||
).applyQuaternion(inverseRotation);
|
||||
|
||||
return {
|
||||
x: localPoint.x,
|
||||
y: localPoint.y,
|
||||
z: localPoint.z
|
||||
};
|
||||
}
|
||||
|
||||
export function transformBrushLocalPointToWorld(
|
||||
brush: Brush,
|
||||
localPoint: Vec3
|
||||
): Vec3 {
|
||||
const rotation = createBrushRotationEuler(brush);
|
||||
const rotatedOffset = new Vector3(
|
||||
localPoint.x,
|
||||
localPoint.y,
|
||||
localPoint.z
|
||||
).applyEuler(rotation);
|
||||
|
||||
return {
|
||||
x: brush.center.x + rotatedOffset.x,
|
||||
y: brush.center.y + rotatedOffset.y,
|
||||
z: brush.center.z + rotatedOffset.z
|
||||
};
|
||||
}
|
||||
|
||||
export function getBrushVertexWorldPosition(
|
||||
brush: Brush,
|
||||
vertexId: WhiteboxVertexId
|
||||
): Vec3 {
|
||||
return transformBrushLocalPointToWorld(
|
||||
brush,
|
||||
getBrushLocalVertexPosition(brush, vertexId)
|
||||
);
|
||||
}
|
||||
|
||||
export function getBrushFaceWorldCenter(
|
||||
brush: Brush,
|
||||
faceId: WhiteboxFaceId
|
||||
): Vec3 {
|
||||
const vertexIds = getBrushFaceVertexIds(brush, faceId);
|
||||
const weight = 1 / vertexIds.length;
|
||||
const localCenter = vertexIds.reduce(
|
||||
(accumulator, vertexId) => {
|
||||
const vertex = getBrushLocalVertexPosition(brush, vertexId);
|
||||
|
||||
return {
|
||||
x: accumulator.x + vertex.x * weight,
|
||||
y: accumulator.y + vertex.y * weight,
|
||||
z: accumulator.z + vertex.z * weight
|
||||
};
|
||||
},
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
z: 0
|
||||
}
|
||||
);
|
||||
|
||||
return transformBrushLocalPointToWorld(brush, localCenter);
|
||||
}
|
||||
|
||||
export function getBrushEdgeWorldSegment(
|
||||
brush: Brush,
|
||||
edgeId: WhiteboxEdgeId
|
||||
): WhiteboxEdgeWorldSegment {
|
||||
const [startId, endId] = getBrushEdgeVertexIds(brush, edgeId);
|
||||
const start = getBrushVertexWorldPosition(brush, startId);
|
||||
const end = getBrushVertexWorldPosition(brush, endId);
|
||||
|
||||
return {
|
||||
id: edgeId,
|
||||
start,
|
||||
end,
|
||||
center: {
|
||||
x: (start.x + end.x) * 0.5,
|
||||
y: (start.y + end.y) * 0.5,
|
||||
z: (start.z + end.z) * 0.5
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getBrushBounds(brush: Brush): WhiteboxBounds {
|
||||
const corners = getBrushVertexIds(brush).map((vertexId) =>
|
||||
getBrushVertexWorldPosition(brush, vertexId)
|
||||
);
|
||||
const firstCorner = corners[0];
|
||||
const min = { ...firstCorner };
|
||||
const max = { ...firstCorner };
|
||||
|
||||
for (const corner of corners.slice(1)) {
|
||||
min.x = Math.min(min.x, corner.x);
|
||||
min.y = Math.min(min.y, corner.y);
|
||||
min.z = Math.min(min.z, corner.z);
|
||||
max.x = Math.max(max.x, corner.x);
|
||||
max.y = Math.max(max.y, corner.y);
|
||||
max.z = Math.max(max.z, corner.z);
|
||||
}
|
||||
|
||||
return {
|
||||
min,
|
||||
max
|
||||
};
|
||||
}
|
||||
|
||||
function subtractVec3(left: Vec3, right: Vec3): Vec3 {
|
||||
return {
|
||||
x: left.x - right.x,
|
||||
y: left.y - right.y,
|
||||
z: left.z - right.z
|
||||
};
|
||||
}
|
||||
|
||||
function crossVec3(left: Vec3, right: Vec3): Vec3 {
|
||||
return {
|
||||
x: left.y * right.z - left.z * right.y,
|
||||
y: left.z * right.x - left.x * right.z,
|
||||
z: left.x * right.y - left.y * right.x
|
||||
};
|
||||
}
|
||||
|
||||
function dotVec3(left: Vec3, right: Vec3): number {
|
||||
return left.x * right.x + left.y * right.y + left.z * right.z;
|
||||
}
|
||||
|
||||
function getVectorLength(vector: Vec3): number {
|
||||
return Math.sqrt(dotVec3(vector, vector));
|
||||
}
|
||||
|
||||
function normalizeVec3(vector: Vec3): Vec3 {
|
||||
const length = getVectorLength(vector);
|
||||
|
||||
if (length <= 1e-8) {
|
||||
return { x: 0, y: 0, z: 0 };
|
||||
}
|
||||
|
||||
return {
|
||||
x: vector.x / length,
|
||||
y: vector.y / length,
|
||||
z: vector.z / length
|
||||
};
|
||||
}
|
||||
|
||||
function getDominantAxis(vector: Vec3): "x" | "y" | "z" {
|
||||
const absolute = {
|
||||
x: Math.abs(vector.x),
|
||||
y: Math.abs(vector.y),
|
||||
z: Math.abs(vector.z)
|
||||
};
|
||||
|
||||
if (absolute.x >= absolute.y && absolute.x >= absolute.z) {
|
||||
return "x";
|
||||
}
|
||||
|
||||
if (absolute.y >= absolute.z) {
|
||||
return "y";
|
||||
}
|
||||
|
||||
return "z";
|
||||
}
|
||||
|
||||
export function getBrushFaceNormal(
|
||||
brush: Brush,
|
||||
faceId: WhiteboxFaceId
|
||||
): Vec3 {
|
||||
const vertexIds = getBrushFaceVertexIds(brush, faceId);
|
||||
const vertices = vertexIds.map((vertexId) =>
|
||||
getBrushLocalVertexPosition(brush, vertexId)
|
||||
);
|
||||
|
||||
let normal = { x: 0, y: 0, z: 0 };
|
||||
|
||||
for (let index = 0; index < vertices.length; index += 1) {
|
||||
const current = vertices[index];
|
||||
const next = vertices[(index + 1) % vertices.length];
|
||||
normal.x += (current.y - next.y) * (current.z + next.z);
|
||||
normal.y += (current.z - next.z) * (current.x + next.x);
|
||||
normal.z += (current.x - next.x) * (current.y + next.y);
|
||||
}
|
||||
|
||||
return normalizeVec3(normal);
|
||||
}
|
||||
|
||||
export function getBrushFaceAxis(
|
||||
brush: Brush,
|
||||
faceId: WhiteboxFaceId
|
||||
): "x" | "y" | "z" {
|
||||
return getDominantAxis(getBrushFaceNormal(brush, faceId));
|
||||
}
|
||||
|
||||
export function getBrushEdgeAxis(
|
||||
brush: Brush,
|
||||
edgeId: WhiteboxEdgeId
|
||||
): "x" | "y" | "z" {
|
||||
const [startId, endId] = getBrushEdgeVertexIds(brush, edgeId);
|
||||
const start = getBrushLocalVertexPosition(brush, startId);
|
||||
const end = getBrushLocalVertexPosition(brush, endId);
|
||||
|
||||
return getDominantAxis(subtractVec3(end, start));
|
||||
}
|
||||
|
||||
export function getBrushEdgeScaleAxes(
|
||||
brush: Brush,
|
||||
edgeId: WhiteboxEdgeId
|
||||
): Array<"x" | "y" | "z"> {
|
||||
const edgeAxis = getBrushEdgeAxis(brush, edgeId);
|
||||
return (["x", "y", "z"] as const).filter((axis) => axis !== edgeAxis);
|
||||
}
|
||||
|
||||
export function getBrushFaceBasis(
|
||||
brush: Brush,
|
||||
faceId: WhiteboxFaceId
|
||||
): { origin: Vec3; uAxis: Vec3; vAxis: Vec3 } {
|
||||
const vertexIds = getBrushFaceVertexIds(brush, faceId);
|
||||
const vertices = vertexIds.map((vertexId) =>
|
||||
getBrushLocalVertexPosition(brush, vertexId)
|
||||
);
|
||||
const origin = vertices[0];
|
||||
const firstEdge = normalizeVec3(subtractVec3(vertices[1], origin));
|
||||
const normal = getBrushFaceNormal(brush, faceId);
|
||||
const vAxis = normalizeVec3(crossVec3(normal, firstEdge));
|
||||
|
||||
return {
|
||||
origin,
|
||||
uAxis: firstEdge,
|
||||
vAxis
|
||||
};
|
||||
}
|
||||
316
src/geometry/whitebox-topology.ts
Normal file
316
src/geometry/whitebox-topology.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
import {
|
||||
BOX_EDGE_IDS,
|
||||
BOX_EDGE_LABELS,
|
||||
BOX_FACE_IDS,
|
||||
BOX_FACE_LABELS,
|
||||
BOX_VERTEX_IDS,
|
||||
BOX_VERTEX_LABELS,
|
||||
WEDGE_EDGE_IDS,
|
||||
WEDGE_EDGE_LABELS,
|
||||
WEDGE_FACE_IDS,
|
||||
WEDGE_FACE_LABELS,
|
||||
WEDGE_VERTEX_IDS,
|
||||
WEDGE_VERTEX_LABELS,
|
||||
getRadialPrismEdgeIds,
|
||||
getRadialPrismFaceIds,
|
||||
getRadialPrismVertexIds,
|
||||
type Brush,
|
||||
type BoxBrush,
|
||||
type BoxEdgeId,
|
||||
type BoxFaceId,
|
||||
type BoxVertexId,
|
||||
type RadialPrismBrush,
|
||||
type RadialPrismEdgeId,
|
||||
type RadialPrismFaceId,
|
||||
type RadialPrismVertexId,
|
||||
type WhiteboxEdgeId,
|
||||
type WhiteboxFaceId,
|
||||
type WhiteboxVertexId,
|
||||
type WedgeBrush,
|
||||
type WedgeEdgeId,
|
||||
type WedgeFaceId,
|
||||
type WedgeVertexId
|
||||
} from "../document/brushes";
|
||||
|
||||
const BOX_FACE_VERTEX_IDS: Record<
|
||||
BoxFaceId,
|
||||
readonly [BoxVertexId, BoxVertexId, BoxVertexId, BoxVertexId]
|
||||
> = {
|
||||
posX: ["posX_negY_posZ", "posX_negY_negZ", "posX_posY_negZ", "posX_posY_posZ"],
|
||||
negX: ["negX_negY_negZ", "negX_negY_posZ", "negX_posY_posZ", "negX_posY_negZ"],
|
||||
posY: ["negX_posY_posZ", "posX_posY_posZ", "posX_posY_negZ", "negX_posY_negZ"],
|
||||
negY: ["negX_negY_negZ", "posX_negY_negZ", "posX_negY_posZ", "negX_negY_posZ"],
|
||||
posZ: ["negX_negY_posZ", "posX_negY_posZ", "posX_posY_posZ", "negX_posY_posZ"],
|
||||
negZ: ["posX_negY_negZ", "negX_negY_negZ", "negX_posY_negZ", "posX_posY_negZ"]
|
||||
};
|
||||
|
||||
const BOX_EDGE_VERTEX_IDS: Record<
|
||||
BoxEdgeId,
|
||||
readonly [BoxVertexId, BoxVertexId]
|
||||
> = {
|
||||
edgeX_negY_negZ: ["negX_negY_negZ", "posX_negY_negZ"],
|
||||
edgeX_posY_negZ: ["negX_posY_negZ", "posX_posY_negZ"],
|
||||
edgeX_negY_posZ: ["negX_negY_posZ", "posX_negY_posZ"],
|
||||
edgeX_posY_posZ: ["negX_posY_posZ", "posX_posY_posZ"],
|
||||
edgeY_negX_negZ: ["negX_negY_negZ", "negX_posY_negZ"],
|
||||
edgeY_posX_negZ: ["posX_negY_negZ", "posX_posY_negZ"],
|
||||
edgeY_negX_posZ: ["negX_negY_posZ", "negX_posY_posZ"],
|
||||
edgeY_posX_posZ: ["posX_negY_posZ", "posX_posY_posZ"],
|
||||
edgeZ_negX_negY: ["negX_negY_negZ", "negX_negY_posZ"],
|
||||
edgeZ_posX_negY: ["posX_negY_negZ", "posX_negY_posZ"],
|
||||
edgeZ_negX_posY: ["negX_posY_negZ", "negX_posY_posZ"],
|
||||
edgeZ_posX_posY: ["posX_posY_negZ", "posX_posY_posZ"]
|
||||
};
|
||||
|
||||
const WEDGE_FACE_VERTEX_IDS: Record<WedgeFaceId, readonly WedgeVertexId[]> = {
|
||||
bottom: [
|
||||
"negX_negY_negZ",
|
||||
"posX_negY_negZ",
|
||||
"posX_negY_posZ",
|
||||
"negX_negY_posZ"
|
||||
],
|
||||
back: ["posX_negY_negZ", "negX_negY_negZ", "negX_posY_negZ", "posX_posY_negZ"],
|
||||
slope: ["negX_negY_posZ", "posX_negY_posZ", "posX_posY_negZ", "negX_posY_negZ"],
|
||||
left: ["negX_negY_negZ", "negX_negY_posZ", "negX_posY_negZ"],
|
||||
right: ["posX_negY_posZ", "posX_negY_negZ", "posX_posY_negZ"]
|
||||
};
|
||||
|
||||
const WEDGE_EDGE_VERTEX_IDS: Record<
|
||||
WedgeEdgeId,
|
||||
readonly [WedgeVertexId, WedgeVertexId]
|
||||
> = {
|
||||
bottomBack: ["negX_negY_negZ", "posX_negY_negZ"],
|
||||
bottomFront: ["negX_negY_posZ", "posX_negY_posZ"],
|
||||
bottomLeft: ["negX_negY_negZ", "negX_negY_posZ"],
|
||||
bottomRight: ["posX_negY_negZ", "posX_negY_posZ"],
|
||||
topBack: ["negX_posY_negZ", "posX_posY_negZ"],
|
||||
leftBack: ["negX_negY_negZ", "negX_posY_negZ"],
|
||||
rightBack: ["posX_negY_negZ", "posX_posY_negZ"],
|
||||
leftSlope: ["negX_posY_negZ", "negX_negY_posZ"],
|
||||
rightSlope: ["posX_posY_negZ", "posX_negY_posZ"]
|
||||
};
|
||||
|
||||
function getRadialPrismFaceLabel(faceId: RadialPrismFaceId): string {
|
||||
if (faceId === "top") {
|
||||
return "Top";
|
||||
}
|
||||
|
||||
if (faceId === "bottom") {
|
||||
return "Bottom";
|
||||
}
|
||||
|
||||
return `Side ${Number(faceId.slice(5)) + 1}`;
|
||||
}
|
||||
|
||||
function getRadialPrismEdgeLabel(edgeId: RadialPrismEdgeId): string {
|
||||
if (edgeId.startsWith("vertical-")) {
|
||||
return `Vertical ${Number(edgeId.slice(9)) + 1}`;
|
||||
}
|
||||
|
||||
if (edgeId.startsWith("top-")) {
|
||||
return `Top Ring ${Number(edgeId.slice(4)) + 1}`;
|
||||
}
|
||||
|
||||
return `Bottom Ring ${Number(edgeId.slice(7)) + 1}`;
|
||||
}
|
||||
|
||||
function getRadialPrismVertexLabel(vertexId: RadialPrismVertexId): string {
|
||||
if (vertexId.startsWith("top-")) {
|
||||
return `Top ${Number(vertexId.slice(4)) + 1}`;
|
||||
}
|
||||
|
||||
return `Bottom ${Number(vertexId.slice(7)) + 1}`;
|
||||
}
|
||||
|
||||
function getRadialPrismFaceVertexIds(
|
||||
brush: RadialPrismBrush,
|
||||
faceId: RadialPrismFaceId
|
||||
): WhiteboxVertexId[] {
|
||||
if (faceId === "top") {
|
||||
return Array.from({ length: brush.sideCount }, (_, index) => `top-${brush.sideCount - 1 - index}` as const);
|
||||
}
|
||||
|
||||
if (faceId === "bottom") {
|
||||
return Array.from({ length: brush.sideCount }, (_, index) => `bottom-${index}` as const);
|
||||
}
|
||||
|
||||
const sideIndex = Number(faceId.slice(5));
|
||||
const nextIndex = (sideIndex + 1) % brush.sideCount;
|
||||
|
||||
return [
|
||||
`bottom-${sideIndex}`,
|
||||
`bottom-${nextIndex}`,
|
||||
`top-${nextIndex}`,
|
||||
`top-${sideIndex}`
|
||||
];
|
||||
}
|
||||
|
||||
function getRadialPrismEdgeVertexIds(
|
||||
edgeId: RadialPrismEdgeId,
|
||||
sideCount: number
|
||||
): [RadialPrismVertexId, RadialPrismVertexId] {
|
||||
if (edgeId.startsWith("vertical-")) {
|
||||
const index = Number(edgeId.slice(9));
|
||||
return [`bottom-${index}`, `top-${index}`];
|
||||
}
|
||||
|
||||
if (edgeId.startsWith("top-")) {
|
||||
const index = Number(edgeId.slice(4));
|
||||
const nextIndex = (index + 1) % sideCount;
|
||||
return [`top-${index}`, `top-${nextIndex}`];
|
||||
}
|
||||
|
||||
const index = Number(edgeId.slice(7));
|
||||
const nextIndex = (index + 1) % sideCount;
|
||||
return [`bottom-${index}`, `bottom-${nextIndex}`];
|
||||
}
|
||||
|
||||
export function getBrushFaceIds(brush: Brush): WhiteboxFaceId[] {
|
||||
switch (brush.kind) {
|
||||
case "box":
|
||||
return [...BOX_FACE_IDS];
|
||||
case "wedge":
|
||||
return [...WEDGE_FACE_IDS];
|
||||
case "radialPrism":
|
||||
return getRadialPrismFaceIds(brush.sideCount);
|
||||
}
|
||||
}
|
||||
|
||||
export function getBrushEdgeIds(brush: Brush): WhiteboxEdgeId[] {
|
||||
switch (brush.kind) {
|
||||
case "box":
|
||||
return [...BOX_EDGE_IDS];
|
||||
case "wedge":
|
||||
return [...WEDGE_EDGE_IDS];
|
||||
case "radialPrism":
|
||||
return getRadialPrismEdgeIds(brush.sideCount);
|
||||
}
|
||||
}
|
||||
|
||||
export function getBrushVertexIds(brush: Brush): WhiteboxVertexId[] {
|
||||
switch (brush.kind) {
|
||||
case "box":
|
||||
return [...BOX_VERTEX_IDS];
|
||||
case "wedge":
|
||||
return [...WEDGE_VERTEX_IDS];
|
||||
case "radialPrism":
|
||||
return getRadialPrismVertexIds(brush.sideCount);
|
||||
}
|
||||
}
|
||||
|
||||
export function getBrushFaceLabel(
|
||||
brush: Brush,
|
||||
faceId: WhiteboxFaceId
|
||||
): string {
|
||||
switch (brush.kind) {
|
||||
case "box":
|
||||
return BOX_FACE_LABELS[faceId as BoxFaceId] ?? faceId;
|
||||
case "wedge":
|
||||
return WEDGE_FACE_LABELS[faceId as WedgeFaceId] ?? faceId;
|
||||
case "radialPrism":
|
||||
return getRadialPrismFaceLabel(faceId as RadialPrismFaceId);
|
||||
}
|
||||
}
|
||||
|
||||
export function getBrushEdgeLabel(
|
||||
brush: Brush,
|
||||
edgeId: WhiteboxEdgeId
|
||||
): string {
|
||||
switch (brush.kind) {
|
||||
case "box":
|
||||
return BOX_EDGE_LABELS[edgeId as BoxEdgeId] ?? edgeId;
|
||||
case "wedge":
|
||||
return WEDGE_EDGE_LABELS[edgeId as WedgeEdgeId] ?? edgeId;
|
||||
case "radialPrism":
|
||||
return getRadialPrismEdgeLabel(edgeId as RadialPrismEdgeId);
|
||||
}
|
||||
}
|
||||
|
||||
export function getBrushVertexLabel(
|
||||
brush: Brush,
|
||||
vertexId: WhiteboxVertexId
|
||||
): string {
|
||||
switch (brush.kind) {
|
||||
case "box":
|
||||
return BOX_VERTEX_LABELS[vertexId as BoxVertexId] ?? vertexId;
|
||||
case "wedge":
|
||||
return WEDGE_VERTEX_LABELS[vertexId as WedgeVertexId] ?? vertexId;
|
||||
case "radialPrism":
|
||||
return getRadialPrismVertexLabel(vertexId as RadialPrismVertexId);
|
||||
}
|
||||
}
|
||||
|
||||
export function getBrushFaceVertexIds(
|
||||
brush: Brush,
|
||||
faceId: WhiteboxFaceId
|
||||
): WhiteboxVertexId[] {
|
||||
switch (brush.kind) {
|
||||
case "box":
|
||||
return [...BOX_FACE_VERTEX_IDS[faceId as BoxFaceId]];
|
||||
case "wedge":
|
||||
return [...WEDGE_FACE_VERTEX_IDS[faceId as WedgeFaceId]];
|
||||
case "radialPrism":
|
||||
return getRadialPrismFaceVertexIds(brush, faceId as RadialPrismFaceId);
|
||||
}
|
||||
}
|
||||
|
||||
export function getBrushEdgeVertexIds(
|
||||
brush: Brush,
|
||||
edgeId: WhiteboxEdgeId
|
||||
): [WhiteboxVertexId, WhiteboxVertexId] {
|
||||
switch (brush.kind) {
|
||||
case "box":
|
||||
return [...BOX_EDGE_VERTEX_IDS[edgeId as BoxEdgeId]] as [
|
||||
WhiteboxVertexId,
|
||||
WhiteboxVertexId
|
||||
];
|
||||
case "wedge":
|
||||
return [...WEDGE_EDGE_VERTEX_IDS[edgeId as WedgeEdgeId]] as [
|
||||
WhiteboxVertexId,
|
||||
WhiteboxVertexId
|
||||
];
|
||||
case "radialPrism":
|
||||
return getRadialPrismEdgeVertexIds(
|
||||
edgeId as RadialPrismEdgeId,
|
||||
brush.sideCount
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function getBoxBrushFaceVertexIds(faceId: BoxFaceId): readonly [
|
||||
BoxVertexId,
|
||||
BoxVertexId,
|
||||
BoxVertexId,
|
||||
BoxVertexId
|
||||
] {
|
||||
return BOX_FACE_VERTEX_IDS[faceId];
|
||||
}
|
||||
|
||||
export function getBoxBrushEdgeVertexIds(edgeId: BoxEdgeId): readonly [
|
||||
BoxVertexId,
|
||||
BoxVertexId
|
||||
] {
|
||||
return BOX_EDGE_VERTEX_IDS[edgeId];
|
||||
}
|
||||
|
||||
export function getBrushDefaultName(brush: Brush, index: number): string {
|
||||
switch (brush.kind) {
|
||||
case "box":
|
||||
return `Whitebox Box ${index + 1}`;
|
||||
case "wedge":
|
||||
return `Whitebox Wedge ${index + 1}`;
|
||||
case "radialPrism":
|
||||
return `Whitebox Cylinder ${index + 1}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function getBrushKindLabel(brush: Brush): string {
|
||||
switch (brush.kind) {
|
||||
case "box":
|
||||
return "Whitebox Box";
|
||||
case "wedge":
|
||||
return "Whitebox Wedge";
|
||||
case "radialPrism":
|
||||
return "Whitebox Cylinder";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user