Enhance transform session handling and grid visibility in ViewportHost

This commit is contained in:
2026-04-11 02:40:21 +02:00
parent dad94d6ec9
commit c51a396a82
2 changed files with 280 additions and 31 deletions

View File

@@ -47,9 +47,12 @@ import {
import {
createTransformSession,
doesTransformSessionChangeTarget,
getTransformAxisLabel,
getTransformAxisSpaceLabel,
getTransformOperationLabel,
getTransformTargetLabel,
resolveTransformTarget,
supportsLocalTransformAxisConstraint,
supportsTransformAxisConstraint,
supportsTransformOperation,
type ActiveTransformSession,

View File

@@ -384,6 +384,7 @@ export class ViewportHost {
pointerId: number;
sessionId: string;
axisConstraint: TransformAxis | null;
axisConstraintSpace: TransformAxisSpace;
initialClientPosition: {
x: number;
y: number;
@@ -411,9 +412,19 @@ export class ViewportHost {
this.gridHelpers.xy.rotation.x = Math.PI * 0.5;
this.gridHelpers.yz.rotation.z = Math.PI * 0.5;
this.gridHelpers.xz.visible = true;
this.gridHelpers.xy.visible = false;
this.gridHelpers.yz.visible = false;
for (const gridHelper of Object.values(this.gridHelpers)) {
const gridMaterial = gridHelper.material as LineBasicMaterial;
const centerLineMaterial = gridHelper.children[0]?.material as LineBasicMaterial | undefined;
gridMaterial.transparent = true;
gridMaterial.opacity = 0.48;
if (centerLineMaterial !== undefined) {
centerLineMaterial.transparent = true;
centerLineMaterial.opacity = 0.8;
}
}
this.scene.add(this.gridHelpers.xz);
this.scene.add(this.gridHelpers.xy);
@@ -593,6 +604,15 @@ export class ViewportHost {
this.applyTransformPreview();
}
setGridVisible(visible: boolean) {
if (this.viewportGridVisible === visible) {
return;
}
this.viewportGridVisible = visible;
this.updateGridPresentation();
}
setWhiteboxSelectionMode(mode: WhiteboxSelectionMode) {
if (this.whiteboxSelectionMode === mode) {
return;
@@ -609,6 +629,7 @@ export class ViewportHost {
}
setTransformSession(transformSession: TransformSessionState) {
const previousTransformSession = this.currentTransformSession;
this.currentTransformSession = cloneTransformSession(transformSession);
if (this.currentTransformSession.kind === "none") {
@@ -627,6 +648,30 @@ export class ViewportHost {
};
}
if (
previousTransformSession.kind === "active" &&
this.currentTransformSession.kind === "active" &&
previousTransformSession.id === this.currentTransformSession.id &&
(previousTransformSession.axisConstraint !== this.currentTransformSession.axisConstraint ||
previousTransformSession.axisConstraintSpace !== this.currentTransformSession.axisConstraintSpace) &&
this.currentTransformSession.sourcePanelId === this.panelId &&
this.currentTransformSession.source !== "gizmo" &&
this.keyboardTransformPointerOrigin !== null &&
this.keyboardTransformPointerOrigin.sessionId === this.currentTransformSession.id &&
this.lastCanvasPointerPosition !== null
) {
this.currentTransformSession = this.buildTransformPreviewFromPointer(
this.currentTransformSession,
{
x: this.keyboardTransformPointerOrigin.clientX,
y: this.keyboardTransformPointerOrigin.clientY
},
this.lastCanvasPointerPosition,
this.currentTransformSession.axisConstraint,
this.currentTransformSession.axisConstraintSpace
);
}
this.applyTransformPreview();
this.syncTransformGizmo();
}
@@ -850,12 +895,9 @@ export class ViewportHost {
}
private applyViewModePose() {
this.updateGridPresentation();
const definition = getViewportViewModeDefinition(this.viewMode);
this.gridHelpers.xz.visible = definition.gridPlane === "xz";
this.gridHelpers.xy.visible = definition.gridPlane === "xy";
this.gridHelpers.yz.visible = definition.gridPlane === "yz";
if (definition.cameraType === "perspective") {
this.applyPerspectiveCameraPose();
return;
@@ -865,6 +907,24 @@ export class ViewportHost {
this.applyOrthographicCameraPose();
}
private updateGridPresentation() {
const definition = getViewportViewModeDefinition(this.viewMode);
const visibleGridPlane = this.viewportGridVisible ? definition.gridPlane : null;
this.gridHelpers.xz.visible = visibleGridPlane === "xz";
this.gridHelpers.xy.visible = visibleGridPlane === "xy";
this.gridHelpers.yz.visible = visibleGridPlane === "yz";
this.updateGridPositioning();
}
private updateGridPositioning() {
const align = (value: number) => Math.round(value / DEFAULT_GRID_SIZE) * DEFAULT_GRID_SIZE;
this.gridHelpers.xz.position.set(align(this.cameraTarget.x), 0, align(this.cameraTarget.z));
this.gridHelpers.xy.position.set(align(this.cameraTarget.x), align(this.cameraTarget.y), 0);
this.gridHelpers.yz.position.set(0, align(this.cameraTarget.y), align(this.cameraTarget.z));
}
private createWireframeDisplayMaterial(material: Material): MeshBasicMaterial {
const source = material as Material & {
color?: { getHex(): number };
@@ -1044,6 +1104,87 @@ export class ViewportHost {
}
}
private createRotationQuaternion(rotationDegrees: Vec3): Quaternion {
return new Quaternion().setFromEuler(
new Euler((rotationDegrees.x * Math.PI) / 180, (rotationDegrees.y * Math.PI) / 180, (rotationDegrees.z * Math.PI) / 180, "XYZ")
);
}
private getTransformTargetOrientation(session: ActiveTransformSession): Quaternion | null {
switch (session.target.kind) {
case "brush":
if (session.preview.kind !== "brush") {
return null;
}
return this.createRotationQuaternion(session.preview.rotationDegrees);
case "modelInstance":
if (session.preview.kind !== "modelInstance") {
return null;
}
return this.createRotationQuaternion(session.preview.rotationDegrees);
case "entity":
if (session.preview.kind !== "entity") {
return null;
}
switch (session.preview.rotation.kind) {
case "yaw":
return this.createRotationQuaternion({
x: 0,
y: session.preview.rotation.yawDegrees,
z: 0
});
case "direction":
return new Quaternion().setFromUnitVectors(
new Vector3(0, 1, 0),
new Vector3(
session.preview.rotation.direction.x,
session.preview.rotation.direction.y,
session.preview.rotation.direction.z
).normalize()
);
case "none":
return null;
}
case "brushFace":
case "brushEdge":
case "brushVertex":
return null;
}
}
private getConstraintAxisWorldVector(
session: ActiveTransformSession,
axis: TransformAxis,
axisSpace: TransformAxisSpace
): Vector3 {
const worldAxis = this.axisVector(axis);
if (axisSpace !== "local") {
return worldAxis;
}
const orientation = this.getTransformTargetOrientation(session);
if (orientation === null) {
return worldAxis;
}
return worldAxis.applyQuaternion(orientation).normalize();
}
private getQuaternionEulerDegrees(quaternion: Quaternion): Vec3 {
const euler = new Euler().setFromQuaternion(quaternion, "XYZ");
return {
x: this.normalizeDegrees((euler.x * 180) / Math.PI),
y: this.normalizeDegrees((euler.y * 180) / Math.PI),
z: this.normalizeDegrees((euler.z * 180) / Math.PI)
};
}
private normalizeDegrees(value: number): number {
const normalized = value % 360;
return normalized < 0 ? normalized + 360 : normalized;
@@ -1413,6 +1554,19 @@ export class ViewportHost {
const pivotVector = new Vector3(pivot.x, pivot.y, pivot.z);
this.transformGizmoGroup.position.copy(pivotVector);
this.transformGizmoGroup.quaternion.identity();
if (
session.axisConstraint !== null &&
session.axisConstraintSpace === "local" &&
supportsLocalTransformAxisConstraint(session, session.axisConstraint)
) {
const orientation = this.getTransformTargetOrientation(session);
if (orientation !== null) {
this.transformGizmoGroup.quaternion.copy(orientation);
}
}
let scale = GIZMO_SCREEN_SIZE_ORTHOGRAPHIC / Math.max(this.orthographicCamera.zoom, 0.0001);
@@ -1478,15 +1632,15 @@ export class ViewportHost {
return ORTHOGRAPHIC_FRUSTUM_HEIGHT / this.orthographicCamera.zoom / height;
}
private getAxisMovementDistance(
axis: TransformAxis,
private getMovementDistanceAlongWorldAxis(
axisVector: Vector3,
pivot: Vec3,
origin: { x: number; y: number },
current: { x: number; y: number }
): number {
const pivotVector = new Vector3(pivot.x, pivot.y, pivot.z);
const projectedStart = pivotVector.clone().project(this.getActiveCamera());
const projectedEnd = pivotVector.clone().add(this.axisVector(axis)).project(this.getActiveCamera());
const projectedEnd = pivotVector.clone().add(axisVector.clone().normalize()).project(this.getActiveCamera());
const screenDelta = new Vector2(projectedEnd.x - projectedStart.x, projectedEnd.y - projectedStart.y);
const pointerDelta = new Vector2(current.x - origin.x, current.y - origin.y);
@@ -1504,21 +1658,35 @@ export class ViewportHost {
return -(current.y - origin.y) * this.getFallbackWorldUnitsPerPixel(pivot);
}
private getAxisMovementDistance(
axis: TransformAxis,
pivot: Vec3,
origin: { x: number; y: number },
current: { x: number; y: number },
session?: ActiveTransformSession,
axisSpace: TransformAxisSpace = "world"
): number {
const axisVector = session === undefined ? this.axisVector(axis) : this.getConstraintAxisWorldVector(session, axis, axisSpace);
return this.getMovementDistanceAlongWorldAxis(axisVector, pivot, origin, current);
}
private buildTransformPreviewFromPointer(
session: ActiveTransformSession,
origin: { x: number; y: number },
current: { x: number; y: number },
axisConstraint: TransformAxis | null
axisConstraint: TransformAxis | null,
axisConstraintSpace: TransformAxisSpace
): ActiveTransformSession {
const nextSession = cloneTransformSession(session) as ActiveTransformSession;
nextSession.axisConstraint = axisConstraint;
nextSession.axisConstraintSpace = axisConstraint === null ? "world" : axisConstraintSpace;
switch (session.operation) {
case "translate":
nextSession.preview = this.buildTranslatedPreview(session, origin, current, axisConstraint);
nextSession.preview = this.buildTranslatedPreview(session, origin, current, axisConstraint, nextSession.axisConstraintSpace);
return nextSession;
case "rotate":
nextSession.preview = this.buildRotatedPreview(session, origin, current, axisConstraint);
nextSession.preview = this.buildRotatedPreview(session, origin, current, axisConstraint, nextSession.axisConstraintSpace);
return nextSession;
case "scale":
nextSession.preview = this.buildScaledPreview(session, origin, current, axisConstraint);
@@ -1530,7 +1698,8 @@ export class ViewportHost {
session: ActiveTransformSession,
origin: { x: number; y: number },
current: { x: number; y: number },
axisConstraint: TransformAxis | null
axisConstraint: TransformAxis | null,
axisConstraintSpace: TransformAxisSpace
) {
if (session.target.kind === "brushFace" || session.target.kind === "brushEdge" || session.target.kind === "brushVertex") {
return this.buildComponentTranslatedBrushPreview(session, origin, current, axisConstraint);
@@ -1575,6 +1744,17 @@ export class ViewportHost {
break;
}
}
} else if (axisConstraintSpace === "local" && supportsLocalTransformAxisConstraint(session, axisConstraint)) {
const axisVector = this.getConstraintAxisWorldVector(session, axisConstraint, axisConstraintSpace);
const axisDelta = this.getMovementDistanceAlongWorldAxis(axisVector, initialPosition, origin, current);
const snappedAxisDelta = this.whiteboxSnapEnabled ? snapValueToGrid(axisDelta, this.whiteboxSnapStep) : axisDelta;
this.transformAxisDelta.copy(axisVector).multiplyScalar(snappedAxisDelta);
nextPosition = {
x: initialPosition.x + this.transformAxisDelta.x,
y: initialPosition.y + this.transformAxisDelta.y,
z: initialPosition.z + this.transformAxisDelta.z
};
} else {
const axisDelta = this.getAxisMovementDistance(axisConstraint, initialPosition, origin, current);
nextPosition = this.setAxisComponent(
@@ -1637,7 +1817,8 @@ export class ViewportHost {
session: ActiveTransformSession,
origin: { x: number; y: number },
current: { x: number; y: number },
axisConstraint: TransformAxis | null
axisConstraint: TransformAxis | null,
axisConstraintSpace: TransformAxisSpace
) {
if (session.target.kind === "brushFace" || session.target.kind === "brushEdge") {
return this.buildComponentRotatedBrushPreview(session, origin, current, axisConstraint);
@@ -1645,13 +1826,28 @@ export class ViewportHost {
const effectiveAxis = axisConstraint ?? this.getEffectiveRotationAxis(session);
const pointerDeltaDegrees = (current.x - origin.x - (current.y - origin.y)) * 0.5;
const pointerDeltaRadians = (pointerDeltaDegrees * Math.PI) / 180;
if (session.target.kind === "brush") {
const nextRotationDegrees = {
let nextRotationDegrees = {
...session.target.initialRotationDegrees
};
nextRotationDegrees[effectiveAxis] = this.normalizeDegrees(nextRotationDegrees[effectiveAxis] + pointerDeltaDegrees);
if (axisConstraint !== null) {
const initialOrientation = this.createRotationQuaternion(session.target.initialRotationDegrees);
const deltaRotation = new Quaternion().setFromAxisAngle(
this.getConstraintAxisWorldVector(session, effectiveAxis, axisConstraintSpace),
pointerDeltaRadians
);
nextRotationDegrees = this.getQuaternionEulerDegrees(
axisConstraintSpace === "local" && supportsLocalTransformAxisConstraint(session, effectiveAxis)
? initialOrientation.multiply(deltaRotation)
: deltaRotation.multiply(initialOrientation)
);
} else {
nextRotationDegrees[effectiveAxis] = this.normalizeDegrees(nextRotationDegrees[effectiveAxis] + pointerDeltaDegrees);
}
return {
kind: "brush" as const,
@@ -1667,11 +1863,25 @@ export class ViewportHost {
}
if (session.target.kind === "modelInstance") {
const nextRotationDegrees = {
let nextRotationDegrees = {
...session.target.initialRotationDegrees
};
nextRotationDegrees[effectiveAxis] = this.normalizeDegrees(nextRotationDegrees[effectiveAxis] + pointerDeltaDegrees);
if (axisConstraint !== null) {
const initialOrientation = this.createRotationQuaternion(session.target.initialRotationDegrees);
const deltaRotation = new Quaternion().setFromAxisAngle(
this.getConstraintAxisWorldVector(session, effectiveAxis, axisConstraintSpace),
pointerDeltaRadians
);
nextRotationDegrees = this.getQuaternionEulerDegrees(
axisConstraintSpace === "local" && supportsLocalTransformAxisConstraint(session, effectiveAxis)
? initialOrientation.multiply(deltaRotation)
: deltaRotation.multiply(initialOrientation)
);
} else {
nextRotationDegrees[effectiveAxis] = this.normalizeDegrees(nextRotationDegrees[effectiveAxis] + pointerDeltaDegrees);
}
return {
kind: "modelInstance" as const,
@@ -1690,6 +1900,27 @@ export class ViewportHost {
}
if (session.target.initialRotation.kind === "yaw") {
if (axisConstraint !== null && axisConstraintSpace === "local" && supportsLocalTransformAxisConstraint(session, effectiveAxis)) {
const initialOrientation = this.createRotationQuaternion({
x: 0,
y: session.target.initialRotation.yawDegrees,
z: 0
});
const deltaRotation = new Quaternion().setFromAxisAngle(this.axisVector("y"), pointerDeltaRadians);
const nextRotationDegrees = this.getQuaternionEulerDegrees(initialOrientation.multiply(deltaRotation));
return {
kind: "entity" as const,
position: {
...session.target.initialPosition
},
rotation: {
kind: "yaw" as const,
yawDegrees: normalizeYawDegrees(nextRotationDegrees.y)
}
};
}
return {
kind: "entity" as const,
position: {
@@ -1703,14 +1934,23 @@ export class ViewportHost {
}
if (session.target.initialRotation.kind === "direction") {
const direction = new Vector3(
session.target.initialRotation.direction.x,
session.target.initialRotation.direction.y,
session.target.initialRotation.direction.z
)
.normalize()
.applyAxisAngle(this.axisVector(effectiveAxis).normalize(), (pointerDeltaDegrees * Math.PI) / 180)
.normalize();
const initialOrientation = new Quaternion().setFromUnitVectors(
new Vector3(0, 1, 0),
new Vector3(
session.target.initialRotation.direction.x,
session.target.initialRotation.direction.y,
session.target.initialRotation.direction.z
).normalize()
);
const deltaRotation = new Quaternion().setFromAxisAngle(
this.getConstraintAxisWorldVector(session, effectiveAxis, axisConstraintSpace),
pointerDeltaRadians
);
const nextOrientation =
axisConstraint !== null && axisConstraintSpace === "local" && supportsLocalTransformAxisConstraint(session, effectiveAxis)
? initialOrientation.multiply(deltaRotation)
: deltaRotation.multiply(initialOrientation);
const direction = new Vector3(0, 1, 0).applyQuaternion(nextOrientation).normalize();
return {
kind: "entity" as const,
@@ -3886,6 +4126,7 @@ export class ViewportHost {
sourcePanelId: this.panelId,
operation: interactionSession.operation,
axisConstraint: transformHandle.axisConstraint,
axisConstraintSpace: transformHandle.axisConstraint === null ? "world" : interactionSession.axisConstraintSpace,
target: interactionSession.target
}),
{
@@ -3896,7 +4137,8 @@ export class ViewportHost {
x: event.clientX,
y: event.clientY
},
transformHandle.axisConstraint
transformHandle.axisConstraint,
transformHandle.axisConstraint === null ? "world" : interactionSession.axisConstraintSpace
);
this.currentTransformSession = nextSession;
@@ -3907,6 +4149,7 @@ export class ViewportHost {
pointerId: event.pointerId,
sessionId: nextSession.id,
axisConstraint: transformHandle.axisConstraint,
axisConstraintSpace: nextSession.axisConstraintSpace,
initialClientPosition: {
x: event.clientX,
y: event.clientY
@@ -4022,7 +4265,8 @@ export class ViewportHost {
x: event.clientX,
y: event.clientY
},
this.activeTransformDrag.axisConstraint
this.activeTransformDrag.axisConstraint,
this.activeTransformDrag.axisConstraintSpace
);
this.currentTransformSession = nextSession;
@@ -4123,7 +4367,8 @@ export class ViewportHost {
x: event.clientX,
y: event.clientY
},
this.currentTransformSession.axisConstraint
this.currentTransformSession.axisConstraint,
this.currentTransformSession.axisConstraintSpace
);
this.currentTransformSession = nextSession;
@@ -4567,6 +4812,7 @@ export class ViewportHost {
}
this.animationFrame = window.requestAnimationFrame(this.render);
this.updateGridPositioning();
this.updateTransformGizmoPose();
const now = performance.now();
const dt = this.previousFrameTime === 0 ? 0 : Math.min((now - this.previousFrameTime) / 1000, 1 / 20);