Add path-related functionality in App.tsx, scene-document-validation.ts, and viewport-host.ts

This commit is contained in:
2026-04-13 21:23:04 +02:00
parent 17e7343a46
commit 1bba848eb7
3 changed files with 60 additions and 5 deletions

View File

@@ -18,6 +18,7 @@ import { createDuplicateSelectionCommand } from "../commands/duplicate-selection
import { createImportAudioAssetCommand } from "../commands/import-audio-asset-command";
import { createImportBackgroundImageAssetCommand } from "../commands/import-background-image-asset-command";
import { createImportModelAssetCommand } from "../commands/import-model-asset-command";
import { createDeletePathCommand } from "../commands/delete-path-command";
import { createDeleteModelInstanceCommand } from "../commands/delete-model-instance-command";
import { createCommitTransformSessionCommand } from "../commands/commit-transform-session-command";
import { createMoveBoxBrushCommand } from "../commands/move-box-brush-command";
@@ -34,6 +35,8 @@ import { createSetActiveSceneCommand } from "../commands/set-active-scene-comman
import { createDeleteInteractionLinkCommand } from "../commands/delete-interaction-link-command";
import { createSetModelInstanceAuthoredStateCommand } from "../commands/set-model-instance-authored-state-command";
import { createSetModelInstanceNameCommand } from "../commands/set-model-instance-name-command";
import { createSetPathAuthoredStateCommand } from "../commands/set-path-authored-state-command";
import { createSetPathNameCommand } from "../commands/set-path-name-command";
import { createSetProjectNameCommand } from "../commands/set-project-name-command";
import { createSetProjectTimeSettingsCommand } from "../commands/set-project-time-settings-command";
import { createSetSceneLoadingScreenCommand } from "../commands/set-scene-loading-screen-command";
@@ -41,6 +44,7 @@ import { createSetSceneNameCommand } from "../commands/set-scene-name-command";
import { createSetWorldSettingsCommand } from "../commands/set-world-settings-command";
import { createUpsertEntityCommand } from "../commands/upsert-entity-command";
import { createUpsertModelInstanceCommand } from "../commands/upsert-model-instance-command";
import { createUpsertPathCommand } from "../commands/upsert-path-command";
import { createUpsertInteractionLinkCommand } from "../commands/upsert-interaction-link-command";
import {
getSelectedBrushEdgeId,
@@ -49,8 +53,10 @@ import {
getSingleSelectedBrushId,
getSingleSelectedEntityId,
getSingleSelectedModelInstanceId,
getSingleSelectedPathId,
isBrushFaceSelected,
isBrushSelected,
isPathSelected,
type EditorSelection
} from "../core/selection";
import {
@@ -161,6 +167,16 @@ import {
DEFAULT_PROJECT_NAME,
type SceneLoadingScreenSettings
} from "../document/scene-document";
import {
MIN_SCENE_PATH_POINT_COUNT,
areScenePathsEqual,
createAppendedScenePathPoint,
createScenePath,
getScenePathLabel,
getScenePaths,
normalizeScenePathName,
type ScenePath
} from "../document/paths";
import {
areProjectTimeSettingsEqual,
cloneProjectTimeSettings,
@@ -876,6 +892,19 @@ function getSelectedModelInstance(
);
}
function getSelectedPath(
selection: EditorSelection,
paths: ScenePath[]
): ScenePath | null {
const selectedPathId = getSingleSelectedPathId(selection);
if (selectedPathId === null) {
return null;
}
return paths.find((path) => path.id === selectedPathId) ?? null;
}
function isModelAsset(asset: ProjectAssetRecord): asset is ModelAssetRecord {
return asset.kind === "model";
}
@@ -992,6 +1021,11 @@ function getBrushLabel(brush: BoxBrush, index: number): string {
return brush.name ?? `Whitebox Box ${index + 1}`;
}
function getPathLabelById(pathId: string, paths: ScenePath[]): string {
const pathIndex = paths.findIndex((path) => path.id === pathId);
return pathIndex === -1 ? "Path" : getScenePathLabel(paths[pathIndex], pathIndex);
}
function formatAuthoredObjectStateSummary(state: {
visible: boolean;
enabled: boolean;
@@ -1032,6 +1066,7 @@ function getSelectedBrushLabel(
function describeSelection(
selection: EditorSelection,
brushes: BoxBrush[],
paths: ScenePath[],
modelInstances: Record<string, ModelInstance>,
assets: Record<string, ProjectAssetRecord>,
entities: Record<string, EntityInstance>
@@ -1047,6 +1082,8 @@ function describeSelection(
return `1 edge selected (${BOX_EDGE_LABELS[selection.edgeId]} on ${getBrushLabelById(selection.brushId, brushes)})`;
case "brushVertex":
return `1 vertex selected (${BOX_VERTEX_LABELS[selection.vertexId]} on ${getBrushLabelById(selection.brushId, brushes)})`;
case "paths":
return `${selection.ids.length} path${selection.ids.length === 1 ? "" : "s"} selected (${getPathLabelById(selection.ids[0], paths)})`;
case "entities":
return `${selection.ids.length} entity selected (${getEntityDisplayLabelById(selection.ids[0], entities, assets)})`;
case "modelInstances":
@@ -1212,6 +1249,7 @@ function isTextEntryTarget(target: EventTarget | null): boolean {
function selectionCanBeDuplicated(selection: EditorSelection): boolean {
switch (selection.kind) {
case "brushes":
case "paths":
case "entities":
case "modelInstances":
return selection.ids.length > 0;
@@ -1564,6 +1602,7 @@ export function App({ store, initialStatusMessage }: AppProps) {
}
const brushList = Object.values(editorState.document.brushes);
const pathList = getScenePaths(editorState.document.paths);
const layoutMode = editorState.viewportLayoutMode;
const activePanelId = editorState.activeViewportPanelId;
const viewportToolPreview = editorState.viewportTransientState.toolPreview;
@@ -1578,6 +1617,7 @@ export function App({ store, initialStatusMessage }: AppProps) {
);
const materialList = sortDocumentMaterials(editorState.document.materials);
const selectedBrush = getSelectedBoxBrush(editorState.selection, brushList);
const selectedPath = getSelectedPath(editorState.selection, pathList);
const selectedEntity = getSelectedEntity(editorState.selection, entityList);
const selectedModelInstance = getSelectedModelInstance(
editorState.selection,

View File

@@ -3554,12 +3554,12 @@ export function validateSceneDocument(
}
}
for (const [modelInstanceKey, modelInstance] of Object.entries(
for (const [pathKey, pathValue] of Object.entries(
document.paths
)) {
const path = `paths.${modelInstanceKey}`;
const path = `paths.${pathKey}`;
if (modelInstance.id !== modelInstanceKey) {
if (pathValue.id !== pathKey) {
diagnostics.push(
createDiagnostic(
"error",
@@ -3570,8 +3570,8 @@ export function validateSceneDocument(
);
}
registerAuthoredId(modelInstance.id, path, seenIds, diagnostics);
validateScenePath(modelInstance, path, diagnostics);
registerAuthoredId(pathValue.id, path, seenIds, diagnostics);
validateScenePath(pathValue, path, diagnostics);
}
for (const [modelInstanceKey, modelInstance] of Object.entries(

View File

@@ -975,6 +975,7 @@ export class ViewportHost {
window.removeEventListener("pointermove", this.handleWindowPointerMove);
this.clearLocalLights();
this.clearBrushMeshes();
this.clearPaths();
this.clearEntityMarkers();
this.creationPreviewChangeHandler = null;
this.creationCommitHandler = null;
@@ -5404,6 +5405,8 @@ export class ViewportHost {
return `brushEdge:${selection.brushId}:${selection.edgeId}`;
case "brushVertex":
return `brushVertex:${selection.brushId}:${selection.vertexId}`;
case "paths":
return selection.ids.length === 1 ? `path:${selection.ids[0]}` : null;
case "entities":
return selection.ids.length === 1 ? `entity:${selection.ids[0]}` : null;
case "modelInstances":
@@ -5427,6 +5430,14 @@ export class ViewportHost {
};
}
const pathId = hit.object.userData.pathId;
if (typeof pathId === "string") {
return {
kind: "paths",
ids: [pathId]
};
}
const modelInstanceId = this.findModelInstanceId(hit.object);
if (modelInstanceId !== null) {
return {
@@ -5504,6 +5515,10 @@ export class ViewportHost {
this.entityRenderObjects.values(),
(renderObjects) => renderObjects.group
),
...Array.from(this.pathRenderObjects.values(), (renderObjects) => [
renderObjects.line,
...renderObjects.pointMeshes.map((pointMesh) => pointMesh.mesh)
]).flat(),
...Array.from(this.modelRenderObjects.values()),
...this.getBrushPickableObjects()
],