diff --git a/src/app/App.tsx b/src/app/App.tsx index e216e025..acf70c07 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -28,6 +28,7 @@ import { createImportAudioAssetCommand } from "../commands/import-audio-asset-co import { createImportBackgroundImageAssetCommand } from "../commands/import-background-image-asset-command"; import { createImportModelAssetCommand } from "../commands/import-model-asset-command"; import { createAddPathPointCommand } from "../commands/add-path-point-command"; +import { createApplySplineRoadToTerrainCommand } from "../commands/apply-spline-road-to-terrain-command"; import { createDeletePathCommand } from "../commands/delete-path-command"; import { createDeletePathPointCommand } from "../commands/delete-path-point-command"; import { createDeletePathPointsCommand } from "../commands/delete-path-points-command"; @@ -9740,6 +9741,24 @@ export function App({ } }; + const handleApplyPathRoadToTerrain = () => { + if (selectedPath === null) { + setStatusMessage("Select a path before applying its road to terrain."); + return; + } + + try { + store.executeCommand( + createApplySplineRoadToTerrainCommand({ + pathId: selectedPath.id + }) + ); + setStatusMessage("Applied Path road to terrain."); + } catch (error) { + setStatusMessage(getErrorMessage(error)); + } + }; + const handlePathPointDraftChange = ( pointIndex: number, axis: keyof Vec3Draft, @@ -22147,10 +22166,23 @@ export function App({ ))} +
+ +
Road preview draws a temporary corridor along the resolved - spline. Width affects the visible strip; shoulder and - falloff are stored for the terrain-apply command next. + spline. Applying it patches terrain heights, terrain paint + weights when the material already exists on a terrain + layer, and foliage blocker masks. It does not generate a + road mesh yet.
diff --git a/src/commands/apply-spline-road-to-terrain-command.ts b/src/commands/apply-spline-road-to-terrain-command.ts new file mode 100644 index 00000000..6ceca65e --- /dev/null +++ b/src/commands/apply-spline-road-to-terrain-command.ts @@ -0,0 +1,119 @@ +import { createOpaqueId } from "../core/ids"; +import { cloneEditorSelection, type EditorSelection } from "../core/selection"; +import type { TerrainBrushPatch } from "../core/terrain-brush"; +import type { ToolMode } from "../core/tool-mode"; +import { getTerrains } from "../document/terrains"; +import { createSplineRoadTerrainPatches } from "../geometry/spline-road-terrain"; + +import { + applyTerrainBrushPatchToDocument, + isTerrainBrushPatchEmpty +} from "./apply-terrain-brush-patch-command"; +import type { CommandContext, EditorCommand } from "./command"; + +interface ApplySplineRoadToTerrainCommandOptions { + pathId: string; + label?: string; +} + +function clonePatch(patch: TerrainBrushPatch): TerrainBrushPatch { + return { + terrainId: patch.terrainId, + heightSamples: patch.heightSamples.map((entry) => ({ ...entry })), + paintWeights: patch.paintWeights.map((entry) => ({ ...entry })), + foliageMaskValues: patch.foliageMaskValues.map((entry) => ({ ...entry })), + foliageBlockerMaskValues: patch.foliageBlockerMaskValues.map((entry) => ({ + ...entry + })) + }; +} + +function setPathSelection(pathId: string): EditorSelection { + return { + kind: "paths", + ids: [pathId] + }; +} + +function applyPatches( + context: CommandContext, + patches: readonly TerrainBrushPatch[], + direction: "forward" | "backward" +) { + let nextDocument = context.getDocument(); + + for (const patch of patches) { + nextDocument = applyTerrainBrushPatchToDocument( + nextDocument, + patch, + direction + ); + } + + context.setDocument(nextDocument); +} + +export function createApplySplineRoadToTerrainCommand( + options: ApplySplineRoadToTerrainCommandOptions +): EditorCommand { + let patches: TerrainBrushPatch[] | null = null; + let previousSelection: EditorSelection | null = null; + let previousToolMode: ToolMode | null = null; + + return { + id: createOpaqueId("command"), + label: options.label ?? "Apply spline road to terrain", + execute(context) { + const currentDocument = context.getDocument(); + const path = currentDocument.paths[options.pathId]; + + if (path === undefined) { + throw new Error(`Path ${options.pathId} does not exist.`); + } + + if (!path.road.enabled) { + throw new Error("Enable road preview before applying it to terrain."); + } + + if (previousSelection === null) { + previousSelection = cloneEditorSelection(context.getSelection()); + } + + if (previousToolMode === null) { + previousToolMode = context.getToolMode(); + } + + if (patches === null) { + patches = createSplineRoadTerrainPatches({ + path, + terrains: getTerrains(currentDocument.terrains) + }) + .filter((patch) => !isTerrainBrushPatchEmpty(patch)) + .map(clonePatch); + } + + if (patches.length === 0) { + throw new Error("The spline road did not affect any enabled terrain."); + } + + applyPatches(context, patches, "forward"); + context.setSelection(setPathSelection(options.pathId)); + context.setToolMode("select"); + }, + undo(context) { + if (patches === null) { + return; + } + + applyPatches(context, [...patches].reverse(), "backward"); + + if (previousSelection !== null) { + context.setSelection(previousSelection); + } + + if (previousToolMode !== null) { + context.setToolMode(previousToolMode); + } + } + }; +}