Refactor viewport tool preview handling for creation and placement modes

This commit is contained in:
2026-04-03 00:21:55 +02:00
parent 1344c404d3
commit 3d7c0d75e4

View File

@@ -1303,28 +1303,18 @@ export class ViewportHost {
return; return;
} }
if (this.toolMode === "box-create") { if (this.toolMode === "create" && this.creationPreview !== null) {
const previewCenter = this.getBoxCreatePreviewCenter(event); const previewCenter = this.getCreationPreviewCenter(event, this.creationPreview.target);
const nextCreationPreview = {
if (previewCenter !== null) { ...this.creationPreview,
this.createBoxBrushHandler?.(previewCenter);
}
return;
}
if (this.toolMode === "place" && this.placementPreview !== null) {
const previewCenter = this.getPlacementPreviewCenter(event);
if (previewCenter !== null) {
const nextPlacementPreview = {
...this.placementPreview,
center: previewCenter center: previewCenter
} as PlacementViewportToolPreview; };
this.syncPlacementPreview(nextPlacementPreview); this.syncCreationPreview(nextCreationPreview);
this.placementPreviewChangeHandler?.(nextPlacementPreview); this.creationPreviewChangeHandler?.(nextCreationPreview);
this.placementCommitHandler?.(nextPlacementPreview);
if (previewCenter !== null) {
this.creationCommitHandler?.(nextCreationPreview);
} }
return; return;
@@ -1471,22 +1461,18 @@ export class ViewportHost {
return; return;
} }
if (this.toolMode !== "box-create") { if (this.toolMode !== "create" || this.creationPreview === null) {
if (this.toolMode !== "place" || this.placementPreview === null) {
return; return;
} }
const previewCenter = this.getPlacementPreviewCenter(event); const previewCenter = this.getCreationPreviewCenter(event, this.creationPreview.target);
const nextPlacementPreview = previewCenter === null ? { kind: "none" } : { ...this.placementPreview, center: previewCenter }; const nextCreationPreview = {
...this.creationPreview,
center: previewCenter
};
this.syncPlacementPreview(previewCenter === null ? null : (nextPlacementPreview as PlacementViewportToolPreview)); this.syncCreationPreview(nextCreationPreview);
this.placementPreviewChangeHandler?.(nextPlacementPreview as ViewportToolPreview); this.creationPreviewChangeHandler?.(nextCreationPreview);
return;
}
const previewCenter = this.getBoxCreatePreviewCenter(event);
this.syncBoxCreatePreview(previewCenter);
this.boxCreatePreviewChangeHandler?.(previewCenter);
}; };
private handlePointerUp = (event: PointerEvent) => { private handlePointerUp = (event: PointerEvent) => {
@@ -1506,19 +1492,6 @@ export class ViewportHost {
if (this.activeCameraDragPointerId !== null) { if (this.activeCameraDragPointerId !== null) {
return; return;
} }
if (this.toolMode !== "box-create" && this.toolMode !== "place") {
return;
}
if (this.toolMode === "box-create") {
this.syncBoxCreatePreview(null);
this.boxCreatePreviewChangeHandler?.(null);
return;
}
this.syncPlacementPreview(null);
this.placementPreviewChangeHandler?.({ kind: "none" });
}; };
private handleWheel = (event: WheelEvent) => { private handleWheel = (event: WheelEvent) => {
@@ -1606,19 +1579,24 @@ export class ViewportHost {
this.applyOrthographicCameraPose(); this.applyOrthographicCameraPose();
} }
private getBoxCreatePreviewCenter(event: PointerEvent): Vec3 | null { private getCreationPreviewCenter(event: PointerEvent, target: CreationTarget): Vec3 | null {
return this.getBoxPlacementPreviewCenter(event, DEFAULT_BOX_BRUSH_SIZE); switch (target.kind) {
case "box-brush":
return this.getBoxCreationPreviewCenter(event, DEFAULT_BOX_BRUSH_SIZE);
case "entity":
switch (target.entityKind) {
case "triggerVolume":
return this.getBoxCreationPreviewCenter(event, DEFAULT_TRIGGER_VOLUME_SIZE);
case "pointLight":
case "playerStart":
case "soundEmitter":
case "teleportTarget":
case "interactable":
case "spotLight":
return this.getPlanarCreationAnchor(event);
} }
case "model-instance": {
private getPlacementPreviewCenter(event: PointerEvent): Vec3 | null { const anchor = this.getPlanarCreationAnchor(event);
if (this.placementPreview === null) {
return null;
}
const target = this.placementPreview.target;
if (target.kind === "model-instance") {
const anchor = this.getPlanarPlacementAnchor(event);
if (anchor === null) { if (anchor === null) {
return null; return null;
@@ -1632,21 +1610,10 @@ export class ViewportHost {
return createModelInstancePlacementPosition(asset, anchor); return createModelInstancePlacementPosition(asset, anchor);
} }
switch (target.entityKind) {
case "triggerVolume":
return this.getBoxPlacementPreviewCenter(event, DEFAULT_TRIGGER_VOLUME_SIZE);
case "pointLight":
case "playerStart":
case "soundEmitter":
case "teleportTarget":
case "interactable":
case "spotLight":
return this.getPlanarPlacementAnchor(event);
} }
} }
private getPlanarPlacementAnchor(event: PointerEvent): Vec3 | null { private getPlanarCreationAnchor(event: PointerEvent): Vec3 | null {
const bounds = this.renderer.domElement.getBoundingClientRect(); const bounds = this.renderer.domElement.getBoundingClientRect();
if (bounds.width === 0 || bounds.height === 0) { if (bounds.width === 0 || bounds.height === 0) {
@@ -1685,7 +1652,7 @@ export class ViewportHost {
} }
} }
private getBoxPlacementPreviewCenter(event: PointerEvent, size: Vec3): Vec3 | null { private getBoxCreationPreviewCenter(event: PointerEvent, size: Vec3): Vec3 | null {
const bounds = this.renderer.domElement.getBoundingClientRect(); const bounds = this.renderer.domElement.getBoundingClientRect();
if (bounds.width === 0 || bounds.height === 0) { if (bounds.width === 0 || bounds.height === 0) {
@@ -1724,49 +1691,30 @@ export class ViewportHost {
} }
} }
private syncBoxCreatePreview(center: Vec3 | null) { private getCreationPreviewTargetKey(target: CreationTarget): string {
if ( switch (target.kind) {
(center === null && this.lastBoxCreatePreviewCenter === null) || case "box-brush":
(center !== null && return "box-brush";
this.lastBoxCreatePreviewCenter !== null && case "entity":
center.x === this.lastBoxCreatePreviewCenter.x && return `entity:${target.entityKind}:${target.audioAssetId}`;
center.y === this.lastBoxCreatePreviewCenter.y && case "model-instance":
center.z === this.lastBoxCreatePreviewCenter.z) return `model-instance:${target.assetId}`;
) { }
}
private clearCreationPreviewObject() {
if (this.creationPreviewObject === null) {
this.creationPreviewTargetKey = null;
return; return;
} }
this.lastBoxCreatePreviewCenter = center === null ? null : { ...center }; this.scene.remove(this.creationPreviewObject);
this.boxCreatePreviewMesh.visible = center !== null; disposeModelInstance(this.creationPreviewObject);
this.boxCreatePreviewEdges.visible = center !== null; this.creationPreviewObject = null;
this.creationPreviewTargetKey = null;
if (center !== null) {
this.boxCreatePreviewMesh.position.set(center.x, center.y, center.z);
this.boxCreatePreviewEdges.position.set(center.x, center.y, center.z);
}
} }
private getPlacementPreviewTargetKey(toolPreview: PlacementViewportToolPreview): string { private createCreationPreviewObject(toolPreview: CreationViewportToolPreview): Group {
if (toolPreview.target.kind === "entity") {
return `entity:${toolPreview.target.entityKind}:${toolPreview.target.audioAssetId}`;
}
return `model-instance:${toolPreview.target.assetId}`;
}
private clearPlacementPreviewObject() {
if (this.placementPreviewObject === null) {
this.placementPreviewTargetKey = null;
return;
}
this.scene.remove(this.placementPreviewObject);
disposeModelInstance(this.placementPreviewObject);
this.placementPreviewObject = null;
this.placementPreviewTargetKey = null;
}
private createPlacementPreviewObject(toolPreview: PlacementViewportToolPreview): Group {
const previewPosition = toolPreview.center ?? { const previewPosition = toolPreview.center ?? {
x: 0, x: 0,
y: 0, y: 0,
@@ -1774,11 +1722,16 @@ export class ViewportHost {
}; };
switch (toolPreview.target.kind) { switch (toolPreview.target.kind) {
case "box-brush": {
const fallbackGroup = new Group();
fallbackGroup.visible = false;
return fallbackGroup;
}
case "entity": case "entity":
switch (toolPreview.target.entityKind) { switch (toolPreview.target.entityKind) {
case "pointLight": case "pointLight":
return this.createPointLightGizmoRenderObjects( return this.createPointLightGizmoRenderObjects(
"placement-preview", "creation-preview",
previewPosition, previewPosition,
DEFAULT_POINT_LIGHT_DISTANCE, DEFAULT_POINT_LIGHT_DISTANCE,
PLACEMENT_PREVIEW_COLOR_HEX, PLACEMENT_PREVIEW_COLOR_HEX,
@@ -1786,7 +1739,7 @@ export class ViewportHost {
).group; ).group;
case "spotLight": case "spotLight":
return this.createSpotLightGizmoRenderObjects( return this.createSpotLightGizmoRenderObjects(
"placement-preview", "creation-preview",
previewPosition, previewPosition,
DEFAULT_SPOT_LIGHT_DIRECTION, DEFAULT_SPOT_LIGHT_DIRECTION,
DEFAULT_SPOT_LIGHT_DISTANCE, DEFAULT_SPOT_LIGHT_DISTANCE,
@@ -1795,10 +1748,10 @@ export class ViewportHost {
false false
).group; ).group;
case "playerStart": case "playerStart":
return this.createPlayerStartRenderObjects("placement-preview", previewPosition, DEFAULT_PLAYER_START_YAW_DEGREES, false).group; return this.createPlayerStartRenderObjects("creation-preview", previewPosition, DEFAULT_PLAYER_START_YAW_DEGREES, false).group;
case "soundEmitter": case "soundEmitter":
return this.createSoundEmitterRenderObjects( return this.createSoundEmitterRenderObjects(
"placement-preview", "creation-preview",
previewPosition, previewPosition,
DEFAULT_SOUND_EMITTER_REF_DISTANCE, DEFAULT_SOUND_EMITTER_REF_DISTANCE,
DEFAULT_SOUND_EMITTER_MAX_DISTANCE, DEFAULT_SOUND_EMITTER_MAX_DISTANCE,
@@ -1807,7 +1760,7 @@ export class ViewportHost {
).group; ).group;
case "triggerVolume": case "triggerVolume":
return this.createTriggerVolumeRenderObjects( return this.createTriggerVolumeRenderObjects(
"placement-preview", "creation-preview",
previewPosition, previewPosition,
DEFAULT_TRIGGER_VOLUME_SIZE, DEFAULT_TRIGGER_VOLUME_SIZE,
false, false,
@@ -1815,7 +1768,7 @@ export class ViewportHost {
).group; ).group;
case "teleportTarget": case "teleportTarget":
return this.createTeleportTargetRenderObjects( return this.createTeleportTargetRenderObjects(
"placement-preview", "creation-preview",
previewPosition, previewPosition,
DEFAULT_TELEPORT_TARGET_YAW_DEGREES, DEFAULT_TELEPORT_TARGET_YAW_DEGREES,
false, false,
@@ -1823,7 +1776,7 @@ export class ViewportHost {
).group; ).group;
case "interactable": case "interactable":
return this.createInteractableRenderObjects( return this.createInteractableRenderObjects(
"placement-preview", "creation-preview",
previewPosition, previewPosition,
DEFAULT_INTERACTABLE_RADIUS, DEFAULT_INTERACTABLE_RADIUS,
false, false,
@@ -1850,19 +1803,19 @@ export class ViewportHost {
} }
} }
throw new Error("Unsupported placement preview target."); throw new Error("Unsupported creation preview target.");
} }
private syncPlacementPreview(toolPreview: PlacementViewportToolPreview | null) { private syncCreationPreview(toolPreview: CreationViewportToolPreview | null) {
const currentToolPreview = this.placementPreview === null ? { kind: "none" as const } : this.placementPreview; const currentToolPreview = this.creationPreview === null ? { kind: "none" as const } : this.creationPreview;
const nextToolPreview = toolPreview === null ? { kind: "none" as const } : toolPreview; const nextToolPreview = toolPreview === null ? { kind: "none" as const } : toolPreview;
if (areViewportToolPreviewsEqual(currentToolPreview, nextToolPreview)) { if (areViewportToolPreviewsEqual(currentToolPreview, nextToolPreview)) {
return; return;
} }
this.placementPreview = toolPreview === null ? null : { this.creationPreview = toolPreview === null ? null : {
kind: "placement", kind: "create",
sourcePanelId: toolPreview.sourcePanelId, sourcePanelId: toolPreview.sourcePanelId,
target: target:
toolPreview.target.kind === "entity" toolPreview.target.kind === "entity"
@@ -1874,34 +1827,56 @@ export class ViewportHost {
: { : {
kind: "model-instance", kind: "model-instance",
assetId: toolPreview.target.assetId assetId: toolPreview.target.assetId
}
: {
kind: "box-brush"
}, },
center: toolPreview.center === null ? null : { ...toolPreview.center } center: toolPreview.center === null ? null : { ...toolPreview.center }
}; };
if (toolPreview === null) { if (toolPreview === null) {
this.clearPlacementPreviewObject(); this.boxCreatePreviewMesh.visible = false;
this.boxCreatePreviewEdges.visible = false;
this.clearCreationPreviewObject();
return; return;
} }
const nextTargetKey = this.getPlacementPreviewTargetKey(toolPreview); if (toolPreview.target.kind === "box-brush") {
this.boxCreatePreviewMesh.visible = toolPreview.center !== null;
if (this.placementPreviewObject !== null && this.placementPreviewTargetKey === nextTargetKey) { this.boxCreatePreviewEdges.visible = toolPreview.center !== null;
this.placementPreviewObject.visible = toolPreview.center !== null;
if (toolPreview.center !== null) { if (toolPreview.center !== null) {
this.placementPreviewObject.position.set(toolPreview.center.x, toolPreview.center.y, toolPreview.center.z); this.boxCreatePreviewMesh.position.set(toolPreview.center.x, toolPreview.center.y, toolPreview.center.z);
this.boxCreatePreviewEdges.position.set(toolPreview.center.x, toolPreview.center.y, toolPreview.center.z);
} }
this.placementPreviewTargetKey = nextTargetKey; this.clearCreationPreviewObject();
this.creationPreviewTargetKey = null;
return; return;
} }
this.clearPlacementPreviewObject(); const nextTargetKey = this.getCreationPreviewTargetKey(toolPreview.target);
const placementPreviewObject = this.createPlacementPreviewObject(toolPreview); this.boxCreatePreviewMesh.visible = false;
placementPreviewObject.visible = toolPreview.center !== null; this.boxCreatePreviewEdges.visible = false;
this.scene.add(placementPreviewObject);
this.placementPreviewObject = placementPreviewObject; if (this.creationPreviewObject !== null && this.creationPreviewTargetKey === nextTargetKey) {
this.creationPreviewObject.visible = toolPreview.center !== null;
if (toolPreview.center !== null) {
this.creationPreviewObject.position.set(toolPreview.center.x, toolPreview.center.y, toolPreview.center.z);
}
this.creationPreviewTargetKey = nextTargetKey;
return;
}
this.clearCreationPreviewObject();
const creationPreviewObject = this.createCreationPreviewObject(toolPreview);
creationPreviewObject.visible = toolPreview.center !== null;
this.scene.add(creationPreviewObject);
this.creationPreviewObject = creationPreviewObject;
this.placementPreviewTargetKey = nextTargetKey; this.placementPreviewTargetKey = nextTargetKey;
} }