Add transform session handling to ViewportCanvas and ViewportPanel
This commit is contained in:
@@ -6,6 +6,7 @@ import type { ProjectAssetRecord } from "../assets/project-assets";
|
||||
import type { EditorSelection } from "../core/selection";
|
||||
import type { ToolMode } from "../core/tool-mode";
|
||||
import type { Vec3 } from "../core/vector";
|
||||
import type { ActiveTransformSession, TransformSessionState } from "../core/transform-session";
|
||||
import type { SceneDocument } from "../document/scene-document";
|
||||
import type { WorldSettings } from "../document/world-settings";
|
||||
import { createWorldBackgroundStyle } from "../shared-ui/world-background-style";
|
||||
@@ -34,6 +35,7 @@ interface ViewportCanvasProps {
|
||||
selection: EditorSelection;
|
||||
toolMode: ToolMode;
|
||||
toolPreview: ViewportToolPreview;
|
||||
transformSession: TransformSessionState;
|
||||
cameraState: ViewportPanelCameraState;
|
||||
viewMode: ViewportViewMode;
|
||||
displayMode: ViewportDisplayMode;
|
||||
@@ -45,6 +47,9 @@ interface ViewportCanvasProps {
|
||||
onCommitCreation(toolPreview: CreationViewportToolPreview): boolean;
|
||||
onCameraStateChange(cameraState: ViewportPanelCameraState): void;
|
||||
onToolPreviewChange(toolPreview: ViewportToolPreview): void;
|
||||
onTransformSessionChange(transformSession: TransformSessionState): void;
|
||||
onTransformCommit(transformSession: ActiveTransformSession): void;
|
||||
onTransformCancel(): void;
|
||||
}
|
||||
|
||||
export function ViewportCanvas({
|
||||
@@ -57,6 +62,7 @@ export function ViewportCanvas({
|
||||
selection,
|
||||
toolMode,
|
||||
toolPreview,
|
||||
transformSession,
|
||||
cameraState,
|
||||
viewMode,
|
||||
displayMode,
|
||||
@@ -67,7 +73,10 @@ export function ViewportCanvas({
|
||||
onSelectionChange,
|
||||
onCommitCreation,
|
||||
onCameraStateChange,
|
||||
onToolPreviewChange
|
||||
onToolPreviewChange,
|
||||
onTransformSessionChange,
|
||||
onTransformCommit,
|
||||
onTransformCancel
|
||||
}: ViewportCanvasProps) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const hostRef = useRef<ViewportHost | null>(null);
|
||||
@@ -94,6 +103,7 @@ export function ViewportCanvas({
|
||||
try {
|
||||
const viewportHost = new ViewportHost();
|
||||
hostRef.current = viewportHost;
|
||||
viewportHost.setPanelId(panelId);
|
||||
viewportHost.mount(container);
|
||||
setViewportMessage(null);
|
||||
|
||||
@@ -108,6 +118,10 @@ export function ViewportCanvas({
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
hostRef.current?.setPanelId(panelId);
|
||||
}, [panelId]);
|
||||
|
||||
useEffect(() => {
|
||||
hostRef.current?.updateWorld(world);
|
||||
}, [world]);
|
||||
@@ -157,6 +171,18 @@ export function ViewportCanvas({
|
||||
hostRef.current?.setCreationCommitHandler(onCommitCreation);
|
||||
}, [onCommitCreation]);
|
||||
|
||||
useEffect(() => {
|
||||
hostRef.current?.setTransformSessionChangeHandler(onTransformSessionChange);
|
||||
}, [onTransformSessionChange]);
|
||||
|
||||
useEffect(() => {
|
||||
hostRef.current?.setTransformCommitHandler(onTransformCommit);
|
||||
}, [onTransformCommit]);
|
||||
|
||||
useEffect(() => {
|
||||
hostRef.current?.setTransformCancelHandler(onTransformCancel);
|
||||
}, [onTransformCancel]);
|
||||
|
||||
useEffect(() => {
|
||||
hostRef.current?.setToolMode(toolMode);
|
||||
}, [toolMode]);
|
||||
@@ -165,6 +191,10 @@ export function ViewportCanvas({
|
||||
hostRef.current?.setCreationPreview(toolMode === "create" && toolPreview.kind === "create" ? toolPreview : null);
|
||||
}, [toolMode, toolPreview]);
|
||||
|
||||
useEffect(() => {
|
||||
hostRef.current?.setTransformSession(transformSession);
|
||||
}, [transformSession]);
|
||||
|
||||
useEffect(() => {
|
||||
if (focusRequestId === 0) {
|
||||
return;
|
||||
@@ -174,8 +204,9 @@ export function ViewportCanvas({
|
||||
}, [focusRequestId, focusSelection, sceneDocument]);
|
||||
|
||||
const previewVisible = toolMode === "create" && toolPreview.kind === "create" && toolPreview.center !== null;
|
||||
const transformPreviewVisible = transformSession.kind === "active";
|
||||
const showViewModeOverlay = layoutMode === "quad";
|
||||
const showOverlay = showViewModeOverlay || previewVisible;
|
||||
const showOverlay = showViewModeOverlay || previewVisible || transformPreviewVisible;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -205,6 +236,13 @@ export function ViewportCanvas({
|
||||
Preview: {(toolPreview.center as Vec3).x}, {(toolPreview.center as Vec3).y}, {(toolPreview.center as Vec3).z}
|
||||
</div>
|
||||
)}
|
||||
{!transformPreviewVisible ? null : (
|
||||
<div className="viewport-canvas__overlay-preview" data-testid={`viewport-transform-preview-${panelId}`}>
|
||||
{transformSession.kind !== "active"
|
||||
? null
|
||||
: `${transformSession.operation}${transformSession.axisConstraint === null ? "" : ` · ${transformSession.axisConstraint.toUpperCase()}`}`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { LoadedModelAsset } from "../assets/gltf-model-import";
|
||||
import type { LoadedImageAsset } from "../assets/image-assets";
|
||||
import type { ProjectAssetRecord } from "../assets/project-assets";
|
||||
import type { EditorSelection } from "../core/selection";
|
||||
import type { ActiveTransformSession, TransformSessionState } from "../core/transform-session";
|
||||
import type { ToolMode } from "../core/tool-mode";
|
||||
import type { SceneDocument } from "../document/scene-document";
|
||||
import type { WorldSettings } from "../document/world-settings";
|
||||
@@ -35,6 +36,7 @@ interface ViewportPanelProps {
|
||||
selection: EditorSelection;
|
||||
toolMode: ToolMode;
|
||||
toolPreview: ViewportToolPreview;
|
||||
transformSession: TransformSessionState;
|
||||
cameraState: ViewportPanelCameraState;
|
||||
focusRequestId: number;
|
||||
focusSelection: EditorSelection;
|
||||
@@ -44,6 +46,9 @@ interface ViewportPanelProps {
|
||||
onCommitCreation(toolPreview: CreationViewportToolPreview): boolean;
|
||||
onCameraStateChange(cameraState: ViewportPanelCameraState): void;
|
||||
onToolPreviewChange(toolPreview: ViewportToolPreview): void;
|
||||
onTransformSessionChange(transformSession: TransformSessionState): void;
|
||||
onTransformCommit(transformSession: ActiveTransformSession): void;
|
||||
onTransformCancel(): void;
|
||||
onSelectionChange(selection: EditorSelection): void;
|
||||
}
|
||||
|
||||
@@ -62,6 +67,7 @@ export function ViewportPanel({
|
||||
selection,
|
||||
toolMode,
|
||||
toolPreview,
|
||||
transformSession,
|
||||
cameraState,
|
||||
focusRequestId,
|
||||
focusSelection,
|
||||
@@ -71,6 +77,9 @@ export function ViewportPanel({
|
||||
onCommitCreation,
|
||||
onCameraStateChange,
|
||||
onToolPreviewChange,
|
||||
onTransformSessionChange,
|
||||
onTransformCommit,
|
||||
onTransformCancel,
|
||||
onSelectionChange
|
||||
}: ViewportPanelProps) {
|
||||
const shouldShow = layoutMode === "quad" || isActive;
|
||||
@@ -131,6 +140,7 @@ export function ViewportPanel({
|
||||
selection={selection}
|
||||
toolMode={toolMode}
|
||||
toolPreview={toolPreview}
|
||||
transformSession={transformSession}
|
||||
cameraState={cameraState}
|
||||
viewMode={panelState.viewMode}
|
||||
displayMode={panelState.displayMode}
|
||||
@@ -142,6 +152,9 @@ export function ViewportPanel({
|
||||
onCommitCreation={onCommitCreation}
|
||||
onCameraStateChange={onCameraStateChange}
|
||||
onToolPreviewChange={onToolPreviewChange}
|
||||
onTransformSessionChange={onTransformSessionChange}
|
||||
onTransformCommit={onTransformCommit}
|
||||
onTransformCancel={onTransformCancel}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1034,13 +1034,17 @@ export class ViewportHost {
|
||||
this.transformGizmoGroup.add(this.createTranslateHandle("y", session.axisConstraint === "y"));
|
||||
this.transformGizmoGroup.add(this.createTranslateHandle("z", session.axisConstraint === "z"));
|
||||
} else if (session.operation === "rotate") {
|
||||
this.transformGizmoGroup.add(this.createRotateHandle("x", effectiveRotationAxis === "x"));
|
||||
this.transformGizmoGroup.add(this.createRotateHandle("y", effectiveRotationAxis === "y"));
|
||||
this.transformGizmoGroup.add(this.createRotateHandle("z", effectiveRotationAxis === "z"));
|
||||
for (const axis of ["x", "y", "z"] as const) {
|
||||
if (!supportsTransformAxisConstraint(session, axis)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.transformGizmoGroup.add(this.createRotateHandle(axis, effectiveRotationAxis === axis));
|
||||
}
|
||||
} else if (session.operation === "scale" && session.target.kind === "modelInstance") {
|
||||
this.transformGizmoGroup.add(this.createScaleHandle("x", session.axisConstraint === "x"));
|
||||
this.transformGizmoGroup.add(this.createScaleHandle("y", session.axisConstraint === "y"));
|
||||
this.transformGizmoGroup.add(this.createScaleHandle("z", session.axisConstraint === "z"));
|
||||
for (const axis of ["x", "y", "z"] as const) {
|
||||
this.transformGizmoGroup.add(this.createScaleHandle(axis, session.axisConstraint === axis));
|
||||
}
|
||||
this.transformGizmoGroup.add(this.createUniformScaleHandle(session.axisConstraint === null));
|
||||
}
|
||||
|
||||
@@ -2191,7 +2195,38 @@ export class ViewportHost {
|
||||
this.advancedRenderingComposer?.setSize(width, height);
|
||||
}
|
||||
|
||||
private pickTransformHandle(event: PointerEvent): { axisConstraint: TransformAxis | null } | null {
|
||||
if (this.currentTransformSession.kind !== "active" || !this.transformGizmoGroup.visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.setPointerFromClientPosition(event.clientX, event.clientY)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.raycaster.setFromCamera(this.pointer, this.getActiveCamera());
|
||||
|
||||
const hits = this.raycaster.intersectObjects(this.transformGizmoGroup.children, true);
|
||||
|
||||
for (const hit of hits) {
|
||||
const axisConstraint = hit.object.userData.transformAxisConstraint;
|
||||
|
||||
if (axisConstraint === null || axisConstraint === "x" || axisConstraint === "y" || axisConstraint === "z") {
|
||||
return {
|
||||
axisConstraint
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private handlePointerDown = (event: PointerEvent) => {
|
||||
this.lastCanvasPointerPosition = {
|
||||
x: event.clientX,
|
||||
y: event.clientY
|
||||
};
|
||||
|
||||
if (event.button === 1) {
|
||||
event.preventDefault();
|
||||
this.activeCameraDragPointerId = event.pointerId;
|
||||
@@ -2207,6 +2242,66 @@ export class ViewportHost {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.currentTransformSession.kind === "active") {
|
||||
if (this.currentTransformSession.sourcePanelId !== this.panelId) {
|
||||
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,
|
||||
axisConstraint: transformHandle.axisConstraint,
|
||||
initialClientPosition: {
|
||||
x: event.clientX,
|
||||
y: event.clientY
|
||||
}
|
||||
};
|
||||
this.renderer.domElement.setPointerCapture(event.pointerId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.currentTransformSession.source !== "gizmo" || this.currentTransformSession.sourcePanelId === this.panelId) {
|
||||
event.preventDefault();
|
||||
this.transformCommitHandler?.(this.currentTransformSession);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.toolMode === "create" && this.creationPreview !== null) {
|
||||
const previewCenter = this.getCreationPreviewCenter(event, this.creationPreview.target);
|
||||
const nextCreationPreview = {
|
||||
@@ -2281,8 +2376,7 @@ export class ViewportHost {
|
||||
} else if (typeof brushId === "string") {
|
||||
const faceMaterialIndex = hit.face?.materialIndex;
|
||||
const faceId = typeof faceMaterialIndex === "number" ? BOX_FACE_IDS[faceMaterialIndex] ?? null : null;
|
||||
// In face-edit mode each face is a distinct candidate; in brush mode collapse to brush.
|
||||
key = faceId !== null ? `brushFace:${brushId}:${faceId}` : `brush:${brushId}`;
|
||||
key = event.altKey && faceId !== null ? `brushFace:${brushId}:${faceId}` : `brush:${brushId}`;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
@@ -2343,7 +2437,7 @@ export class ViewportHost {
|
||||
return;
|
||||
}
|
||||
|
||||
if (faceId !== null) {
|
||||
if (event.altKey && faceId !== null) {
|
||||
this.brushSelectionChangeHandler?.({ kind: "brushFace", brushId, faceId });
|
||||
return;
|
||||
}
|
||||
@@ -2352,6 +2446,11 @@ export class ViewportHost {
|
||||
};
|
||||
|
||||
private handlePointerMove = (event: PointerEvent) => {
|
||||
this.lastCanvasPointerPosition = {
|
||||
x: event.clientX,
|
||||
y: event.clientY
|
||||
};
|
||||
|
||||
if (this.activeCameraDragPointerId === event.pointerId && this.lastCameraDragClientPosition !== null) {
|
||||
const deltaX = event.clientX - this.lastCameraDragClientPosition.x;
|
||||
const deltaY = event.clientY - this.lastCameraDragClientPosition.y;
|
||||
@@ -2370,6 +2469,56 @@ export class ViewportHost {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this.activeTransformDrag !== null &&
|
||||
this.activeTransformDrag.pointerId === event.pointerId &&
|
||||
this.currentTransformSession.kind === "active" &&
|
||||
this.currentTransformSession.id === this.activeTransformDrag.sessionId
|
||||
) {
|
||||
const nextSession = this.buildTransformPreviewFromPointer(
|
||||
this.currentTransformSession,
|
||||
this.activeTransformDrag.initialClientPosition,
|
||||
{
|
||||
x: event.clientX,
|
||||
y: event.clientY
|
||||
},
|
||||
this.activeTransformDrag.axisConstraint
|
||||
);
|
||||
|
||||
this.currentTransformSession = nextSession;
|
||||
this.applyTransformPreview();
|
||||
this.syncTransformGizmo();
|
||||
this.transformSessionChangeHandler?.(nextSession);
|
||||
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;
|
||||
}
|
||||
@@ -2385,6 +2534,25 @@ export class ViewportHost {
|
||||
};
|
||||
|
||||
private handlePointerUp = (event: PointerEvent) => {
|
||||
if (this.activeTransformDrag !== null && this.activeTransformDrag.pointerId === event.pointerId) {
|
||||
if (this.renderer.domElement.hasPointerCapture(event.pointerId)) {
|
||||
this.renderer.domElement.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
|
||||
const completedSession = this.currentTransformSession.kind === "active" ? this.currentTransformSession : null;
|
||||
this.activeTransformDrag = null;
|
||||
|
||||
if (completedSession !== null) {
|
||||
if (event.type === "pointercancel") {
|
||||
this.transformCancelHandler?.();
|
||||
} else {
|
||||
this.transformCommitHandler?.(completedSession);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.activeCameraDragPointerId !== event.pointerId) {
|
||||
return;
|
||||
}
|
||||
@@ -2799,6 +2967,7 @@ export class ViewportHost {
|
||||
|
||||
private render = () => {
|
||||
this.animationFrame = window.requestAnimationFrame(this.render);
|
||||
this.updateTransformGizmoPose();
|
||||
|
||||
if (this.advancedRenderingComposer !== null) {
|
||||
this.advancedRenderingComposer.render();
|
||||
|
||||
Reference in New Issue
Block a user