Add hover functionality to ViewportPanel and enhance transform session handling in ViewportHost

This commit is contained in:
2026-04-03 02:34:55 +02:00
parent 2dfa68822c
commit e0b1c31323
2 changed files with 143 additions and 83 deletions

View File

@@ -41,6 +41,7 @@ interface ViewportPanelProps {
focusRequestId: number;
focusSelection: EditorSelection;
onActivatePanel(panelId: ViewportPanelId): void;
onHoverPanel(panelId: ViewportPanelId | null): void;
onSetPanelViewMode(panelId: ViewportPanelId, viewMode: ViewportViewMode): void;
onSetPanelDisplayMode(panelId: ViewportPanelId, displayMode: ViewportDisplayMode): void;
onCommitCreation(toolPreview: CreationViewportToolPreview): boolean;
@@ -72,6 +73,7 @@ export function ViewportPanel({
focusRequestId,
focusSelection,
onActivatePanel,
onHoverPanel,
onSetPanelViewMode,
onSetPanelDisplayMode,
onCommitCreation,
@@ -94,6 +96,8 @@ export function ViewportPanel({
aria-label={`${getViewportPanelLabel(panelId)} viewport panel`}
style={panelStyle}
onPointerDownCapture={() => onActivatePanel(panelId)}
onPointerEnterCapture={() => onHoverPanel(panelId)}
onPointerLeaveCapture={() => onHoverPanel(null)}
onFocusCapture={() => onActivatePanel(panelId)}
>
<div className="viewport-panel__header">

View File

@@ -36,7 +36,10 @@ import { isBrushFaceSelected, isBrushSelected, isModelInstanceSelected, type Edi
import {
cloneTransformSession,
createInactiveTransformSession,
createTransformPreviewFromTarget,
createTransformSession,
resolveTransformTarget,
supportsTransformOperation,
supportsTransformAxisConstraint,
type ActiveTransformSession,
type TransformAxis,
@@ -163,6 +166,7 @@ const GIZMO_PICK_RING_TUBE = 0.14;
const GIZMO_CENTER_HANDLE_SIZE = 0.16;
const GIZMO_SCREEN_SIZE_PERSPECTIVE = 0.11;
const GIZMO_SCREEN_SIZE_ORTHOGRAPHIC = 1.4;
const GIZMO_RENDER_ORDER = 4_000;
const ROTATION_SNAP_DEGREES = 15;
const SCALE_SNAP_STEP = 0.1;
const MIN_SCALE_COMPONENT = 0.1;
@@ -337,6 +341,7 @@ export class ViewportHost {
this.renderer.domElement.addEventListener("pointerleave", this.handlePointerLeave);
this.renderer.domElement.addEventListener("wheel", this.handleWheel, { passive: false });
this.renderer.domElement.addEventListener("auxclick", this.handleAuxClick);
window.addEventListener("pointermove", this.handleWindowPointerMove);
this.resize();
this.resizeObserver = new ResizeObserver(() => {
@@ -549,6 +554,7 @@ export class ViewportHost {
this.renderer.domElement.removeEventListener("pointerleave", this.handlePointerLeave);
this.renderer.domElement.removeEventListener("wheel", this.handleWheel);
this.renderer.domElement.removeEventListener("auxclick", this.handleAuxClick);
window.removeEventListener("pointermove", this.handleWindowPointerMove);
this.clearLocalLights();
this.clearBrushMeshes();
this.clearEntityMarkers();
@@ -903,12 +909,23 @@ export class ViewportHost {
this.transformGizmoGroup.visible = false;
}
private markTransformHandleObject<TObject extends Object3D>(object: TObject): TObject {
object.renderOrder = GIZMO_RENDER_ORDER;
object.traverse((child) => {
child.renderOrder = GIZMO_RENDER_ORDER;
});
return object;
}
private createTransformHandleMaterial(color: number, isActive: boolean, transparent = false) {
return new MeshBasicMaterial({
color,
transparent: transparent || isActive,
opacity: transparent ? 0.001 : isActive ? GIZMO_ACTIVE_OPACITY : GIZMO_INACTIVE_OPACITY,
depthWrite: false
depthWrite: false,
depthTest: false
});
}
@@ -948,7 +965,7 @@ export class ViewportHost {
group.add(line);
group.add(arrow);
group.add(pick);
return group;
return this.markTransformHandleObject(group);
}
private createRotateHandle(axis: TransformAxis, isActive: boolean): Group {
@@ -974,7 +991,7 @@ export class ViewportHost {
pick.userData.transformAxisConstraint = axis;
group.add(ring);
group.add(pick);
return group;
return this.markTransformHandleObject(group);
}
private createScaleHandle(axis: TransformAxis, isActive: boolean): Group {
@@ -1011,7 +1028,7 @@ export class ViewportHost {
group.add(line);
group.add(cube);
group.add(pick);
return group;
return this.markTransformHandleObject(group);
}
private createUniformScaleHandle(isActive: boolean): Mesh {
@@ -1020,17 +1037,45 @@ export class ViewportHost {
this.createTransformHandleMaterial(isActive ? GIZMO_ACTIVE_COLOR : 0xe6edf8, isActive)
);
mesh.userData.transformAxisConstraint = null;
return mesh;
return this.markTransformHandleObject(mesh);
}
private getDisplayedTransformSession(): ActiveTransformSession | null {
if (this.currentTransformSession.kind === "active") {
return this.currentTransformSession;
}
if (this.toolMode !== "select" || this.currentDocument === null) {
return null;
}
const transformTarget = resolveTransformTarget(this.currentDocument, this.currentSelection).target;
if (transformTarget === null || !supportsTransformOperation(transformTarget, "translate")) {
return null;
}
return {
kind: "active",
id: "__selection-translate-gizmo__",
source: "gizmo",
sourcePanelId: this.panelId,
operation: "translate",
axisConstraint: null,
target: transformTarget,
preview: createTransformPreviewFromTarget(transformTarget)
};
}
private syncTransformGizmo() {
this.clearTransformGizmo();
if (this.currentTransformSession.kind !== "active") {
const session = this.getDisplayedTransformSession();
if (session === null) {
return;
}
const session = this.currentTransformSession;
const effectiveRotationAxis = session.operation === "rotate" ? this.getEffectiveRotationAxis(session) : null;
if (session.operation === "translate") {
@@ -1057,11 +1102,13 @@ export class ViewportHost {
}
private updateTransformGizmoPose() {
if (this.currentTransformSession.kind !== "active" || !this.transformGizmoGroup.visible) {
const session = this.getDisplayedTransformSession();
if (session === null || !this.transformGizmoGroup.visible) {
return;
}
const pivot = this.getTransformPivotPosition(this.currentTransformSession);
const pivot = this.getTransformPivotPosition(session);
const pivotVector = new Vector3(pivot.x, pivot.y, pivot.z);
this.transformGizmoGroup.position.copy(pivotVector);
@@ -2200,7 +2247,7 @@ export class ViewportHost {
}
private pickTransformHandle(event: PointerEvent): { axisConstraint: TransformAxis | null } | null {
if (this.currentTransformSession.kind !== "active" || !this.transformGizmoGroup.visible) {
if (!this.transformGizmoGroup.visible) {
return null;
}
@@ -2246,56 +2293,62 @@ export class ViewportHost {
return;
}
if (this.currentTransformSession.kind === "active") {
if (this.currentTransformSession.sourcePanelId !== this.panelId) {
const transformHandle = this.pickTransformHandle(event);
const interactionSession =
this.currentTransformSession.kind === "active"
? this.currentTransformSession.sourcePanelId === this.panelId
? this.currentTransformSession
: null
: this.getDisplayedTransformSession();
if (transformHandle !== null && interactionSession !== null) {
event.preventDefault();
if (
transformHandle.axisConstraint !== null &&
!supportsTransformAxisConstraint(interactionSession, transformHandle.axisConstraint)
) {
return;
}
const transformHandle = this.pickTransformHandle(event);
if (transformHandle !== null) {
event.preventDefault();
if (
transformHandle.axisConstraint !== null &&
!supportsTransformAxisConstraint(this.currentTransformSession, transformHandle.axisConstraint)
) {
return;
}
const nextSession = this.buildTransformPreviewFromPointer(
createTransformSession({
source: "gizmo",
sourcePanelId: this.panelId,
operation: this.currentTransformSession.operation,
axisConstraint: transformHandle.axisConstraint,
target: this.currentTransformSession.target
}),
{
x: event.clientX,
y: event.clientY
},
{
x: event.clientX,
y: event.clientY
},
transformHandle.axisConstraint
);
this.currentTransformSession = nextSession;
this.applyTransformPreview();
this.syncTransformGizmo();
this.transformSessionChangeHandler?.(nextSession);
this.activeTransformDrag = {
pointerId: event.pointerId,
sessionId: nextSession.id,
const nextSession = this.buildTransformPreviewFromPointer(
createTransformSession({
source: "gizmo",
sourcePanelId: this.panelId,
operation: interactionSession.operation,
axisConstraint: transformHandle.axisConstraint,
initialClientPosition: {
x: event.clientX,
y: event.clientY
}
};
this.renderer.domElement.setPointerCapture(event.pointerId);
target: interactionSession.target
}),
{
x: event.clientX,
y: event.clientY
},
{
x: event.clientX,
y: event.clientY
},
transformHandle.axisConstraint
);
this.currentTransformSession = nextSession;
this.applyTransformPreview();
this.syncTransformGizmo();
this.transformSessionChangeHandler?.(nextSession);
this.activeTransformDrag = {
pointerId: event.pointerId,
sessionId: nextSession.id,
axisConstraint: transformHandle.axisConstraint,
initialClientPosition: {
x: event.clientX,
y: event.clientY
}
};
this.renderer.domElement.setPointerCapture(event.pointerId);
return;
}
if (this.currentTransformSession.kind === "active") {
if (this.currentTransformSession.sourcePanelId !== this.panelId) {
return;
}
@@ -2496,33 +2549,6 @@ export class ViewportHost {
return;
}
if (
this.currentTransformSession.kind === "active" &&
this.currentTransformSession.sourcePanelId === this.panelId &&
this.currentTransformSession.source !== "gizmo" &&
this.keyboardTransformPointerOrigin !== null &&
this.keyboardTransformPointerOrigin.sessionId === this.currentTransformSession.id
) {
const nextSession = this.buildTransformPreviewFromPointer(
this.currentTransformSession,
{
x: this.keyboardTransformPointerOrigin.clientX,
y: this.keyboardTransformPointerOrigin.clientY
},
{
x: event.clientX,
y: event.clientY
},
this.currentTransformSession.axisConstraint
);
this.currentTransformSession = nextSession;
this.applyTransformPreview();
this.syncTransformGizmo();
this.transformSessionChangeHandler?.(nextSession);
return;
}
if (this.toolMode !== "create" || this.creationPreview === null) {
return;
}
@@ -2579,6 +2605,36 @@ export class ViewportHost {
// viewport panel will update it as the pointer continues moving.
};
private handleWindowPointerMove = (event: PointerEvent) => {
if (
this.currentTransformSession.kind !== "active" ||
this.currentTransformSession.sourcePanelId !== this.panelId ||
this.currentTransformSession.source === "gizmo" ||
this.keyboardTransformPointerOrigin === null ||
this.keyboardTransformPointerOrigin.sessionId !== this.currentTransformSession.id
) {
return;
}
const nextSession = this.buildTransformPreviewFromPointer(
this.currentTransformSession,
{
x: this.keyboardTransformPointerOrigin.clientX,
y: this.keyboardTransformPointerOrigin.clientY
},
{
x: event.clientX,
y: event.clientY
},
this.currentTransformSession.axisConstraint
);
this.currentTransformSession = nextSession;
this.applyTransformPreview();
this.syncTransformGizmo();
this.transformSessionChangeHandler?.(nextSession);
};
private handleWheel = (event: WheelEvent) => {
event.preventDefault();