Add box brush mesh and update related files
This commit is contained in:
@@ -1,25 +1,25 @@
|
||||
import { cloneEditorSelection, type EditorSelection } from "../core/selection";
|
||||
import {
|
||||
cloneBrush,
|
||||
cloneBrushGeometry,
|
||||
cloneFaceUvState,
|
||||
type BoxBrush,
|
||||
type BoxEdgeId,
|
||||
type BoxFaceId,
|
||||
type BoxVertexId,
|
||||
type Brush,
|
||||
type BrushFace
|
||||
} from "../document/brushes";
|
||||
import type { SceneDocument } from "../document/scene-document";
|
||||
import type {
|
||||
WhiteboxEdgeId,
|
||||
WhiteboxFaceId,
|
||||
WhiteboxVertexId
|
||||
} from "../document/brushes";
|
||||
|
||||
export function getBoxBrushOrThrow(document: SceneDocument, brushId: string): BoxBrush {
|
||||
export function getBoxBrushOrThrow(document: SceneDocument, brushId: string): Brush {
|
||||
const brush = document.brushes[brushId];
|
||||
|
||||
if (brush === undefined) {
|
||||
throw new Error(`Box brush ${brushId} does not exist.`);
|
||||
}
|
||||
|
||||
if (brush.kind !== "box") {
|
||||
throw new Error(`Brush ${brushId} is not a supported box brush.`);
|
||||
}
|
||||
|
||||
return brush;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export function setSingleBrushSelection(brushId: string): EditorSelection {
|
||||
};
|
||||
}
|
||||
|
||||
export function setSingleBrushFaceSelection(brushId: string, faceId: BoxFaceId): EditorSelection {
|
||||
export function setSingleBrushFaceSelection(brushId: string, faceId: WhiteboxFaceId): EditorSelection {
|
||||
return {
|
||||
kind: "brushFace",
|
||||
brushId,
|
||||
@@ -38,7 +38,7 @@ export function setSingleBrushFaceSelection(brushId: string, faceId: BoxFaceId):
|
||||
};
|
||||
}
|
||||
|
||||
export function setSingleBrushEdgeSelection(brushId: string, edgeId: BoxEdgeId): EditorSelection {
|
||||
export function setSingleBrushEdgeSelection(brushId: string, edgeId: WhiteboxEdgeId): EditorSelection {
|
||||
return {
|
||||
kind: "brushEdge",
|
||||
brushId,
|
||||
@@ -46,7 +46,7 @@ export function setSingleBrushEdgeSelection(brushId: string, edgeId: BoxEdgeId):
|
||||
};
|
||||
}
|
||||
|
||||
export function setSingleBrushVertexSelection(brushId: string, vertexId: BoxVertexId): EditorSelection {
|
||||
export function setSingleBrushVertexSelection(brushId: string, vertexId: WhiteboxVertexId): EditorSelection {
|
||||
return {
|
||||
kind: "brushVertex",
|
||||
brushId,
|
||||
@@ -58,12 +58,12 @@ export function cloneSelectionForCommand(selection: EditorSelection): EditorSele
|
||||
return cloneEditorSelection(selection);
|
||||
}
|
||||
|
||||
export function replaceBrush(document: SceneDocument, brush: BoxBrush): SceneDocument {
|
||||
export function replaceBrush(document: SceneDocument, brush: Brush): SceneDocument {
|
||||
return {
|
||||
...document,
|
||||
brushes: {
|
||||
...document.brushes,
|
||||
[brush.id]: brush
|
||||
[brush.id]: cloneBrush(brush)
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -80,7 +80,7 @@ export function removeBrush(document: SceneDocument, brushId: string): SceneDocu
|
||||
};
|
||||
}
|
||||
|
||||
export function getBoxBrushFaceOrThrow(document: SceneDocument, brushId: string, faceId: BoxFaceId): BrushFace {
|
||||
export function getBoxBrushFaceOrThrow(document: SceneDocument, brushId: string, faceId: WhiteboxFaceId): BrushFace {
|
||||
const brush = getBoxBrushOrThrow(document, brushId);
|
||||
const face = brush.faces[faceId];
|
||||
|
||||
@@ -91,7 +91,7 @@ export function getBoxBrushFaceOrThrow(document: SceneDocument, brushId: string,
|
||||
return face;
|
||||
}
|
||||
|
||||
export function replaceBoxBrushFace(document: SceneDocument, brushId: string, faceId: BoxFaceId, face: BrushFace): SceneDocument {
|
||||
export function replaceBoxBrushFace(document: SceneDocument, brushId: string, faceId: WhiteboxFaceId, face: BrushFace): SceneDocument {
|
||||
const brush = getBoxBrushOrThrow(document, brushId);
|
||||
|
||||
return replaceBrush(document, {
|
||||
@@ -102,6 +102,7 @@ export function replaceBoxBrushFace(document: SceneDocument, brushId: string, fa
|
||||
materialId: face.materialId,
|
||||
uv: cloneFaceUvState(face.uv)
|
||||
}
|
||||
}
|
||||
},
|
||||
geometry: cloneBrushGeometry(brush.geometry)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { ToolMode } from "../core/tool-mode";
|
||||
import { createOpaqueId } from "../core/ids";
|
||||
import type { EditorSelection } from "../core/selection";
|
||||
import { BOX_FACE_IDS, type BoxFaceId } from "../document/brushes";
|
||||
import type { WhiteboxFaceId } from "../document/brushes";
|
||||
import { getBrushFaceIds } from "../geometry/whitebox-topology";
|
||||
|
||||
import {
|
||||
cloneSelectionForCommand,
|
||||
@@ -18,7 +19,7 @@ interface SetBoxBrushAllFaceMaterialsCommandOptions {
|
||||
export function createSetBoxBrushAllFaceMaterialsCommand(
|
||||
options: SetBoxBrushAllFaceMaterialsCommandOptions
|
||||
): EditorCommand {
|
||||
let previousMaterialIds: Record<BoxFaceId, string | null> | null = null;
|
||||
let previousMaterialIds: Record<WhiteboxFaceId, string | null> | null = null;
|
||||
let previousSelection: EditorSelection | null = null;
|
||||
let previousToolMode: ToolMode | null = null;
|
||||
|
||||
@@ -42,14 +43,12 @@ export function createSetBoxBrushAllFaceMaterialsCommand(
|
||||
}
|
||||
|
||||
if (previousMaterialIds === null) {
|
||||
previousMaterialIds = {
|
||||
posX: currentBrush.faces.posX.materialId,
|
||||
negX: currentBrush.faces.negX.materialId,
|
||||
posY: currentBrush.faces.posY.materialId,
|
||||
negY: currentBrush.faces.negY.materialId,
|
||||
posZ: currentBrush.faces.posZ.materialId,
|
||||
negZ: currentBrush.faces.negZ.materialId
|
||||
};
|
||||
previousMaterialIds = Object.fromEntries(
|
||||
getBrushFaceIds(currentBrush).map((faceId) => [
|
||||
faceId,
|
||||
currentBrush.faces[faceId].materialId
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if (previousSelection === null) {
|
||||
@@ -64,7 +63,7 @@ export function createSetBoxBrushAllFaceMaterialsCommand(
|
||||
replaceBrush(currentDocument, {
|
||||
...currentBrush,
|
||||
faces: Object.fromEntries(
|
||||
BOX_FACE_IDS.map((faceId) => [
|
||||
getBrushFaceIds(currentBrush).map((faceId) => [
|
||||
faceId,
|
||||
{
|
||||
...currentBrush.faces[faceId],
|
||||
@@ -87,32 +86,15 @@ export function createSetBoxBrushAllFaceMaterialsCommand(
|
||||
context.setDocument(
|
||||
replaceBrush(currentDocument, {
|
||||
...currentBrush,
|
||||
faces: {
|
||||
posX: {
|
||||
...currentBrush.faces.posX,
|
||||
materialId: previousMaterialIds.posX
|
||||
},
|
||||
negX: {
|
||||
...currentBrush.faces.negX,
|
||||
materialId: previousMaterialIds.negX
|
||||
},
|
||||
posY: {
|
||||
...currentBrush.faces.posY,
|
||||
materialId: previousMaterialIds.posY
|
||||
},
|
||||
negY: {
|
||||
...currentBrush.faces.negY,
|
||||
materialId: previousMaterialIds.negY
|
||||
},
|
||||
posZ: {
|
||||
...currentBrush.faces.posZ,
|
||||
materialId: previousMaterialIds.posZ
|
||||
},
|
||||
negZ: {
|
||||
...currentBrush.faces.negZ,
|
||||
materialId: previousMaterialIds.negZ
|
||||
}
|
||||
}
|
||||
faces: Object.fromEntries(
|
||||
getBrushFaceIds(currentBrush).map((faceId) => [
|
||||
faceId,
|
||||
{
|
||||
...currentBrush.faces[faceId],
|
||||
materialId: previousMaterialIds[faceId] ?? null
|
||||
}
|
||||
])
|
||||
) as typeof currentBrush.faces
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ToolMode } from "../core/tool-mode";
|
||||
import { createOpaqueId } from "../core/ids";
|
||||
import type { EditorSelection } from "../core/selection";
|
||||
import type { BoxFaceId } from "../document/brushes";
|
||||
import type { WhiteboxFaceId } from "../document/brushes";
|
||||
|
||||
import {
|
||||
cloneSelectionForCommand,
|
||||
@@ -13,7 +13,7 @@ import type { EditorCommand } from "./command";
|
||||
|
||||
interface SetBoxBrushFaceMaterialCommandOptions {
|
||||
brushId: string;
|
||||
faceId: BoxFaceId;
|
||||
faceId: WhiteboxFaceId;
|
||||
materialId: string | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import type { ToolMode } from "../core/tool-mode";
|
||||
import { createOpaqueId } from "../core/ids";
|
||||
import type { EditorSelection } from "../core/selection";
|
||||
import { cloneFaceUvState, type BoxFaceId, type FaceUvState } from "../document/brushes";
|
||||
import {
|
||||
cloneFaceUvState,
|
||||
type FaceUvState,
|
||||
type WhiteboxFaceId
|
||||
} from "../document/brushes";
|
||||
|
||||
import {
|
||||
cloneSelectionForCommand,
|
||||
@@ -13,7 +17,7 @@ import type { EditorCommand } from "./command";
|
||||
|
||||
interface SetBoxBrushFaceUvStateCommandOptions {
|
||||
brushId: string;
|
||||
faceId: BoxFaceId;
|
||||
faceId: WhiteboxFaceId;
|
||||
uvState: FaceUvState;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ import type { ToolMode } from "../core/tool-mode";
|
||||
import { createOpaqueId } from "../core/ids";
|
||||
import type { EditorSelection } from "../core/selection";
|
||||
import {
|
||||
BOX_FACE_IDS,
|
||||
cloneFaceUvState,
|
||||
type BoxFaceId,
|
||||
type FaceUvState
|
||||
type FaceUvState,
|
||||
type WhiteboxFaceId
|
||||
} from "../document/brushes";
|
||||
import { getBrushFaceIds } from "../geometry/whitebox-topology";
|
||||
|
||||
import {
|
||||
cloneSelectionForCommand,
|
||||
@@ -18,13 +18,13 @@ import type { EditorCommand } from "./command";
|
||||
interface UpdateBoxBrushAllFaceUvsCommandOptions {
|
||||
brushId: string;
|
||||
label: string;
|
||||
updateUvState(uvState: FaceUvState, faceId: BoxFaceId): FaceUvState;
|
||||
updateUvState(uvState: FaceUvState, faceId: WhiteboxFaceId): FaceUvState;
|
||||
}
|
||||
|
||||
export function createUpdateBoxBrushAllFaceUvsCommand(
|
||||
options: UpdateBoxBrushAllFaceUvsCommandOptions
|
||||
): EditorCommand {
|
||||
let previousUvStates: Record<BoxFaceId, FaceUvState> | null = null;
|
||||
let previousUvStates: Record<WhiteboxFaceId, FaceUvState> | null = null;
|
||||
let previousSelection: EditorSelection | null = null;
|
||||
let previousToolMode: ToolMode | null = null;
|
||||
|
||||
@@ -36,14 +36,12 @@ export function createUpdateBoxBrushAllFaceUvsCommand(
|
||||
const currentBrush = getBoxBrushOrThrow(currentDocument, options.brushId);
|
||||
|
||||
if (previousUvStates === null) {
|
||||
previousUvStates = {
|
||||
posX: cloneFaceUvState(currentBrush.faces.posX.uv),
|
||||
negX: cloneFaceUvState(currentBrush.faces.negX.uv),
|
||||
posY: cloneFaceUvState(currentBrush.faces.posY.uv),
|
||||
negY: cloneFaceUvState(currentBrush.faces.negY.uv),
|
||||
posZ: cloneFaceUvState(currentBrush.faces.posZ.uv),
|
||||
negZ: cloneFaceUvState(currentBrush.faces.negZ.uv)
|
||||
};
|
||||
previousUvStates = Object.fromEntries(
|
||||
getBrushFaceIds(currentBrush).map((faceId) => [
|
||||
faceId,
|
||||
cloneFaceUvState(currentBrush.faces[faceId].uv)
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if (previousSelection === null) {
|
||||
@@ -58,7 +56,7 @@ export function createUpdateBoxBrushAllFaceUvsCommand(
|
||||
replaceBrush(currentDocument, {
|
||||
...currentBrush,
|
||||
faces: Object.fromEntries(
|
||||
BOX_FACE_IDS.map((faceId) => [
|
||||
getBrushFaceIds(currentBrush).map((faceId) => [
|
||||
faceId,
|
||||
{
|
||||
...currentBrush.faces[faceId],
|
||||
@@ -83,32 +81,15 @@ export function createUpdateBoxBrushAllFaceUvsCommand(
|
||||
context.setDocument(
|
||||
replaceBrush(currentDocument, {
|
||||
...currentBrush,
|
||||
faces: {
|
||||
posX: {
|
||||
...currentBrush.faces.posX,
|
||||
uv: cloneFaceUvState(previousUvStates.posX)
|
||||
},
|
||||
negX: {
|
||||
...currentBrush.faces.negX,
|
||||
uv: cloneFaceUvState(previousUvStates.negX)
|
||||
},
|
||||
posY: {
|
||||
...currentBrush.faces.posY,
|
||||
uv: cloneFaceUvState(previousUvStates.posY)
|
||||
},
|
||||
negY: {
|
||||
...currentBrush.faces.negY,
|
||||
uv: cloneFaceUvState(previousUvStates.negY)
|
||||
},
|
||||
posZ: {
|
||||
...currentBrush.faces.posZ,
|
||||
uv: cloneFaceUvState(previousUvStates.posZ)
|
||||
},
|
||||
negZ: {
|
||||
...currentBrush.faces.negZ,
|
||||
uv: cloneFaceUvState(previousUvStates.negZ)
|
||||
}
|
||||
}
|
||||
faces: Object.fromEntries(
|
||||
getBrushFaceIds(currentBrush).map((faceId) => [
|
||||
faceId,
|
||||
{
|
||||
...currentBrush.faces[faceId],
|
||||
uv: cloneFaceUvState(previousUvStates[faceId])
|
||||
}
|
||||
])
|
||||
) as typeof currentBrush.faces
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import type { WhiteboxSelectionMode } from "./whitebox-selection-mode";
|
||||
import type { BoxEdgeId, BoxFaceId, BoxVertexId } from "../document/brushes";
|
||||
import type {
|
||||
WhiteboxEdgeId,
|
||||
WhiteboxFaceId,
|
||||
WhiteboxVertexId
|
||||
} from "../document/brushes";
|
||||
|
||||
export type EditorSelection =
|
||||
| { kind: "none" }
|
||||
| { kind: "brushes"; ids: string[] }
|
||||
| { kind: "brushFace"; brushId: string; faceId: BoxFaceId }
|
||||
| { kind: "brushEdge"; brushId: string; edgeId: BoxEdgeId }
|
||||
| { kind: "brushVertex"; brushId: string; vertexId: BoxVertexId }
|
||||
| { kind: "brushFace"; brushId: string; faceId: WhiteboxFaceId }
|
||||
| { kind: "brushEdge"; brushId: string; edgeId: WhiteboxEdgeId }
|
||||
| { kind: "brushVertex"; brushId: string; vertexId: WhiteboxVertexId }
|
||||
| { kind: "paths"; ids: string[] }
|
||||
| { kind: "pathPoint"; pathId: string; pointId: string }
|
||||
| { kind: "entities"; ids: string[] }
|
||||
@@ -93,7 +97,9 @@ export function getSingleSelectedBrushId(selection: EditorSelection): string | n
|
||||
return selection.ids[0];
|
||||
}
|
||||
|
||||
export function getSelectedBrushFaceId(selection: EditorSelection): BoxFaceId | null {
|
||||
export function getSelectedBrushFaceId(
|
||||
selection: EditorSelection
|
||||
): WhiteboxFaceId | null {
|
||||
if (selection.kind !== "brushFace") {
|
||||
return null;
|
||||
}
|
||||
@@ -101,7 +107,9 @@ export function getSelectedBrushFaceId(selection: EditorSelection): BoxFaceId |
|
||||
return selection.faceId;
|
||||
}
|
||||
|
||||
export function getSelectedBrushEdgeId(selection: EditorSelection): BoxEdgeId | null {
|
||||
export function getSelectedBrushEdgeId(
|
||||
selection: EditorSelection
|
||||
): WhiteboxEdgeId | null {
|
||||
if (selection.kind !== "brushEdge") {
|
||||
return null;
|
||||
}
|
||||
@@ -109,7 +117,9 @@ export function getSelectedBrushEdgeId(selection: EditorSelection): BoxEdgeId |
|
||||
return selection.edgeId;
|
||||
}
|
||||
|
||||
export function getSelectedBrushVertexId(selection: EditorSelection): BoxVertexId | null {
|
||||
export function getSelectedBrushVertexId(
|
||||
selection: EditorSelection
|
||||
): WhiteboxVertexId | null {
|
||||
if (selection.kind !== "brushVertex") {
|
||||
return null;
|
||||
}
|
||||
@@ -168,15 +178,27 @@ export function isBrushSelected(selection: EditorSelection, brushId: string): bo
|
||||
);
|
||||
}
|
||||
|
||||
export function isBrushFaceSelected(selection: EditorSelection, brushId: string, faceId: BoxFaceId): boolean {
|
||||
export function isBrushFaceSelected(
|
||||
selection: EditorSelection,
|
||||
brushId: string,
|
||||
faceId: WhiteboxFaceId
|
||||
): boolean {
|
||||
return selection.kind === "brushFace" && selection.brushId === brushId && selection.faceId === faceId;
|
||||
}
|
||||
|
||||
export function isBrushEdgeSelected(selection: EditorSelection, brushId: string, edgeId: BoxEdgeId): boolean {
|
||||
export function isBrushEdgeSelected(
|
||||
selection: EditorSelection,
|
||||
brushId: string,
|
||||
edgeId: WhiteboxEdgeId
|
||||
): boolean {
|
||||
return selection.kind === "brushEdge" && selection.brushId === brushId && selection.edgeId === edgeId;
|
||||
}
|
||||
|
||||
export function isBrushVertexSelected(selection: EditorSelection, brushId: string, vertexId: BoxVertexId): boolean {
|
||||
export function isBrushVertexSelected(
|
||||
selection: EditorSelection,
|
||||
brushId: string,
|
||||
vertexId: WhiteboxVertexId
|
||||
): boolean {
|
||||
return selection.kind === "brushVertex" && selection.brushId === brushId && selection.vertexId === vertexId;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +1,56 @@
|
||||
import type { EditorSelection } from "./selection";
|
||||
import {
|
||||
BOX_EDGE_LABELS,
|
||||
BOX_FACE_LABELS,
|
||||
BOX_VERTEX_LABELS
|
||||
type Brush
|
||||
} from "../document/brushes";
|
||||
import type { SceneDocument } from "../document/scene-document";
|
||||
import {
|
||||
getBrushDefaultName,
|
||||
getBrushEdgeLabel,
|
||||
getBrushFaceLabel,
|
||||
getBrushKindLabel,
|
||||
getBrushVertexLabel
|
||||
} from "../geometry/whitebox-topology";
|
||||
|
||||
function getBrushDisplayLabel(document: SceneDocument, brushId: string): string {
|
||||
const brushes = Object.values(document.brushes);
|
||||
const brushIndex = brushes.findIndex((brush) => brush.id === brushId);
|
||||
|
||||
if (brushIndex === -1) {
|
||||
return "Whitebox Box";
|
||||
return "Whitebox Solid";
|
||||
}
|
||||
|
||||
return brushes[brushIndex].name ?? `Whitebox Box ${brushIndex + 1}`;
|
||||
return brushes[brushIndex].name ?? getBrushDefaultName(brushes[brushIndex], brushIndex);
|
||||
}
|
||||
|
||||
function getBrushOrNull(
|
||||
document: SceneDocument,
|
||||
brushId: string
|
||||
): Brush | null {
|
||||
return document.brushes[brushId] ?? null;
|
||||
}
|
||||
|
||||
export function getWhiteboxSelectionFeedbackLabel(document: SceneDocument, selection: EditorSelection): string | null {
|
||||
switch (selection.kind) {
|
||||
case "brushes":
|
||||
return selection.ids.length === 1 ? `Solid · ${getBrushDisplayLabel(document, selection.ids[0])}` : null;
|
||||
case "brushFace":
|
||||
return `Face · ${BOX_FACE_LABELS[selection.faceId]} · ${getBrushDisplayLabel(document, selection.brushId)}`;
|
||||
case "brushEdge":
|
||||
return `Edge · ${BOX_EDGE_LABELS[selection.edgeId]} · ${getBrushDisplayLabel(document, selection.brushId)}`;
|
||||
case "brushVertex":
|
||||
return `Vertex · ${BOX_VERTEX_LABELS[selection.vertexId]} · ${getBrushDisplayLabel(document, selection.brushId)}`;
|
||||
case "brushFace": {
|
||||
const brush = getBrushOrNull(document, selection.brushId);
|
||||
return brush === null
|
||||
? `Face · ${selection.faceId} · ${getBrushDisplayLabel(document, selection.brushId)}`
|
||||
: `Face · ${getBrushFaceLabel(brush, selection.faceId)} · ${getBrushDisplayLabel(document, selection.brushId)}`;
|
||||
}
|
||||
case "brushEdge": {
|
||||
const brush = getBrushOrNull(document, selection.brushId);
|
||||
return brush === null
|
||||
? `Edge · ${selection.edgeId} · ${getBrushDisplayLabel(document, selection.brushId)}`
|
||||
: `Edge · ${getBrushEdgeLabel(brush, selection.edgeId)} · ${getBrushDisplayLabel(document, selection.brushId)}`;
|
||||
}
|
||||
case "brushVertex": {
|
||||
const brush = getBrushOrNull(document, selection.brushId);
|
||||
return brush === null
|
||||
? `Vertex · ${selection.vertexId} · ${getBrushDisplayLabel(document, selection.brushId)}`
|
||||
: `Vertex · ${getBrushVertexLabel(brush, selection.vertexId)} · ${getBrushDisplayLabel(document, selection.brushId)}`;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -177,20 +177,24 @@ export interface BrushGeometry {
|
||||
vertices: Record<WhiteboxVertexId, Vec3>;
|
||||
}
|
||||
|
||||
export type BoxBrushFaces = Record<BoxFaceId, BrushFace>;
|
||||
export type BoxBrushGeometryVertices = Record<BoxVertexId, Vec3>;
|
||||
export type BoxBrushFaces = Record<WhiteboxFaceId, BrushFace> &
|
||||
Record<BoxFaceId, BrushFace>;
|
||||
export type BoxBrushGeometryVertices = Record<WhiteboxVertexId, Vec3> &
|
||||
Record<BoxVertexId, Vec3>;
|
||||
export interface BoxBrushGeometry extends BrushGeometry {
|
||||
vertices: BoxBrushGeometryVertices;
|
||||
}
|
||||
|
||||
export type WedgeBrushFaces = Record<WedgeFaceId, BrushFace>;
|
||||
export type WedgeBrushGeometryVertices = Record<WedgeVertexId, Vec3>;
|
||||
export type WedgeBrushFaces = Record<WhiteboxFaceId, BrushFace> &
|
||||
Record<WedgeFaceId, BrushFace>;
|
||||
export type WedgeBrushGeometryVertices = Record<WhiteboxVertexId, Vec3> &
|
||||
Record<WedgeVertexId, Vec3>;
|
||||
export interface WedgeBrushGeometry extends BrushGeometry {
|
||||
vertices: WedgeBrushGeometryVertices;
|
||||
}
|
||||
|
||||
export interface RadialPrismBrushGeometry extends BrushGeometry {
|
||||
vertices: Record<RadialPrismVertexId, Vec3>;
|
||||
vertices: Record<WhiteboxVertexId, Vec3> & Record<RadialPrismVertexId, Vec3>;
|
||||
}
|
||||
|
||||
interface BrushBase {
|
||||
@@ -223,7 +227,7 @@ export interface RadialPrismBrush extends BrushBase {
|
||||
kind: "radialPrism";
|
||||
sideCount: number;
|
||||
geometry: RadialPrismBrushGeometry;
|
||||
faces: Record<RadialPrismFaceId, BrushFace>;
|
||||
faces: Record<WhiteboxFaceId, BrushFace> & Record<RadialPrismFaceId, BrushFace>;
|
||||
volume: BoxBrushVolumeSettings;
|
||||
}
|
||||
|
||||
|
||||
659
src/geometry/box-brush-mesh.ts
Normal file
659
src/geometry/box-brush-mesh.ts
Normal file
@@ -0,0 +1,659 @@
|
||||
import { BufferAttribute, BufferGeometry } from "three";
|
||||
|
||||
import type { Vec2, Vec3 } from "../core/vector";
|
||||
import type {
|
||||
Brush,
|
||||
BoxBrush,
|
||||
BoxEdgeId,
|
||||
BoxFaceId,
|
||||
BoxVertexId,
|
||||
FaceUvState,
|
||||
WhiteboxEdgeId,
|
||||
WhiteboxFaceId,
|
||||
WhiteboxVertexId
|
||||
} from "../document/brushes";
|
||||
import { transformProjectedFaceUv } from "./box-face-uvs";
|
||||
import { getBrushFaceBasis, getBrushFaceNormal, getBrushLocalVertexPosition } from "./whitebox-brush";
|
||||
import {
|
||||
getBoxBrushEdgeVertexIds as getTopologyBoxBrushEdgeVertexIds,
|
||||
getBoxBrushFaceVertexIds as getTopologyBoxBrushFaceVertexIds,
|
||||
getBrushEdgeIds,
|
||||
getBrushEdgeVertexIds,
|
||||
getBrushFaceIds,
|
||||
getBrushFaceVertexIds,
|
||||
getBrushVertexIds
|
||||
} from "./whitebox-topology";
|
||||
|
||||
const WATER_TOP_FACE_RENDER_SEGMENTS = 12;
|
||||
|
||||
export interface BoxBrushGeometryDiagnostic {
|
||||
code: string;
|
||||
message: string;
|
||||
faceId?: WhiteboxFaceId;
|
||||
}
|
||||
|
||||
export interface DerivedBoxBrushFaceSurface {
|
||||
faceId: WhiteboxFaceId;
|
||||
vertexIds: readonly WhiteboxVertexId[];
|
||||
triangles: Array<readonly [number, number, number]>;
|
||||
normal: Vec3;
|
||||
}
|
||||
|
||||
export interface DerivedBoxBrushMeshData {
|
||||
geometry: BufferGeometry;
|
||||
faceIdsInOrder: WhiteboxFaceId[];
|
||||
faceSurfaces: DerivedBoxBrushFaceSurface[];
|
||||
edgeSegments: Array<{ edgeId: WhiteboxEdgeId; 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 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 triangulateFace(vertices: Vec3[]): Array<readonly [number, number, number]> {
|
||||
if (vertices.length < 3) {
|
||||
throw new Error("Face must contain at least three vertices.");
|
||||
}
|
||||
|
||||
if (vertices.length === 3) {
|
||||
return [[0, 1, 2]];
|
||||
}
|
||||
|
||||
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 = vertices.map((_, index) => index);
|
||||
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 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 projectBoxLocalVertexToFaceUv(
|
||||
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 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 getGenericFaceUvProjection(
|
||||
brush: Brush,
|
||||
faceId: WhiteboxFaceId,
|
||||
vertexPosition: Vec3
|
||||
): Vec2 {
|
||||
const basis = getBrushFaceBasis(brush, faceId);
|
||||
const relative = {
|
||||
x: vertexPosition.x - basis.origin.x,
|
||||
y: vertexPosition.y - basis.origin.y,
|
||||
z: vertexPosition.z - basis.origin.z
|
||||
};
|
||||
|
||||
return {
|
||||
x: dotVec3(relative, basis.uAxis),
|
||||
y: dotVec3(relative, basis.vAxis)
|
||||
};
|
||||
}
|
||||
|
||||
function getFaceUvProjection(
|
||||
brush: Brush,
|
||||
faceId: WhiteboxFaceId,
|
||||
vertexPosition: Vec3,
|
||||
faceBounds: { min: Vec3; max: Vec3 }
|
||||
): Vec2 {
|
||||
if (brush.kind === "box") {
|
||||
return projectBoxLocalVertexToFaceUv(vertexPosition, faceId as BoxFaceId, faceBounds);
|
||||
}
|
||||
|
||||
return getGenericFaceUvProjection(brush, faceId, vertexPosition);
|
||||
}
|
||||
|
||||
function computeProjectedFaceBounds(projectedUvs: Vec2[]): { min: Vec2; max: Vec2 } {
|
||||
const firstUv = projectedUvs[0];
|
||||
const min = { ...firstUv };
|
||||
const max = { ...firstUv };
|
||||
|
||||
for (const projectedUv of projectedUvs.slice(1)) {
|
||||
min.x = Math.min(min.x, projectedUv.x);
|
||||
min.y = Math.min(min.y, projectedUv.y);
|
||||
max.x = Math.max(max.x, projectedUv.x);
|
||||
max.y = Math.max(max.y, projectedUv.y);
|
||||
}
|
||||
|
||||
return {
|
||||
min,
|
||||
max
|
||||
};
|
||||
}
|
||||
|
||||
function pushRenderedFaceVertex(
|
||||
positions: number[],
|
||||
normals: number[],
|
||||
uvs: number[],
|
||||
faceUvs: number[],
|
||||
indices: number[],
|
||||
vertex: Vec3,
|
||||
normal: Vec3,
|
||||
projectedUv: Vec2,
|
||||
projectedBounds: { min: Vec2; max: Vec2 },
|
||||
uvSize: Vec2,
|
||||
uvState: FaceUvState
|
||||
) {
|
||||
const baseUv = {
|
||||
x: projectedUv.x - projectedBounds.min.x,
|
||||
y: projectedUv.y - projectedBounds.min.y
|
||||
};
|
||||
const transformedUv = transformProjectedFaceUv(baseUv, 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 : baseUv.x / uvSize.x,
|
||||
uvSize.y <= 1e-8 ? 0.5 : baseUv.y / uvSize.y
|
||||
);
|
||||
indices.push(indices.length);
|
||||
}
|
||||
|
||||
export function getBoxBrushFaceVertexIds(faceId: BoxFaceId): readonly [
|
||||
BoxVertexId,
|
||||
BoxVertexId,
|
||||
BoxVertexId,
|
||||
BoxVertexId
|
||||
] {
|
||||
return getTopologyBoxBrushFaceVertexIds(faceId);
|
||||
}
|
||||
|
||||
export function getBoxBrushEdgeVertexIds(edgeId: BoxEdgeId): readonly [
|
||||
BoxVertexId,
|
||||
BoxVertexId
|
||||
] {
|
||||
return getTopologyBoxBrushEdgeVertexIds(edgeId);
|
||||
}
|
||||
|
||||
export function getBoxBrushLocalVertexPosition(
|
||||
brush: Brush,
|
||||
vertexId: WhiteboxVertexId
|
||||
): Vec3 {
|
||||
return getBrushLocalVertexPosition(brush, vertexId);
|
||||
}
|
||||
|
||||
export function buildBoxBrushDerivedMeshData(brush: Brush): DerivedBoxBrushMeshData {
|
||||
const diagnostics = validateBoxBrushGeometry(brush);
|
||||
|
||||
if (diagnostics.length > 0) {
|
||||
throw new Error(diagnostics[0].message);
|
||||
}
|
||||
|
||||
const faceIds = getBrushFaceIds(brush);
|
||||
const edgeIds = getBrushEdgeIds(brush);
|
||||
const vertexIds = getBrushVertexIds(brush);
|
||||
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<WhiteboxVertexId, number>();
|
||||
|
||||
for (const vertexId of vertexIds) {
|
||||
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 faceIds.entries()) {
|
||||
const faceVertexIds = getBrushFaceVertexIds(brush, faceId);
|
||||
const faceVertices = faceVertexIds.map((vertexId) =>
|
||||
getBrushLocalVertexPosition(brush, vertexId)
|
||||
);
|
||||
const triangles = triangulateFace(faceVertices);
|
||||
const normal = getBrushFaceNormal(brush, faceId);
|
||||
const faceBounds = computeFaceBounds(faceVertices);
|
||||
const projectedUvs = faceVertices.map((vertex) =>
|
||||
getFaceUvProjection(brush, faceId, vertex, faceBounds)
|
||||
);
|
||||
const projectedBounds = computeProjectedFaceBounds(projectedUvs);
|
||||
const uvSize = {
|
||||
x: projectedBounds.max.x - projectedBounds.min.x,
|
||||
y: projectedBounds.max.y - projectedBounds.min.y
|
||||
};
|
||||
const uvState = brush.faces[faceId].uv as FaceUvState;
|
||||
const indexStart = indices.length;
|
||||
|
||||
faceSurfaces.push({
|
||||
faceId,
|
||||
vertexIds: faceVertexIds,
|
||||
triangles,
|
||||
normal
|
||||
});
|
||||
|
||||
const useSubdividedWaterTopFace =
|
||||
brush.kind === "box" &&
|
||||
brush.volume.mode === "water" &&
|
||||
faceId === "posY" &&
|
||||
brush.volume.water.surfaceDisplacementEnabled &&
|
||||
faceVertices.length === 4;
|
||||
|
||||
if (useSubdividedWaterTopFace) {
|
||||
const faceCorners = faceVertices as [Vec3, Vec3, Vec3, Vec3];
|
||||
const projectedCornerUvs = projectedUvs as [Vec2, Vec2, Vec2, Vec2];
|
||||
|
||||
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)
|
||||
];
|
||||
const quadProjectedUvs: [Vec2, Vec2, Vec2, Vec2] = [
|
||||
interpolateQuadSurfaceUv(projectedCornerUvs, u0, v0),
|
||||
interpolateQuadSurfaceUv(projectedCornerUvs, u1, v0),
|
||||
interpolateQuadSurfaceUv(projectedCornerUvs, u1, v1),
|
||||
interpolateQuadSurfaceUv(projectedCornerUvs, u0, v1)
|
||||
];
|
||||
|
||||
for (const [vertex, projectedUv] of [
|
||||
[quadVertices[0], quadProjectedUvs[0]],
|
||||
[quadVertices[1], quadProjectedUvs[1]],
|
||||
[quadVertices[2], quadProjectedUvs[2]],
|
||||
[quadVertices[0], quadProjectedUvs[0]],
|
||||
[quadVertices[2], quadProjectedUvs[2]],
|
||||
[quadVertices[3], quadProjectedUvs[3]]
|
||||
] as const) {
|
||||
pushRenderedFaceVertex(
|
||||
positions,
|
||||
normals,
|
||||
uvs,
|
||||
faceUvs,
|
||||
indices,
|
||||
vertex,
|
||||
normal,
|
||||
projectedUv,
|
||||
projectedBounds,
|
||||
uvSize,
|
||||
uvState
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const triangle of triangles) {
|
||||
for (const vertexOffset of triangle) {
|
||||
pushRenderedFaceVertex(
|
||||
positions,
|
||||
normals,
|
||||
uvs,
|
||||
faceUvs,
|
||||
indices,
|
||||
faceVertices[vertexOffset],
|
||||
normal,
|
||||
projectedUvs[vertexOffset],
|
||||
projectedBounds,
|
||||
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[vertexIds[0]];
|
||||
const localBounds = {
|
||||
min: cloneVec3(firstVertex),
|
||||
max: cloneVec3(firstVertex)
|
||||
};
|
||||
|
||||
for (const vertexId of vertexIds) {
|
||||
const vertex = brush.geometry.vertices[vertexId];
|
||||
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,
|
||||
faceIdsInOrder: faceIds,
|
||||
faceSurfaces,
|
||||
edgeSegments: edgeIds.map((edgeId) => {
|
||||
const [startId, endId] = getBrushEdgeVertexIds(brush, edgeId);
|
||||
return {
|
||||
edgeId,
|
||||
start: getBrushLocalVertexPosition(brush, startId),
|
||||
end: getBrushLocalVertexPosition(brush, endId)
|
||||
};
|
||||
}),
|
||||
colliderVertices: new Float32Array(colliderVertices),
|
||||
colliderIndices: new Uint32Array(colliderIndices),
|
||||
localBounds
|
||||
};
|
||||
}
|
||||
|
||||
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 lerpVec2(start: Vec2, end: Vec2, amount: number): Vec2 {
|
||||
return {
|
||||
x: lerpNumber(start.x, end.x, amount),
|
||||
y: lerpNumber(start.y, end.y, 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 interpolateQuadSurfaceUv(
|
||||
corners: readonly [Vec2, Vec2, Vec2, Vec2],
|
||||
u: number,
|
||||
v: number
|
||||
): Vec2 {
|
||||
const topEdge = lerpVec2(corners[0], corners[1], u);
|
||||
const bottomEdge = lerpVec2(corners[3], corners[2], u);
|
||||
|
||||
return lerpVec2(topEdge, bottomEdge, v);
|
||||
}
|
||||
|
||||
export function validateBoxBrushGeometry(brush: Brush): BoxBrushGeometryDiagnostic[] {
|
||||
const diagnostics: BoxBrushGeometryDiagnostic[] = [];
|
||||
|
||||
for (const vertexId of getBrushVertexIds(brush)) {
|
||||
const vertex = brush.geometry.vertices[vertexId];
|
||||
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.`,
|
||||
faceId: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const faceId of getBrushFaceIds(brush)) {
|
||||
const faceVertices = getBrushFaceVertexIds(brush, 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 {
|
||||
triangulateFace(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;
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Euler, MathUtils, Vector3 } from "three";
|
||||
|
||||
import type { Vec3 } from "../core/vector";
|
||||
import { BOX_VERTEX_IDS, type BoxBrush } from "../document/brushes";
|
||||
import type { Brush } from "../document/brushes";
|
||||
import { getBoxBrushLocalVertexPosition } from "./box-brush-mesh";
|
||||
import { getBrushVertexIds } from "./whitebox-topology";
|
||||
|
||||
export interface BoxBrushBounds {
|
||||
min: Vec3;
|
||||
max: Vec3;
|
||||
}
|
||||
|
||||
export function getBoxBrushHalfSize(brush: BoxBrush): Vec3 {
|
||||
export function getBoxBrushHalfSize(brush: Brush): Vec3 {
|
||||
return {
|
||||
x: brush.size.x * 0.5,
|
||||
y: brush.size.y * 0.5,
|
||||
@@ -17,7 +18,7 @@ export function getBoxBrushHalfSize(brush: BoxBrush): Vec3 {
|
||||
};
|
||||
}
|
||||
|
||||
export function getBoxBrushBounds(brush: BoxBrush): BoxBrushBounds {
|
||||
export function getBoxBrushBounds(brush: Brush): BoxBrushBounds {
|
||||
const corners = getBoxBrushCornerPositions(brush);
|
||||
const firstCorner = corners[0];
|
||||
const min = { ...firstCorner };
|
||||
@@ -38,14 +39,14 @@ export function getBoxBrushBounds(brush: BoxBrush): BoxBrushBounds {
|
||||
};
|
||||
}
|
||||
|
||||
export function getBoxBrushCornerPositions(brush: BoxBrush): Vec3[] {
|
||||
export function getBoxBrushCornerPositions(brush: Brush): Vec3[] {
|
||||
const rotation = new Euler(
|
||||
MathUtils.degToRad(brush.rotationDegrees.x),
|
||||
MathUtils.degToRad(brush.rotationDegrees.y),
|
||||
MathUtils.degToRad(brush.rotationDegrees.z),
|
||||
"XYZ"
|
||||
);
|
||||
const offsets = BOX_VERTEX_IDS.map((vertexId) => {
|
||||
const offsets = getBrushVertexIds(brush).map((vertexId) => {
|
||||
const localVertex = getBoxBrushLocalVertexPosition(brush, vertexId);
|
||||
return new Vector3(localVertex.x, localVertex.y, localVertex.z);
|
||||
});
|
||||
|
||||
@@ -1,14 +1,72 @@
|
||||
import { BoxGeometry } from "three";
|
||||
|
||||
import type { Vec2, Vec3 } from "../core/vector";
|
||||
import { BOX_FACE_IDS, createDefaultFaceUvState, type BoxBrush, type BoxFaceId, type FaceUvState } from "../document/brushes";
|
||||
import {
|
||||
BOX_FACE_IDS,
|
||||
createDefaultFaceUvState,
|
||||
type Brush,
|
||||
type BoxFaceId,
|
||||
type FaceUvState,
|
||||
type WhiteboxFaceId
|
||||
} from "../document/brushes";
|
||||
import { getBrushFaceBasis, getBrushLocalVertexPosition } from "./whitebox-brush";
|
||||
import { getBrushFaceVertexIds } from "./whitebox-topology";
|
||||
|
||||
interface BoxBrushUvProjectionSource {
|
||||
size: Vec3;
|
||||
faces: Record<BoxFaceId, { uv: FaceUvState }>;
|
||||
}
|
||||
|
||||
export function getBoxBrushFaceSize(brush: BoxBrushUvProjectionSource, faceId: BoxFaceId): Vec2 {
|
||||
function computeGenericBrushFaceSize(brush: Brush, faceId: WhiteboxFaceId): Vec2 {
|
||||
const basis = getBrushFaceBasis(brush, faceId);
|
||||
const projectedVertices = getBrushFaceVertexIds(brush, faceId).map(
|
||||
(vertexId) => {
|
||||
const vertex = getBrushLocalVertexPosition(brush, vertexId);
|
||||
const relative = {
|
||||
x: vertex.x - basis.origin.x,
|
||||
y: vertex.y - basis.origin.y,
|
||||
z: vertex.z - basis.origin.z
|
||||
};
|
||||
|
||||
return {
|
||||
x:
|
||||
relative.x * basis.uAxis.x +
|
||||
relative.y * basis.uAxis.y +
|
||||
relative.z * basis.uAxis.z,
|
||||
y:
|
||||
relative.x * basis.vAxis.x +
|
||||
relative.y * basis.vAxis.y +
|
||||
relative.z * basis.vAxis.z
|
||||
};
|
||||
}
|
||||
);
|
||||
const firstVertex = projectedVertices[0];
|
||||
const min = { ...firstVertex };
|
||||
const max = { ...firstVertex };
|
||||
|
||||
for (const projectedVertex of projectedVertices.slice(1)) {
|
||||
min.x = Math.min(min.x, projectedVertex.x);
|
||||
min.y = Math.min(min.y, projectedVertex.y);
|
||||
max.x = Math.max(max.x, projectedVertex.x);
|
||||
max.y = Math.max(max.y, projectedVertex.y);
|
||||
}
|
||||
|
||||
return {
|
||||
x: max.x - min.x,
|
||||
y: max.y - min.y
|
||||
};
|
||||
}
|
||||
|
||||
export function getBoxBrushFaceSize(
|
||||
brush: BoxBrushUvProjectionSource | Brush,
|
||||
faceId: BoxFaceId | WhiteboxFaceId
|
||||
): Vec2 {
|
||||
if ("kind" in brush) {
|
||||
if (brush.kind !== "box") {
|
||||
return computeGenericBrushFaceSize(brush, faceId);
|
||||
}
|
||||
}
|
||||
|
||||
switch (faceId) {
|
||||
case "posX":
|
||||
case "negX":
|
||||
@@ -29,9 +87,18 @@ export function getBoxBrushFaceSize(brush: BoxBrushUvProjectionSource, faceId: B
|
||||
y: brush.size.y
|
||||
};
|
||||
}
|
||||
|
||||
if ("kind" in brush) {
|
||||
return computeGenericBrushFaceSize(brush, faceId);
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported box face id ${faceId}.`);
|
||||
}
|
||||
|
||||
export function createFitToFaceBoxBrushFaceUvState(brush: BoxBrush, faceId: BoxFaceId): FaceUvState {
|
||||
export function createFitToFaceBoxBrushFaceUvState(
|
||||
brush: Brush,
|
||||
faceId: WhiteboxFaceId
|
||||
): FaceUvState {
|
||||
const faceSize = getBoxBrushFaceSize(brush, faceId);
|
||||
|
||||
return {
|
||||
@@ -44,8 +111,8 @@ export function createFitToFaceBoxBrushFaceUvState(brush: BoxBrush, faceId: BoxF
|
||||
}
|
||||
|
||||
export function createFitToMaterialTileBoxBrushFaceUvState(
|
||||
brush: BoxBrush,
|
||||
faceId: BoxFaceId,
|
||||
brush: Brush,
|
||||
faceId: WhiteboxFaceId,
|
||||
tileSize: Vec2
|
||||
): FaceUvState {
|
||||
const faceSize = getBoxBrushFaceSize(brush, faceId);
|
||||
|
||||
Reference in New Issue
Block a user