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 { cloneEditorSelection, type EditorSelection } from "../core/selection";
|
||||||
import {
|
import {
|
||||||
|
cloneBrush,
|
||||||
|
cloneBrushGeometry,
|
||||||
cloneFaceUvState,
|
cloneFaceUvState,
|
||||||
type BoxBrush,
|
type Brush,
|
||||||
type BoxEdgeId,
|
|
||||||
type BoxFaceId,
|
|
||||||
type BoxVertexId,
|
|
||||||
type BrushFace
|
type BrushFace
|
||||||
} from "../document/brushes";
|
} from "../document/brushes";
|
||||||
import type { SceneDocument } from "../document/scene-document";
|
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];
|
const brush = document.brushes[brushId];
|
||||||
|
|
||||||
if (brush === undefined) {
|
if (brush === undefined) {
|
||||||
throw new Error(`Box brush ${brushId} does not exist.`);
|
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;
|
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 {
|
return {
|
||||||
kind: "brushFace",
|
kind: "brushFace",
|
||||||
brushId,
|
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 {
|
return {
|
||||||
kind: "brushEdge",
|
kind: "brushEdge",
|
||||||
brushId,
|
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 {
|
return {
|
||||||
kind: "brushVertex",
|
kind: "brushVertex",
|
||||||
brushId,
|
brushId,
|
||||||
@@ -58,12 +58,12 @@ export function cloneSelectionForCommand(selection: EditorSelection): EditorSele
|
|||||||
return cloneEditorSelection(selection);
|
return cloneEditorSelection(selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function replaceBrush(document: SceneDocument, brush: BoxBrush): SceneDocument {
|
export function replaceBrush(document: SceneDocument, brush: Brush): SceneDocument {
|
||||||
return {
|
return {
|
||||||
...document,
|
...document,
|
||||||
brushes: {
|
brushes: {
|
||||||
...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 brush = getBoxBrushOrThrow(document, brushId);
|
||||||
const face = brush.faces[faceId];
|
const face = brush.faces[faceId];
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ export function getBoxBrushFaceOrThrow(document: SceneDocument, brushId: string,
|
|||||||
return face;
|
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);
|
const brush = getBoxBrushOrThrow(document, brushId);
|
||||||
|
|
||||||
return replaceBrush(document, {
|
return replaceBrush(document, {
|
||||||
@@ -102,6 +102,7 @@ export function replaceBoxBrushFace(document: SceneDocument, brushId: string, fa
|
|||||||
materialId: face.materialId,
|
materialId: face.materialId,
|
||||||
uv: cloneFaceUvState(face.uv)
|
uv: cloneFaceUvState(face.uv)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
geometry: cloneBrushGeometry(brush.geometry)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { ToolMode } from "../core/tool-mode";
|
import type { ToolMode } from "../core/tool-mode";
|
||||||
import { createOpaqueId } from "../core/ids";
|
import { createOpaqueId } from "../core/ids";
|
||||||
import type { EditorSelection } from "../core/selection";
|
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 {
|
import {
|
||||||
cloneSelectionForCommand,
|
cloneSelectionForCommand,
|
||||||
@@ -18,7 +19,7 @@ interface SetBoxBrushAllFaceMaterialsCommandOptions {
|
|||||||
export function createSetBoxBrushAllFaceMaterialsCommand(
|
export function createSetBoxBrushAllFaceMaterialsCommand(
|
||||||
options: SetBoxBrushAllFaceMaterialsCommandOptions
|
options: SetBoxBrushAllFaceMaterialsCommandOptions
|
||||||
): EditorCommand {
|
): EditorCommand {
|
||||||
let previousMaterialIds: Record<BoxFaceId, string | null> | null = null;
|
let previousMaterialIds: Record<WhiteboxFaceId, string | null> | null = null;
|
||||||
let previousSelection: EditorSelection | null = null;
|
let previousSelection: EditorSelection | null = null;
|
||||||
let previousToolMode: ToolMode | null = null;
|
let previousToolMode: ToolMode | null = null;
|
||||||
|
|
||||||
@@ -42,14 +43,12 @@ export function createSetBoxBrushAllFaceMaterialsCommand(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (previousMaterialIds === null) {
|
if (previousMaterialIds === null) {
|
||||||
previousMaterialIds = {
|
previousMaterialIds = Object.fromEntries(
|
||||||
posX: currentBrush.faces.posX.materialId,
|
getBrushFaceIds(currentBrush).map((faceId) => [
|
||||||
negX: currentBrush.faces.negX.materialId,
|
faceId,
|
||||||
posY: currentBrush.faces.posY.materialId,
|
currentBrush.faces[faceId].materialId
|
||||||
negY: currentBrush.faces.negY.materialId,
|
])
|
||||||
posZ: currentBrush.faces.posZ.materialId,
|
);
|
||||||
negZ: currentBrush.faces.negZ.materialId
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (previousSelection === null) {
|
if (previousSelection === null) {
|
||||||
@@ -64,7 +63,7 @@ export function createSetBoxBrushAllFaceMaterialsCommand(
|
|||||||
replaceBrush(currentDocument, {
|
replaceBrush(currentDocument, {
|
||||||
...currentBrush,
|
...currentBrush,
|
||||||
faces: Object.fromEntries(
|
faces: Object.fromEntries(
|
||||||
BOX_FACE_IDS.map((faceId) => [
|
getBrushFaceIds(currentBrush).map((faceId) => [
|
||||||
faceId,
|
faceId,
|
||||||
{
|
{
|
||||||
...currentBrush.faces[faceId],
|
...currentBrush.faces[faceId],
|
||||||
@@ -87,32 +86,15 @@ export function createSetBoxBrushAllFaceMaterialsCommand(
|
|||||||
context.setDocument(
|
context.setDocument(
|
||||||
replaceBrush(currentDocument, {
|
replaceBrush(currentDocument, {
|
||||||
...currentBrush,
|
...currentBrush,
|
||||||
faces: {
|
faces: Object.fromEntries(
|
||||||
posX: {
|
getBrushFaceIds(currentBrush).map((faceId) => [
|
||||||
...currentBrush.faces.posX,
|
faceId,
|
||||||
materialId: previousMaterialIds.posX
|
{
|
||||||
},
|
...currentBrush.faces[faceId],
|
||||||
negX: {
|
materialId: previousMaterialIds[faceId] ?? null
|
||||||
...currentBrush.faces.negX,
|
}
|
||||||
materialId: previousMaterialIds.negX
|
])
|
||||||
},
|
) as typeof currentBrush.faces
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ToolMode } from "../core/tool-mode";
|
import type { ToolMode } from "../core/tool-mode";
|
||||||
import { createOpaqueId } from "../core/ids";
|
import { createOpaqueId } from "../core/ids";
|
||||||
import type { EditorSelection } from "../core/selection";
|
import type { EditorSelection } from "../core/selection";
|
||||||
import type { BoxFaceId } from "../document/brushes";
|
import type { WhiteboxFaceId } from "../document/brushes";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
cloneSelectionForCommand,
|
cloneSelectionForCommand,
|
||||||
@@ -13,7 +13,7 @@ import type { EditorCommand } from "./command";
|
|||||||
|
|
||||||
interface SetBoxBrushFaceMaterialCommandOptions {
|
interface SetBoxBrushFaceMaterialCommandOptions {
|
||||||
brushId: string;
|
brushId: string;
|
||||||
faceId: BoxFaceId;
|
faceId: WhiteboxFaceId;
|
||||||
materialId: string | null;
|
materialId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import type { ToolMode } from "../core/tool-mode";
|
import type { ToolMode } from "../core/tool-mode";
|
||||||
import { createOpaqueId } from "../core/ids";
|
import { createOpaqueId } from "../core/ids";
|
||||||
import type { EditorSelection } from "../core/selection";
|
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 {
|
import {
|
||||||
cloneSelectionForCommand,
|
cloneSelectionForCommand,
|
||||||
@@ -13,7 +17,7 @@ import type { EditorCommand } from "./command";
|
|||||||
|
|
||||||
interface SetBoxBrushFaceUvStateCommandOptions {
|
interface SetBoxBrushFaceUvStateCommandOptions {
|
||||||
brushId: string;
|
brushId: string;
|
||||||
faceId: BoxFaceId;
|
faceId: WhiteboxFaceId;
|
||||||
uvState: FaceUvState;
|
uvState: FaceUvState;
|
||||||
label?: string;
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import type { ToolMode } from "../core/tool-mode";
|
|||||||
import { createOpaqueId } from "../core/ids";
|
import { createOpaqueId } from "../core/ids";
|
||||||
import type { EditorSelection } from "../core/selection";
|
import type { EditorSelection } from "../core/selection";
|
||||||
import {
|
import {
|
||||||
BOX_FACE_IDS,
|
|
||||||
cloneFaceUvState,
|
cloneFaceUvState,
|
||||||
type BoxFaceId,
|
type FaceUvState,
|
||||||
type FaceUvState
|
type WhiteboxFaceId
|
||||||
} from "../document/brushes";
|
} from "../document/brushes";
|
||||||
|
import { getBrushFaceIds } from "../geometry/whitebox-topology";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
cloneSelectionForCommand,
|
cloneSelectionForCommand,
|
||||||
@@ -18,13 +18,13 @@ import type { EditorCommand } from "./command";
|
|||||||
interface UpdateBoxBrushAllFaceUvsCommandOptions {
|
interface UpdateBoxBrushAllFaceUvsCommandOptions {
|
||||||
brushId: string;
|
brushId: string;
|
||||||
label: string;
|
label: string;
|
||||||
updateUvState(uvState: FaceUvState, faceId: BoxFaceId): FaceUvState;
|
updateUvState(uvState: FaceUvState, faceId: WhiteboxFaceId): FaceUvState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createUpdateBoxBrushAllFaceUvsCommand(
|
export function createUpdateBoxBrushAllFaceUvsCommand(
|
||||||
options: UpdateBoxBrushAllFaceUvsCommandOptions
|
options: UpdateBoxBrushAllFaceUvsCommandOptions
|
||||||
): EditorCommand {
|
): EditorCommand {
|
||||||
let previousUvStates: Record<BoxFaceId, FaceUvState> | null = null;
|
let previousUvStates: Record<WhiteboxFaceId, FaceUvState> | null = null;
|
||||||
let previousSelection: EditorSelection | null = null;
|
let previousSelection: EditorSelection | null = null;
|
||||||
let previousToolMode: ToolMode | null = null;
|
let previousToolMode: ToolMode | null = null;
|
||||||
|
|
||||||
@@ -36,14 +36,12 @@ export function createUpdateBoxBrushAllFaceUvsCommand(
|
|||||||
const currentBrush = getBoxBrushOrThrow(currentDocument, options.brushId);
|
const currentBrush = getBoxBrushOrThrow(currentDocument, options.brushId);
|
||||||
|
|
||||||
if (previousUvStates === null) {
|
if (previousUvStates === null) {
|
||||||
previousUvStates = {
|
previousUvStates = Object.fromEntries(
|
||||||
posX: cloneFaceUvState(currentBrush.faces.posX.uv),
|
getBrushFaceIds(currentBrush).map((faceId) => [
|
||||||
negX: cloneFaceUvState(currentBrush.faces.negX.uv),
|
faceId,
|
||||||
posY: cloneFaceUvState(currentBrush.faces.posY.uv),
|
cloneFaceUvState(currentBrush.faces[faceId].uv)
|
||||||
negY: cloneFaceUvState(currentBrush.faces.negY.uv),
|
])
|
||||||
posZ: cloneFaceUvState(currentBrush.faces.posZ.uv),
|
);
|
||||||
negZ: cloneFaceUvState(currentBrush.faces.negZ.uv)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (previousSelection === null) {
|
if (previousSelection === null) {
|
||||||
@@ -58,7 +56,7 @@ export function createUpdateBoxBrushAllFaceUvsCommand(
|
|||||||
replaceBrush(currentDocument, {
|
replaceBrush(currentDocument, {
|
||||||
...currentBrush,
|
...currentBrush,
|
||||||
faces: Object.fromEntries(
|
faces: Object.fromEntries(
|
||||||
BOX_FACE_IDS.map((faceId) => [
|
getBrushFaceIds(currentBrush).map((faceId) => [
|
||||||
faceId,
|
faceId,
|
||||||
{
|
{
|
||||||
...currentBrush.faces[faceId],
|
...currentBrush.faces[faceId],
|
||||||
@@ -83,32 +81,15 @@ export function createUpdateBoxBrushAllFaceUvsCommand(
|
|||||||
context.setDocument(
|
context.setDocument(
|
||||||
replaceBrush(currentDocument, {
|
replaceBrush(currentDocument, {
|
||||||
...currentBrush,
|
...currentBrush,
|
||||||
faces: {
|
faces: Object.fromEntries(
|
||||||
posX: {
|
getBrushFaceIds(currentBrush).map((faceId) => [
|
||||||
...currentBrush.faces.posX,
|
faceId,
|
||||||
uv: cloneFaceUvState(previousUvStates.posX)
|
{
|
||||||
},
|
...currentBrush.faces[faceId],
|
||||||
negX: {
|
uv: cloneFaceUvState(previousUvStates[faceId])
|
||||||
...currentBrush.faces.negX,
|
}
|
||||||
uv: cloneFaceUvState(previousUvStates.negX)
|
])
|
||||||
},
|
) as typeof currentBrush.faces
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import type { WhiteboxSelectionMode } from "./whitebox-selection-mode";
|
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 =
|
export type EditorSelection =
|
||||||
| { kind: "none" }
|
| { kind: "none" }
|
||||||
| { kind: "brushes"; ids: string[] }
|
| { kind: "brushes"; ids: string[] }
|
||||||
| { kind: "brushFace"; brushId: string; faceId: BoxFaceId }
|
| { kind: "brushFace"; brushId: string; faceId: WhiteboxFaceId }
|
||||||
| { kind: "brushEdge"; brushId: string; edgeId: BoxEdgeId }
|
| { kind: "brushEdge"; brushId: string; edgeId: WhiteboxEdgeId }
|
||||||
| { kind: "brushVertex"; brushId: string; vertexId: BoxVertexId }
|
| { kind: "brushVertex"; brushId: string; vertexId: WhiteboxVertexId }
|
||||||
| { kind: "paths"; ids: string[] }
|
| { kind: "paths"; ids: string[] }
|
||||||
| { kind: "pathPoint"; pathId: string; pointId: string }
|
| { kind: "pathPoint"; pathId: string; pointId: string }
|
||||||
| { kind: "entities"; ids: string[] }
|
| { kind: "entities"; ids: string[] }
|
||||||
@@ -93,7 +97,9 @@ export function getSingleSelectedBrushId(selection: EditorSelection): string | n
|
|||||||
return selection.ids[0];
|
return selection.ids[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSelectedBrushFaceId(selection: EditorSelection): BoxFaceId | null {
|
export function getSelectedBrushFaceId(
|
||||||
|
selection: EditorSelection
|
||||||
|
): WhiteboxFaceId | null {
|
||||||
if (selection.kind !== "brushFace") {
|
if (selection.kind !== "brushFace") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -101,7 +107,9 @@ export function getSelectedBrushFaceId(selection: EditorSelection): BoxFaceId |
|
|||||||
return selection.faceId;
|
return selection.faceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSelectedBrushEdgeId(selection: EditorSelection): BoxEdgeId | null {
|
export function getSelectedBrushEdgeId(
|
||||||
|
selection: EditorSelection
|
||||||
|
): WhiteboxEdgeId | null {
|
||||||
if (selection.kind !== "brushEdge") {
|
if (selection.kind !== "brushEdge") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -109,7 +117,9 @@ export function getSelectedBrushEdgeId(selection: EditorSelection): BoxEdgeId |
|
|||||||
return selection.edgeId;
|
return selection.edgeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSelectedBrushVertexId(selection: EditorSelection): BoxVertexId | null {
|
export function getSelectedBrushVertexId(
|
||||||
|
selection: EditorSelection
|
||||||
|
): WhiteboxVertexId | null {
|
||||||
if (selection.kind !== "brushVertex") {
|
if (selection.kind !== "brushVertex") {
|
||||||
return null;
|
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;
|
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;
|
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;
|
return selection.kind === "brushVertex" && selection.brushId === brushId && selection.vertexId === vertexId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,56 @@
|
|||||||
import type { EditorSelection } from "./selection";
|
import type { EditorSelection } from "./selection";
|
||||||
import {
|
import {
|
||||||
BOX_EDGE_LABELS,
|
type Brush
|
||||||
BOX_FACE_LABELS,
|
|
||||||
BOX_VERTEX_LABELS
|
|
||||||
} from "../document/brushes";
|
} from "../document/brushes";
|
||||||
import type { SceneDocument } from "../document/scene-document";
|
import type { SceneDocument } from "../document/scene-document";
|
||||||
|
import {
|
||||||
|
getBrushDefaultName,
|
||||||
|
getBrushEdgeLabel,
|
||||||
|
getBrushFaceLabel,
|
||||||
|
getBrushKindLabel,
|
||||||
|
getBrushVertexLabel
|
||||||
|
} from "../geometry/whitebox-topology";
|
||||||
|
|
||||||
function getBrushDisplayLabel(document: SceneDocument, brushId: string): string {
|
function getBrushDisplayLabel(document: SceneDocument, brushId: string): string {
|
||||||
const brushes = Object.values(document.brushes);
|
const brushes = Object.values(document.brushes);
|
||||||
const brushIndex = brushes.findIndex((brush) => brush.id === brushId);
|
const brushIndex = brushes.findIndex((brush) => brush.id === brushId);
|
||||||
|
|
||||||
if (brushIndex === -1) {
|
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 {
|
export function getWhiteboxSelectionFeedbackLabel(document: SceneDocument, selection: EditorSelection): string | null {
|
||||||
switch (selection.kind) {
|
switch (selection.kind) {
|
||||||
case "brushes":
|
case "brushes":
|
||||||
return selection.ids.length === 1 ? `Solid · ${getBrushDisplayLabel(document, selection.ids[0])}` : null;
|
return selection.ids.length === 1 ? `Solid · ${getBrushDisplayLabel(document, selection.ids[0])}` : null;
|
||||||
case "brushFace":
|
case "brushFace": {
|
||||||
return `Face · ${BOX_FACE_LABELS[selection.faceId]} · ${getBrushDisplayLabel(document, selection.brushId)}`;
|
const brush = getBrushOrNull(document, selection.brushId);
|
||||||
case "brushEdge":
|
return brush === null
|
||||||
return `Edge · ${BOX_EDGE_LABELS[selection.edgeId]} · ${getBrushDisplayLabel(document, selection.brushId)}`;
|
? `Face · ${selection.faceId} · ${getBrushDisplayLabel(document, selection.brushId)}`
|
||||||
case "brushVertex":
|
: `Face · ${getBrushFaceLabel(brush, selection.faceId)} · ${getBrushDisplayLabel(document, selection.brushId)}`;
|
||||||
return `Vertex · ${BOX_VERTEX_LABELS[selection.vertexId]} · ${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:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,20 +177,24 @@ export interface BrushGeometry {
|
|||||||
vertices: Record<WhiteboxVertexId, Vec3>;
|
vertices: Record<WhiteboxVertexId, Vec3>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BoxBrushFaces = Record<BoxFaceId, BrushFace>;
|
export type BoxBrushFaces = Record<WhiteboxFaceId, BrushFace> &
|
||||||
export type BoxBrushGeometryVertices = Record<BoxVertexId, Vec3>;
|
Record<BoxFaceId, BrushFace>;
|
||||||
|
export type BoxBrushGeometryVertices = Record<WhiteboxVertexId, Vec3> &
|
||||||
|
Record<BoxVertexId, Vec3>;
|
||||||
export interface BoxBrushGeometry extends BrushGeometry {
|
export interface BoxBrushGeometry extends BrushGeometry {
|
||||||
vertices: BoxBrushGeometryVertices;
|
vertices: BoxBrushGeometryVertices;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WedgeBrushFaces = Record<WedgeFaceId, BrushFace>;
|
export type WedgeBrushFaces = Record<WhiteboxFaceId, BrushFace> &
|
||||||
export type WedgeBrushGeometryVertices = Record<WedgeVertexId, Vec3>;
|
Record<WedgeFaceId, BrushFace>;
|
||||||
|
export type WedgeBrushGeometryVertices = Record<WhiteboxVertexId, Vec3> &
|
||||||
|
Record<WedgeVertexId, Vec3>;
|
||||||
export interface WedgeBrushGeometry extends BrushGeometry {
|
export interface WedgeBrushGeometry extends BrushGeometry {
|
||||||
vertices: WedgeBrushGeometryVertices;
|
vertices: WedgeBrushGeometryVertices;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RadialPrismBrushGeometry extends BrushGeometry {
|
export interface RadialPrismBrushGeometry extends BrushGeometry {
|
||||||
vertices: Record<RadialPrismVertexId, Vec3>;
|
vertices: Record<WhiteboxVertexId, Vec3> & Record<RadialPrismVertexId, Vec3>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BrushBase {
|
interface BrushBase {
|
||||||
@@ -223,7 +227,7 @@ export interface RadialPrismBrush extends BrushBase {
|
|||||||
kind: "radialPrism";
|
kind: "radialPrism";
|
||||||
sideCount: number;
|
sideCount: number;
|
||||||
geometry: RadialPrismBrushGeometry;
|
geometry: RadialPrismBrushGeometry;
|
||||||
faces: Record<RadialPrismFaceId, BrushFace>;
|
faces: Record<WhiteboxFaceId, BrushFace> & Record<RadialPrismFaceId, BrushFace>;
|
||||||
volume: BoxBrushVolumeSettings;
|
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 { Euler, MathUtils, Vector3 } from "three";
|
||||||
|
|
||||||
import type { Vec3 } from "../core/vector";
|
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 { getBoxBrushLocalVertexPosition } from "./box-brush-mesh";
|
||||||
|
import { getBrushVertexIds } from "./whitebox-topology";
|
||||||
|
|
||||||
export interface BoxBrushBounds {
|
export interface BoxBrushBounds {
|
||||||
min: Vec3;
|
min: Vec3;
|
||||||
max: Vec3;
|
max: Vec3;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBoxBrushHalfSize(brush: BoxBrush): Vec3 {
|
export function getBoxBrushHalfSize(brush: Brush): Vec3 {
|
||||||
return {
|
return {
|
||||||
x: brush.size.x * 0.5,
|
x: brush.size.x * 0.5,
|
||||||
y: brush.size.y * 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 corners = getBoxBrushCornerPositions(brush);
|
||||||
const firstCorner = corners[0];
|
const firstCorner = corners[0];
|
||||||
const min = { ...firstCorner };
|
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(
|
const rotation = new Euler(
|
||||||
MathUtils.degToRad(brush.rotationDegrees.x),
|
MathUtils.degToRad(brush.rotationDegrees.x),
|
||||||
MathUtils.degToRad(brush.rotationDegrees.y),
|
MathUtils.degToRad(brush.rotationDegrees.y),
|
||||||
MathUtils.degToRad(brush.rotationDegrees.z),
|
MathUtils.degToRad(brush.rotationDegrees.z),
|
||||||
"XYZ"
|
"XYZ"
|
||||||
);
|
);
|
||||||
const offsets = BOX_VERTEX_IDS.map((vertexId) => {
|
const offsets = getBrushVertexIds(brush).map((vertexId) => {
|
||||||
const localVertex = getBoxBrushLocalVertexPosition(brush, vertexId);
|
const localVertex = getBoxBrushLocalVertexPosition(brush, vertexId);
|
||||||
return new Vector3(localVertex.x, localVertex.y, localVertex.z);
|
return new Vector3(localVertex.x, localVertex.y, localVertex.z);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,72 @@
|
|||||||
import { BoxGeometry } from "three";
|
import { BoxGeometry } from "three";
|
||||||
|
|
||||||
import type { Vec2, Vec3 } from "../core/vector";
|
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 {
|
interface BoxBrushUvProjectionSource {
|
||||||
size: Vec3;
|
size: Vec3;
|
||||||
faces: Record<BoxFaceId, { uv: FaceUvState }>;
|
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) {
|
switch (faceId) {
|
||||||
case "posX":
|
case "posX":
|
||||||
case "negX":
|
case "negX":
|
||||||
@@ -29,9 +87,18 @@ export function getBoxBrushFaceSize(brush: BoxBrushUvProjectionSource, faceId: B
|
|||||||
y: brush.size.y
|
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);
|
const faceSize = getBoxBrushFaceSize(brush, faceId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -44,8 +111,8 @@ export function createFitToFaceBoxBrushFaceUvState(brush: BoxBrush, faceId: BoxF
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createFitToMaterialTileBoxBrushFaceUvState(
|
export function createFitToMaterialTileBoxBrushFaceUvState(
|
||||||
brush: BoxBrush,
|
brush: Brush,
|
||||||
faceId: BoxFaceId,
|
faceId: WhiteboxFaceId,
|
||||||
tileSize: Vec2
|
tileSize: Vec2
|
||||||
): FaceUvState {
|
): FaceUvState {
|
||||||
const faceSize = getBoxBrushFaceSize(brush, faceId);
|
const faceSize = getBoxBrushFaceSize(brush, faceId);
|
||||||
|
|||||||
Reference in New Issue
Block a user