auto-git:
[change] src/app/App.tsx [change] src/app/editor-store.ts [change] src/core/transform-session.ts [change] src/viewport-three/ViewportCanvas.tsx [change] src/viewport-three/ViewportPanel.tsx [change] src/viewport-three/viewport-host.ts [change] tests/unit/viewport-canvas.test.tsx
This commit is contained in:
8283
src/app/App.tsx
8283
src/app/App.tsx
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,10 @@ import {
|
|||||||
type TransformAxisSpace,
|
type TransformAxisSpace,
|
||||||
type TransformSessionState
|
type TransformSessionState
|
||||||
} from "../core/transform-session";
|
} from "../core/transform-session";
|
||||||
import { createEmptySceneDocument, type SceneDocument } from "../document/scene-document";
|
import {
|
||||||
|
createEmptySceneDocument,
|
||||||
|
type SceneDocument
|
||||||
|
} from "../document/scene-document";
|
||||||
import {
|
import {
|
||||||
DEFAULT_SCENE_DRAFT_STORAGE_KEY,
|
DEFAULT_SCENE_DRAFT_STORAGE_KEY,
|
||||||
type LoadSceneDocumentDraftResult,
|
type LoadSceneDocumentDraftResult,
|
||||||
@@ -24,7 +27,10 @@ import {
|
|||||||
type SaveSceneDocumentDraftResult,
|
type SaveSceneDocumentDraftResult,
|
||||||
saveSceneDocumentDraft
|
saveSceneDocumentDraft
|
||||||
} from "../serialization/local-draft-storage";
|
} from "../serialization/local-draft-storage";
|
||||||
import { parseSceneDocumentJson, serializeSceneDocument } from "../serialization/scene-document-json";
|
import {
|
||||||
|
parseSceneDocumentJson,
|
||||||
|
serializeSceneDocument
|
||||||
|
} from "../serialization/scene-document-json";
|
||||||
import type { ViewportViewMode } from "../viewport-three/viewport-view-modes";
|
import type { ViewportViewMode } from "../viewport-three/viewport-view-modes";
|
||||||
import {
|
import {
|
||||||
areViewportToolPreviewsEqual,
|
areViewportToolPreviewsEqual,
|
||||||
@@ -110,7 +116,9 @@ export class EditorStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
constructor(options: EditorStoreOptions = {}) {
|
constructor(options: EditorStoreOptions = {}) {
|
||||||
const initialViewportLayoutState = cloneViewportLayoutState(options.initialViewportLayoutState ?? createDefaultViewportLayoutState());
|
const initialViewportLayoutState = cloneViewportLayoutState(
|
||||||
|
options.initialViewportLayoutState ?? createDefaultViewportLayoutState()
|
||||||
|
);
|
||||||
|
|
||||||
this.document = options.initialDocument ?? createEmptySceneDocument();
|
this.document = options.initialDocument ?? createEmptySceneDocument();
|
||||||
this.viewportLayoutMode = initialViewportLayoutState.layoutMode;
|
this.viewportLayoutMode = initialViewportLayoutState.layoutMode;
|
||||||
@@ -143,14 +151,22 @@ export class EditorStore {
|
|||||||
|
|
||||||
this.toolMode = toolMode;
|
this.toolMode = toolMode;
|
||||||
|
|
||||||
if (!isViewportToolPreviewCompatible(toolMode, this.viewportTransientState.toolPreview)) {
|
if (
|
||||||
|
!isViewportToolPreviewCompatible(
|
||||||
|
toolMode,
|
||||||
|
this.viewportTransientState.toolPreview
|
||||||
|
)
|
||||||
|
) {
|
||||||
this.viewportTransientState = {
|
this.viewportTransientState = {
|
||||||
...this.viewportTransientState,
|
...this.viewportTransientState,
|
||||||
toolPreview: createDefaultViewportTransientState().toolPreview
|
toolPreview: createDefaultViewportTransientState().toolPreview
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolMode !== "select" && this.viewportTransientState.transformSession.kind !== "none") {
|
if (
|
||||||
|
toolMode !== "select" &&
|
||||||
|
this.viewportTransientState.transformSession.kind !== "none"
|
||||||
|
) {
|
||||||
this.viewportTransientState = {
|
this.viewportTransientState = {
|
||||||
...this.viewportTransientState,
|
...this.viewportTransientState,
|
||||||
transformSession: createInactiveTransformSession()
|
transformSession: createInactiveTransformSession()
|
||||||
@@ -178,7 +194,10 @@ export class EditorStore {
|
|||||||
this.emit();
|
this.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
setViewportPanelViewMode(panelId: ViewportPanelId, viewMode: ViewportViewMode) {
|
setViewportPanelViewMode(
|
||||||
|
panelId: ViewportPanelId,
|
||||||
|
viewMode: ViewportViewMode
|
||||||
|
) {
|
||||||
if (this.viewportPanels[panelId].viewMode === viewMode) {
|
if (this.viewportPanels[panelId].viewMode === viewMode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -193,7 +212,10 @@ export class EditorStore {
|
|||||||
this.emit();
|
this.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
setViewportPanelDisplayMode(panelId: ViewportPanelId, displayMode: ViewportDisplayMode) {
|
setViewportPanelDisplayMode(
|
||||||
|
panelId: ViewportPanelId,
|
||||||
|
displayMode: ViewportDisplayMode
|
||||||
|
) {
|
||||||
if (this.viewportPanels[panelId].displayMode === displayMode) {
|
if (this.viewportPanels[panelId].displayMode === displayMode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -208,8 +230,16 @@ export class EditorStore {
|
|||||||
this.emit();
|
this.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
setViewportPanelCameraState(panelId: ViewportPanelId, cameraState: ViewportPanelCameraState) {
|
setViewportPanelCameraState(
|
||||||
if (areViewportPanelCameraStatesEqual(this.viewportPanels[panelId].cameraState, cameraState)) {
|
panelId: ViewportPanelId,
|
||||||
|
cameraState: ViewportPanelCameraState
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
areViewportPanelCameraStatesEqual(
|
||||||
|
this.viewportPanels[panelId].cameraState,
|
||||||
|
cameraState
|
||||||
|
)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,7 +254,10 @@ export class EditorStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setViewportQuadSplit(viewportQuadSplit: ViewportQuadSplit) {
|
setViewportQuadSplit(viewportQuadSplit: ViewportQuadSplit) {
|
||||||
if (this.viewportQuadSplit.x === viewportQuadSplit.x && this.viewportQuadSplit.y === viewportQuadSplit.y) {
|
if (
|
||||||
|
this.viewportQuadSplit.x === viewportQuadSplit.x &&
|
||||||
|
this.viewportQuadSplit.y === viewportQuadSplit.y
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +271,12 @@ export class EditorStore {
|
|||||||
setViewportToolPreview(toolPreview: ViewportToolPreview) {
|
setViewportToolPreview(toolPreview: ViewportToolPreview) {
|
||||||
const nextToolPreview = cloneViewportToolPreview(toolPreview);
|
const nextToolPreview = cloneViewportToolPreview(toolPreview);
|
||||||
|
|
||||||
if (areViewportToolPreviewsEqual(this.viewportTransientState.toolPreview, nextToolPreview)) {
|
if (
|
||||||
|
areViewportToolPreviewsEqual(
|
||||||
|
this.viewportTransientState.toolPreview,
|
||||||
|
nextToolPreview
|
||||||
|
)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +294,10 @@ export class EditorStore {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sourcePanelId !== undefined && currentToolPreview.sourcePanelId !== sourcePanelId) {
|
if (
|
||||||
|
sourcePanelId !== undefined &&
|
||||||
|
currentToolPreview.sourcePanelId !== sourcePanelId
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,7 +311,12 @@ export class EditorStore {
|
|||||||
setTransformSession(transformSession: TransformSessionState) {
|
setTransformSession(transformSession: TransformSessionState) {
|
||||||
const nextTransformSession = cloneTransformSession(transformSession);
|
const nextTransformSession = cloneTransformSession(transformSession);
|
||||||
|
|
||||||
if (areTransformSessionsEqual(this.viewportTransientState.transformSession, nextTransformSession)) {
|
if (
|
||||||
|
areTransformSessionsEqual(
|
||||||
|
this.viewportTransientState.transformSession,
|
||||||
|
nextTransformSession
|
||||||
|
)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,14 +339,19 @@ export class EditorStore {
|
|||||||
this.emit();
|
this.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
setTransformAxisConstraint(axisConstraint: TransformAxis | null, axisConstraintSpace: TransformAxisSpace = "world") {
|
setTransformAxisConstraint(
|
||||||
|
axisConstraint: TransformAxis | null,
|
||||||
|
axisConstraintSpace: TransformAxisSpace = "world"
|
||||||
|
) {
|
||||||
if (this.viewportTransientState.transformSession.kind !== "active") {
|
if (this.viewportTransientState.transformSession.kind !== "active") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.viewportTransientState.transformSession.axisConstraint === axisConstraint &&
|
this.viewportTransientState.transformSession.axisConstraint ===
|
||||||
this.viewportTransientState.transformSession.axisConstraintSpace === axisConstraintSpace
|
axisConstraint &&
|
||||||
|
this.viewportTransientState.transformSession.axisConstraintSpace ===
|
||||||
|
axisConstraintSpace
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -308,7 +359,9 @@ export class EditorStore {
|
|||||||
this.viewportTransientState = {
|
this.viewportTransientState = {
|
||||||
...this.viewportTransientState,
|
...this.viewportTransientState,
|
||||||
transformSession: {
|
transformSession: {
|
||||||
...(cloneTransformSession(this.viewportTransientState.transformSession) as Extract<TransformSessionState, { kind: "active" }>),
|
...(cloneTransformSession(
|
||||||
|
this.viewportTransientState.transformSession
|
||||||
|
) as Extract<TransformSessionState, { kind: "active" }>),
|
||||||
axisConstraint,
|
axisConstraint,
|
||||||
axisConstraintSpace
|
axisConstraintSpace
|
||||||
}
|
}
|
||||||
@@ -345,7 +398,10 @@ export class EditorStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setSelection(selection: EditorSelection) {
|
setSelection(selection: EditorSelection) {
|
||||||
if (this.viewportTransientState.transformSession.kind === "active" && !areEditorSelectionsEqual(this.selection, selection)) {
|
if (
|
||||||
|
this.viewportTransientState.transformSession.kind === "active" &&
|
||||||
|
!areEditorSelectionsEqual(this.selection, selection)
|
||||||
|
) {
|
||||||
this.viewportTransientState = {
|
this.viewportTransientState = {
|
||||||
...this.viewportTransientState,
|
...this.viewportTransientState,
|
||||||
transformSession: createInactiveTransformSession()
|
transformSession: createInactiveTransformSession()
|
||||||
@@ -369,7 +425,10 @@ export class EditorStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.whiteboxSelectionMode = mode;
|
this.whiteboxSelectionMode = mode;
|
||||||
this.selection = normalizeSelectionForWhiteboxSelectionMode(this.selection, mode);
|
this.selection = normalizeSelectionForWhiteboxSelectionMode(
|
||||||
|
this.selection,
|
||||||
|
mode
|
||||||
|
);
|
||||||
this.emit();
|
this.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,7 +521,12 @@ export class EditorStore {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return saveSceneDocumentDraft(this.storage, this.document, this.createViewportLayoutState(), this.storageKey);
|
return saveSceneDocumentDraft(
|
||||||
|
this.storage,
|
||||||
|
this.document,
|
||||||
|
this.createViewportLayoutState(),
|
||||||
|
this.storageKey
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadDraft(): EditorDraftLoadResult {
|
loadDraft(): EditorDraftLoadResult {
|
||||||
@@ -517,7 +581,8 @@ export class EditorStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private applyViewportLayoutState(viewportLayoutState: ViewportLayoutState) {
|
private applyViewportLayoutState(viewportLayoutState: ViewportLayoutState) {
|
||||||
const nextViewportLayoutState = cloneViewportLayoutState(viewportLayoutState);
|
const nextViewportLayoutState =
|
||||||
|
cloneViewportLayoutState(viewportLayoutState);
|
||||||
|
|
||||||
this.viewportLayoutMode = nextViewportLayoutState.layoutMode;
|
this.viewportLayoutMode = nextViewportLayoutState.layoutMode;
|
||||||
this.activeViewportPanelId = nextViewportLayoutState.activePanelId;
|
this.activeViewportPanelId = nextViewportLayoutState.activePanelId;
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ import {
|
|||||||
type EntityInstance,
|
type EntityInstance,
|
||||||
type EntityKind
|
type EntityKind
|
||||||
} from "../entities/entity-instances";
|
} from "../entities/entity-instances";
|
||||||
import { cloneModelInstance, getModelInstanceKindLabel } from "../assets/model-instances";
|
import {
|
||||||
|
cloneModelInstance,
|
||||||
|
getModelInstanceKindLabel
|
||||||
|
} from "../assets/model-instances";
|
||||||
import type { ViewportPanelId } from "../viewport-three/viewport-layout";
|
import type { ViewportPanelId } from "../viewport-three/viewport-layout";
|
||||||
|
|
||||||
export type TransformOperation = "translate" | "rotate" | "scale";
|
export type TransformOperation = "translate" | "rotate" | "scale";
|
||||||
@@ -42,7 +45,10 @@ export interface NoEntityRotationState {
|
|||||||
kind: "none";
|
kind: "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EntityTransformRotationState = NoEntityRotationState | YawEntityRotationState | DirectionEntityRotationState;
|
export type EntityTransformRotationState =
|
||||||
|
| NoEntityRotationState
|
||||||
|
| YawEntityRotationState
|
||||||
|
| DirectionEntityRotationState;
|
||||||
|
|
||||||
export interface BrushTransformTarget {
|
export interface BrushTransformTarget {
|
||||||
kind: "brush";
|
kind: "brush";
|
||||||
@@ -116,7 +122,10 @@ export interface BrushTransformPreview {
|
|||||||
geometry: BoxBrushGeometry;
|
geometry: BoxBrushGeometry;
|
||||||
}
|
}
|
||||||
|
|
||||||
function areBrushGeometriesEqual(left: BoxBrushGeometry, right: BoxBrushGeometry): boolean {
|
function areBrushGeometriesEqual(
|
||||||
|
left: BoxBrushGeometry,
|
||||||
|
right: BoxBrushGeometry
|
||||||
|
): boolean {
|
||||||
return BOX_VERTEX_IDS.every((vertexId) => {
|
return BOX_VERTEX_IDS.every((vertexId) => {
|
||||||
const leftVertex = left.vertices[vertexId];
|
const leftVertex = left.vertices[vertexId];
|
||||||
const rightVertex = right.vertices[vertexId];
|
const rightVertex = right.vertices[vertexId];
|
||||||
@@ -137,7 +146,10 @@ export interface EntityTransformPreview {
|
|||||||
rotation: EntityTransformRotationState;
|
rotation: EntityTransformRotationState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TransformPreview = BrushTransformPreview | ModelInstanceTransformPreview | EntityTransformPreview;
|
export type TransformPreview =
|
||||||
|
| BrushTransformPreview
|
||||||
|
| ModelInstanceTransformPreview
|
||||||
|
| EntityTransformPreview;
|
||||||
|
|
||||||
export interface ActiveTransformSession {
|
export interface ActiveTransformSession {
|
||||||
kind: "active";
|
kind: "active";
|
||||||
@@ -170,7 +182,9 @@ function areVec3Equal(left: Vec3, right: Vec3): boolean {
|
|||||||
return left.x === right.x && left.y === right.y && left.z === right.z;
|
return left.x === right.x && left.y === right.y && left.z === right.z;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneEntityTransformRotationState(rotation: EntityTransformRotationState): EntityTransformRotationState {
|
function cloneEntityTransformRotationState(
|
||||||
|
rotation: EntityTransformRotationState
|
||||||
|
): EntityTransformRotationState {
|
||||||
switch (rotation.kind) {
|
switch (rotation.kind) {
|
||||||
case "none":
|
case "none":
|
||||||
return {
|
return {
|
||||||
@@ -189,7 +203,10 @@ function cloneEntityTransformRotationState(rotation: EntityTransformRotationStat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function areEntityTransformRotationsEqual(left: EntityTransformRotationState, right: EntityTransformRotationState): boolean {
|
function areEntityTransformRotationsEqual(
|
||||||
|
left: EntityTransformRotationState,
|
||||||
|
right: EntityTransformRotationState
|
||||||
|
): boolean {
|
||||||
if (left.kind !== right.kind) {
|
if (left.kind !== right.kind) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -200,7 +217,10 @@ function areEntityTransformRotationsEqual(left: EntityTransformRotationState, ri
|
|||||||
case "yaw":
|
case "yaw":
|
||||||
return right.kind === "yaw" && left.yawDegrees === right.yawDegrees;
|
return right.kind === "yaw" && left.yawDegrees === right.yawDegrees;
|
||||||
case "direction":
|
case "direction":
|
||||||
return right.kind === "direction" && areVec3Equal(left.direction, right.direction);
|
return (
|
||||||
|
right.kind === "direction" &&
|
||||||
|
areVec3Equal(left.direction, right.direction)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,12 +286,16 @@ export function cloneTransformTarget(target: TransformTarget): TransformTarget {
|
|||||||
entityId: target.entityId,
|
entityId: target.entityId,
|
||||||
entityKind: target.entityKind,
|
entityKind: target.entityKind,
|
||||||
initialPosition: cloneVec3(target.initialPosition),
|
initialPosition: cloneVec3(target.initialPosition),
|
||||||
initialRotation: cloneEntityTransformRotationState(target.initialRotation)
|
initialRotation: cloneEntityTransformRotationState(
|
||||||
|
target.initialRotation
|
||||||
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cloneTransformPreview(preview: TransformPreview): TransformPreview {
|
export function cloneTransformPreview(
|
||||||
|
preview: TransformPreview
|
||||||
|
): TransformPreview {
|
||||||
switch (preview.kind) {
|
switch (preview.kind) {
|
||||||
case "brush":
|
case "brush":
|
||||||
return {
|
return {
|
||||||
@@ -297,7 +321,9 @@ export function cloneTransformPreview(preview: TransformPreview): TransformPrevi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cloneTransformSession(session: TransformSessionState): TransformSessionState {
|
export function cloneTransformSession(
|
||||||
|
session: TransformSessionState
|
||||||
|
): TransformSessionState {
|
||||||
if (session.kind === "none") {
|
if (session.kind === "none") {
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
@@ -315,7 +341,10 @@ export function cloneTransformSession(session: TransformSessionState): Transform
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function areTransformSessionsEqual(left: TransformSessionState, right: TransformSessionState): boolean {
|
export function areTransformSessionsEqual(
|
||||||
|
left: TransformSessionState,
|
||||||
|
right: TransformSessionState
|
||||||
|
): boolean {
|
||||||
if (left.kind !== right.kind) {
|
if (left.kind !== right.kind) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -336,7 +365,10 @@ export function areTransformSessionsEqual(left: TransformSessionState, right: Tr
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function areTransformTargetsEqual(left: TransformTarget, right: TransformTarget): boolean {
|
function areTransformTargetsEqual(
|
||||||
|
left: TransformTarget,
|
||||||
|
right: TransformTarget
|
||||||
|
): boolean {
|
||||||
if (left.kind !== right.kind) {
|
if (left.kind !== right.kind) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -347,7 +379,10 @@ function areTransformTargetsEqual(left: TransformTarget, right: TransformTarget)
|
|||||||
right.kind === "brush" &&
|
right.kind === "brush" &&
|
||||||
left.brushId === right.brushId &&
|
left.brushId === right.brushId &&
|
||||||
areVec3Equal(left.initialCenter, right.initialCenter) &&
|
areVec3Equal(left.initialCenter, right.initialCenter) &&
|
||||||
areVec3Equal(left.initialRotationDegrees, right.initialRotationDegrees) &&
|
areVec3Equal(
|
||||||
|
left.initialRotationDegrees,
|
||||||
|
right.initialRotationDegrees
|
||||||
|
) &&
|
||||||
areVec3Equal(left.initialSize, right.initialSize) &&
|
areVec3Equal(left.initialSize, right.initialSize) &&
|
||||||
areBrushGeometriesEqual(left.initialGeometry, right.initialGeometry)
|
areBrushGeometriesEqual(left.initialGeometry, right.initialGeometry)
|
||||||
);
|
);
|
||||||
@@ -357,7 +392,10 @@ function areTransformTargetsEqual(left: TransformTarget, right: TransformTarget)
|
|||||||
left.brushId === right.brushId &&
|
left.brushId === right.brushId &&
|
||||||
left.faceId === right.faceId &&
|
left.faceId === right.faceId &&
|
||||||
areVec3Equal(left.initialCenter, right.initialCenter) &&
|
areVec3Equal(left.initialCenter, right.initialCenter) &&
|
||||||
areVec3Equal(left.initialRotationDegrees, right.initialRotationDegrees) &&
|
areVec3Equal(
|
||||||
|
left.initialRotationDegrees,
|
||||||
|
right.initialRotationDegrees
|
||||||
|
) &&
|
||||||
areVec3Equal(left.initialSize, right.initialSize) &&
|
areVec3Equal(left.initialSize, right.initialSize) &&
|
||||||
areBrushGeometriesEqual(left.initialGeometry, right.initialGeometry)
|
areBrushGeometriesEqual(left.initialGeometry, right.initialGeometry)
|
||||||
);
|
);
|
||||||
@@ -367,7 +405,10 @@ function areTransformTargetsEqual(left: TransformTarget, right: TransformTarget)
|
|||||||
left.brushId === right.brushId &&
|
left.brushId === right.brushId &&
|
||||||
left.edgeId === right.edgeId &&
|
left.edgeId === right.edgeId &&
|
||||||
areVec3Equal(left.initialCenter, right.initialCenter) &&
|
areVec3Equal(left.initialCenter, right.initialCenter) &&
|
||||||
areVec3Equal(left.initialRotationDegrees, right.initialRotationDegrees) &&
|
areVec3Equal(
|
||||||
|
left.initialRotationDegrees,
|
||||||
|
right.initialRotationDegrees
|
||||||
|
) &&
|
||||||
areVec3Equal(left.initialSize, right.initialSize) &&
|
areVec3Equal(left.initialSize, right.initialSize) &&
|
||||||
areBrushGeometriesEqual(left.initialGeometry, right.initialGeometry)
|
areBrushGeometriesEqual(left.initialGeometry, right.initialGeometry)
|
||||||
);
|
);
|
||||||
@@ -377,7 +418,10 @@ function areTransformTargetsEqual(left: TransformTarget, right: TransformTarget)
|
|||||||
left.brushId === right.brushId &&
|
left.brushId === right.brushId &&
|
||||||
left.vertexId === right.vertexId &&
|
left.vertexId === right.vertexId &&
|
||||||
areVec3Equal(left.initialCenter, right.initialCenter) &&
|
areVec3Equal(left.initialCenter, right.initialCenter) &&
|
||||||
areVec3Equal(left.initialRotationDegrees, right.initialRotationDegrees) &&
|
areVec3Equal(
|
||||||
|
left.initialRotationDegrees,
|
||||||
|
right.initialRotationDegrees
|
||||||
|
) &&
|
||||||
areVec3Equal(left.initialSize, right.initialSize) &&
|
areVec3Equal(left.initialSize, right.initialSize) &&
|
||||||
areBrushGeometriesEqual(left.initialGeometry, right.initialGeometry)
|
areBrushGeometriesEqual(left.initialGeometry, right.initialGeometry)
|
||||||
);
|
);
|
||||||
@@ -387,7 +431,10 @@ function areTransformTargetsEqual(left: TransformTarget, right: TransformTarget)
|
|||||||
left.modelInstanceId === right.modelInstanceId &&
|
left.modelInstanceId === right.modelInstanceId &&
|
||||||
left.assetId === right.assetId &&
|
left.assetId === right.assetId &&
|
||||||
areVec3Equal(left.initialPosition, right.initialPosition) &&
|
areVec3Equal(left.initialPosition, right.initialPosition) &&
|
||||||
areVec3Equal(left.initialRotationDegrees, right.initialRotationDegrees) &&
|
areVec3Equal(
|
||||||
|
left.initialRotationDegrees,
|
||||||
|
right.initialRotationDegrees
|
||||||
|
) &&
|
||||||
areVec3Equal(left.initialScale, right.initialScale)
|
areVec3Equal(left.initialScale, right.initialScale)
|
||||||
);
|
);
|
||||||
case "entity":
|
case "entity":
|
||||||
@@ -396,12 +443,18 @@ function areTransformTargetsEqual(left: TransformTarget, right: TransformTarget)
|
|||||||
left.entityId === right.entityId &&
|
left.entityId === right.entityId &&
|
||||||
left.entityKind === right.entityKind &&
|
left.entityKind === right.entityKind &&
|
||||||
areVec3Equal(left.initialPosition, right.initialPosition) &&
|
areVec3Equal(left.initialPosition, right.initialPosition) &&
|
||||||
areEntityTransformRotationsEqual(left.initialRotation, right.initialRotation)
|
areEntityTransformRotationsEqual(
|
||||||
|
left.initialRotation,
|
||||||
|
right.initialRotation
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function areTransformPreviewsEqual(left: TransformPreview, right: TransformPreview): boolean {
|
function areTransformPreviewsEqual(
|
||||||
|
left: TransformPreview,
|
||||||
|
right: TransformPreview
|
||||||
|
): boolean {
|
||||||
if (left.kind !== right.kind) {
|
if (left.kind !== right.kind) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -423,7 +476,11 @@ function areTransformPreviewsEqual(left: TransformPreview, right: TransformPrevi
|
|||||||
areVec3Equal(left.scale, right.scale)
|
areVec3Equal(left.scale, right.scale)
|
||||||
);
|
);
|
||||||
case "entity":
|
case "entity":
|
||||||
return right.kind === "entity" && areVec3Equal(left.position, right.position) && areEntityTransformRotationsEqual(left.rotation, right.rotation);
|
return (
|
||||||
|
right.kind === "entity" &&
|
||||||
|
areVec3Equal(left.position, right.position) &&
|
||||||
|
areEntityTransformRotationsEqual(left.rotation, right.rotation)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,7 +505,9 @@ export function createTransformSession(options: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createTransformPreviewFromTarget(target: TransformTarget): TransformPreview {
|
export function createTransformPreviewFromTarget(
|
||||||
|
target: TransformTarget
|
||||||
|
): TransformPreview {
|
||||||
switch (target.kind) {
|
switch (target.kind) {
|
||||||
case "brush":
|
case "brush":
|
||||||
case "brushFace":
|
case "brushFace":
|
||||||
@@ -477,7 +536,9 @@ export function createTransformPreviewFromTarget(target: TransformTarget): Trans
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doesTransformSessionChangeTarget(session: ActiveTransformSession): boolean {
|
export function doesTransformSessionChangeTarget(
|
||||||
|
session: ActiveTransformSession
|
||||||
|
): boolean {
|
||||||
switch (session.target.kind) {
|
switch (session.target.kind) {
|
||||||
case "brush":
|
case "brush":
|
||||||
case "brushFace":
|
case "brushFace":
|
||||||
@@ -486,27 +547,47 @@ export function doesTransformSessionChangeTarget(session: ActiveTransformSession
|
|||||||
return (
|
return (
|
||||||
session.preview.kind === "brush" &&
|
session.preview.kind === "brush" &&
|
||||||
(!areVec3Equal(session.preview.center, session.target.initialCenter) ||
|
(!areVec3Equal(session.preview.center, session.target.initialCenter) ||
|
||||||
!areVec3Equal(session.preview.rotationDegrees, session.target.initialRotationDegrees) ||
|
!areVec3Equal(
|
||||||
|
session.preview.rotationDegrees,
|
||||||
|
session.target.initialRotationDegrees
|
||||||
|
) ||
|
||||||
!areVec3Equal(session.preview.size, session.target.initialSize) ||
|
!areVec3Equal(session.preview.size, session.target.initialSize) ||
|
||||||
!areBrushGeometriesEqual(session.preview.geometry, session.target.initialGeometry))
|
!areBrushGeometriesEqual(
|
||||||
|
session.preview.geometry,
|
||||||
|
session.target.initialGeometry
|
||||||
|
))
|
||||||
);
|
);
|
||||||
case "modelInstance":
|
case "modelInstance":
|
||||||
return (
|
return (
|
||||||
session.preview.kind === "modelInstance" &&
|
session.preview.kind === "modelInstance" &&
|
||||||
(!areVec3Equal(session.preview.position, session.target.initialPosition) ||
|
(!areVec3Equal(
|
||||||
!areVec3Equal(session.preview.rotationDegrees, session.target.initialRotationDegrees) ||
|
session.preview.position,
|
||||||
|
session.target.initialPosition
|
||||||
|
) ||
|
||||||
|
!areVec3Equal(
|
||||||
|
session.preview.rotationDegrees,
|
||||||
|
session.target.initialRotationDegrees
|
||||||
|
) ||
|
||||||
!areVec3Equal(session.preview.scale, session.target.initialScale))
|
!areVec3Equal(session.preview.scale, session.target.initialScale))
|
||||||
);
|
);
|
||||||
case "entity":
|
case "entity":
|
||||||
return (
|
return (
|
||||||
session.preview.kind === "entity" &&
|
session.preview.kind === "entity" &&
|
||||||
(!areVec3Equal(session.preview.position, session.target.initialPosition) ||
|
(!areVec3Equal(
|
||||||
!areEntityTransformRotationsEqual(session.preview.rotation, session.target.initialRotation))
|
session.preview.position,
|
||||||
|
session.target.initialPosition
|
||||||
|
) ||
|
||||||
|
!areEntityTransformRotationsEqual(
|
||||||
|
session.preview.rotation,
|
||||||
|
session.target.initialRotation
|
||||||
|
))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTransformOperationLabel(operation: TransformOperation): string {
|
export function getTransformOperationLabel(
|
||||||
|
operation: TransformOperation
|
||||||
|
): string {
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
case "translate":
|
case "translate":
|
||||||
return "Move";
|
return "Move";
|
||||||
@@ -521,7 +602,9 @@ export function getTransformAxisLabel(axis: TransformAxis): string {
|
|||||||
return axis.toUpperCase();
|
return axis.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTransformAxisSpaceLabel(axisSpace: TransformAxisSpace): string {
|
export function getTransformAxisSpaceLabel(
|
||||||
|
axisSpace: TransformAxisSpace
|
||||||
|
): string {
|
||||||
switch (axisSpace) {
|
switch (axisSpace) {
|
||||||
case "world":
|
case "world":
|
||||||
return "World";
|
return "World";
|
||||||
@@ -547,7 +630,9 @@ export function getTransformTargetLabel(target: TransformTarget): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSupportedTransformOperations(target: TransformTarget): TransformOperation[] {
|
export function getSupportedTransformOperations(
|
||||||
|
target: TransformTarget
|
||||||
|
): TransformOperation[] {
|
||||||
switch (target.kind) {
|
switch (target.kind) {
|
||||||
case "brush":
|
case "brush":
|
||||||
case "brushFace":
|
case "brushFace":
|
||||||
@@ -558,25 +643,43 @@ export function getSupportedTransformOperations(target: TransformTarget): Transf
|
|||||||
case "modelInstance":
|
case "modelInstance":
|
||||||
return ["translate", "rotate", "scale"];
|
return ["translate", "rotate", "scale"];
|
||||||
case "entity":
|
case "entity":
|
||||||
return target.initialRotation.kind === "none" ? ["translate"] : ["translate", "rotate"];
|
return target.initialRotation.kind === "none"
|
||||||
|
? ["translate"]
|
||||||
|
: ["translate", "rotate"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function supportsTransformOperation(target: TransformTarget, operation: TransformOperation): boolean {
|
export function supportsTransformOperation(
|
||||||
|
target: TransformTarget,
|
||||||
|
operation: TransformOperation
|
||||||
|
): boolean {
|
||||||
return getSupportedTransformOperations(target).includes(operation);
|
return getSupportedTransformOperations(target).includes(operation);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function supportsTransformAxisConstraint(session: ActiveTransformSession, axis: TransformAxis): boolean {
|
export function supportsTransformAxisConstraint(
|
||||||
|
session: ActiveTransformSession,
|
||||||
|
axis: TransformAxis
|
||||||
|
): boolean {
|
||||||
switch (session.operation) {
|
switch (session.operation) {
|
||||||
case "translate":
|
case "translate":
|
||||||
return true;
|
return true;
|
||||||
case "scale":
|
case "scale":
|
||||||
if (session.target.kind === "modelInstance" || session.target.kind === "brush" || session.target.kind === "brushVertex") {
|
if (
|
||||||
|
session.target.kind === "modelInstance" ||
|
||||||
|
session.target.kind === "brush" ||
|
||||||
|
session.target.kind === "brushVertex"
|
||||||
|
) {
|
||||||
return session.target.kind !== "brushVertex";
|
return session.target.kind !== "brushVertex";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.target.kind === "brushFace") {
|
if (session.target.kind === "brushFace") {
|
||||||
const normalAxis = session.target.faceId === "posX" || session.target.faceId === "negX" ? "x" : session.target.faceId === "posY" || session.target.faceId === "negY" ? "y" : "z";
|
const normalAxis =
|
||||||
|
session.target.faceId === "posX" || session.target.faceId === "negX"
|
||||||
|
? "x"
|
||||||
|
: session.target.faceId === "posY" ||
|
||||||
|
session.target.faceId === "negY"
|
||||||
|
? "y"
|
||||||
|
: "z";
|
||||||
return axis === normalAxis;
|
return axis === normalAxis;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,12 +697,21 @@ export function supportsTransformAxisConstraint(session: ActiveTransformSession,
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
case "rotate":
|
case "rotate":
|
||||||
if (session.target.kind === "entity" && session.target.initialRotation.kind === "yaw") {
|
if (
|
||||||
|
session.target.kind === "entity" &&
|
||||||
|
session.target.initialRotation.kind === "yaw"
|
||||||
|
) {
|
||||||
return axis === "y";
|
return axis === "y";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.target.kind === "brushFace") {
|
if (session.target.kind === "brushFace") {
|
||||||
const normalAxis = session.target.faceId === "posX" || session.target.faceId === "negX" ? "x" : session.target.faceId === "posY" || session.target.faceId === "negY" ? "y" : "z";
|
const normalAxis =
|
||||||
|
session.target.faceId === "posX" || session.target.faceId === "negX"
|
||||||
|
? "x"
|
||||||
|
: session.target.faceId === "posY" ||
|
||||||
|
session.target.faceId === "negY"
|
||||||
|
? "y"
|
||||||
|
: "z";
|
||||||
return axis === normalAxis;
|
return axis === normalAxis;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -623,7 +735,10 @@ export function supportsTransformAxisConstraint(session: ActiveTransformSession,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function supportsLocalTransformAxisConstraint(session: ActiveTransformSession, axis: TransformAxis): boolean {
|
export function supportsLocalTransformAxisConstraint(
|
||||||
|
session: ActiveTransformSession,
|
||||||
|
axis: TransformAxis
|
||||||
|
): boolean {
|
||||||
if (!supportsTransformAxisConstraint(session, axis)) {
|
if (!supportsTransformAxisConstraint(session, axis)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -645,7 +760,9 @@ export function supportsLocalTransformAxisConstraint(session: ActiveTransformSes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveEntityRotation(entity: EntityInstance): EntityTransformRotationState {
|
function resolveEntityRotation(
|
||||||
|
entity: EntityInstance
|
||||||
|
): EntityTransformRotationState {
|
||||||
switch (entity.kind) {
|
switch (entity.kind) {
|
||||||
case "playerStart":
|
case "playerStart":
|
||||||
case "teleportTarget":
|
case "teleportTarget":
|
||||||
@@ -668,7 +785,10 @@ function resolveEntityRotation(entity: EntityInstance): EntityTransformRotationS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createBrushTransformTarget(document: SceneDocument, brushId: string): TransformTargetResolution {
|
function createBrushTransformTarget(
|
||||||
|
document: SceneDocument,
|
||||||
|
brushId: string
|
||||||
|
): TransformTargetResolution {
|
||||||
const brush = document.brushes[brushId];
|
const brush = document.brushes[brushId];
|
||||||
|
|
||||||
if (brush === undefined || brush.kind !== "box") {
|
if (brush === undefined || brush.kind !== "box") {
|
||||||
@@ -691,10 +811,17 @@ function createBrushTransformTarget(document: SceneDocument, brushId: string): T
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createBrushFaceTransformTarget(document: SceneDocument, brushId: string, faceId: BoxFaceId): TransformTargetResolution {
|
function createBrushFaceTransformTarget(
|
||||||
|
document: SceneDocument,
|
||||||
|
brushId: string,
|
||||||
|
faceId: BoxFaceId
|
||||||
|
): TransformTargetResolution {
|
||||||
const brushResolution = createBrushTransformTarget(document, brushId);
|
const brushResolution = createBrushTransformTarget(document, brushId);
|
||||||
|
|
||||||
if (brushResolution.target === null || brushResolution.target.kind !== "brush") {
|
if (
|
||||||
|
brushResolution.target === null ||
|
||||||
|
brushResolution.target.kind !== "brush"
|
||||||
|
) {
|
||||||
return brushResolution;
|
return brushResolution;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -704,18 +831,29 @@ function createBrushFaceTransformTarget(document: SceneDocument, brushId: string
|
|||||||
brushId,
|
brushId,
|
||||||
faceId,
|
faceId,
|
||||||
initialCenter: cloneVec3(brushResolution.target.initialCenter),
|
initialCenter: cloneVec3(brushResolution.target.initialCenter),
|
||||||
initialRotationDegrees: cloneVec3(brushResolution.target.initialRotationDegrees),
|
initialRotationDegrees: cloneVec3(
|
||||||
|
brushResolution.target.initialRotationDegrees
|
||||||
|
),
|
||||||
initialSize: cloneVec3(brushResolution.target.initialSize),
|
initialSize: cloneVec3(brushResolution.target.initialSize),
|
||||||
initialGeometry: cloneBoxBrushGeometry(brushResolution.target.initialGeometry)
|
initialGeometry: cloneBoxBrushGeometry(
|
||||||
|
brushResolution.target.initialGeometry
|
||||||
|
)
|
||||||
},
|
},
|
||||||
message: null
|
message: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createBrushEdgeTransformTarget(document: SceneDocument, brushId: string, edgeId: BoxEdgeId): TransformTargetResolution {
|
function createBrushEdgeTransformTarget(
|
||||||
|
document: SceneDocument,
|
||||||
|
brushId: string,
|
||||||
|
edgeId: BoxEdgeId
|
||||||
|
): TransformTargetResolution {
|
||||||
const brushResolution = createBrushTransformTarget(document, brushId);
|
const brushResolution = createBrushTransformTarget(document, brushId);
|
||||||
|
|
||||||
if (brushResolution.target === null || brushResolution.target.kind !== "brush") {
|
if (
|
||||||
|
brushResolution.target === null ||
|
||||||
|
brushResolution.target.kind !== "brush"
|
||||||
|
) {
|
||||||
return brushResolution;
|
return brushResolution;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -725,18 +863,29 @@ function createBrushEdgeTransformTarget(document: SceneDocument, brushId: string
|
|||||||
brushId,
|
brushId,
|
||||||
edgeId,
|
edgeId,
|
||||||
initialCenter: cloneVec3(brushResolution.target.initialCenter),
|
initialCenter: cloneVec3(brushResolution.target.initialCenter),
|
||||||
initialRotationDegrees: cloneVec3(brushResolution.target.initialRotationDegrees),
|
initialRotationDegrees: cloneVec3(
|
||||||
|
brushResolution.target.initialRotationDegrees
|
||||||
|
),
|
||||||
initialSize: cloneVec3(brushResolution.target.initialSize),
|
initialSize: cloneVec3(brushResolution.target.initialSize),
|
||||||
initialGeometry: cloneBoxBrushGeometry(brushResolution.target.initialGeometry)
|
initialGeometry: cloneBoxBrushGeometry(
|
||||||
|
brushResolution.target.initialGeometry
|
||||||
|
)
|
||||||
},
|
},
|
||||||
message: null
|
message: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createBrushVertexTransformTarget(document: SceneDocument, brushId: string, vertexId: BoxVertexId): TransformTargetResolution {
|
function createBrushVertexTransformTarget(
|
||||||
|
document: SceneDocument,
|
||||||
|
brushId: string,
|
||||||
|
vertexId: BoxVertexId
|
||||||
|
): TransformTargetResolution {
|
||||||
const brushResolution = createBrushTransformTarget(document, brushId);
|
const brushResolution = createBrushTransformTarget(document, brushId);
|
||||||
|
|
||||||
if (brushResolution.target === null || brushResolution.target.kind !== "brush") {
|
if (
|
||||||
|
brushResolution.target === null ||
|
||||||
|
brushResolution.target.kind !== "brush"
|
||||||
|
) {
|
||||||
return brushResolution;
|
return brushResolution;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -746,15 +895,22 @@ function createBrushVertexTransformTarget(document: SceneDocument, brushId: stri
|
|||||||
brushId,
|
brushId,
|
||||||
vertexId,
|
vertexId,
|
||||||
initialCenter: cloneVec3(brushResolution.target.initialCenter),
|
initialCenter: cloneVec3(brushResolution.target.initialCenter),
|
||||||
initialRotationDegrees: cloneVec3(brushResolution.target.initialRotationDegrees),
|
initialRotationDegrees: cloneVec3(
|
||||||
|
brushResolution.target.initialRotationDegrees
|
||||||
|
),
|
||||||
initialSize: cloneVec3(brushResolution.target.initialSize),
|
initialSize: cloneVec3(brushResolution.target.initialSize),
|
||||||
initialGeometry: cloneBoxBrushGeometry(brushResolution.target.initialGeometry)
|
initialGeometry: cloneBoxBrushGeometry(
|
||||||
|
brushResolution.target.initialGeometry
|
||||||
|
)
|
||||||
},
|
},
|
||||||
message: null
|
message: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEntityTransformTarget(document: SceneDocument, entityId: string): TransformTargetResolution {
|
function createEntityTransformTarget(
|
||||||
|
document: SceneDocument,
|
||||||
|
entityId: string
|
||||||
|
): TransformTargetResolution {
|
||||||
const entity = document.entities[entityId];
|
const entity = document.entities[entityId];
|
||||||
|
|
||||||
if (entity === undefined) {
|
if (entity === undefined) {
|
||||||
@@ -778,7 +934,10 @@ function createEntityTransformTarget(document: SceneDocument, entityId: string):
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createModelInstanceTransformTarget(document: SceneDocument, modelInstanceId: string): TransformTargetResolution {
|
function createModelInstanceTransformTarget(
|
||||||
|
document: SceneDocument,
|
||||||
|
modelInstanceId: string
|
||||||
|
): TransformTargetResolution {
|
||||||
const modelInstance = document.modelInstances[modelInstanceId];
|
const modelInstance = document.modelInstances[modelInstanceId];
|
||||||
|
|
||||||
if (modelInstance === undefined) {
|
if (modelInstance === undefined) {
|
||||||
@@ -812,7 +971,8 @@ export function resolveTransformTarget(
|
|||||||
case "none":
|
case "none":
|
||||||
return {
|
return {
|
||||||
target: null,
|
target: null,
|
||||||
message: "Select a single brush, entity, or model instance before transforming it."
|
message:
|
||||||
|
"Select a single brush, entity, or model instance before transforming it."
|
||||||
};
|
};
|
||||||
case "brushFace":
|
case "brushFace":
|
||||||
if (whiteboxSelectionMode !== "face") {
|
if (whiteboxSelectionMode !== "face") {
|
||||||
@@ -822,7 +982,11 @@ export function resolveTransformTarget(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return createBrushFaceTransformTarget(document, selection.brushId, selection.faceId);
|
return createBrushFaceTransformTarget(
|
||||||
|
document,
|
||||||
|
selection.brushId,
|
||||||
|
selection.faceId
|
||||||
|
);
|
||||||
case "brushEdge":
|
case "brushEdge":
|
||||||
if (whiteboxSelectionMode !== "edge") {
|
if (whiteboxSelectionMode !== "edge") {
|
||||||
return {
|
return {
|
||||||
@@ -831,16 +995,25 @@ export function resolveTransformTarget(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return createBrushEdgeTransformTarget(document, selection.brushId, selection.edgeId);
|
return createBrushEdgeTransformTarget(
|
||||||
|
document,
|
||||||
|
selection.brushId,
|
||||||
|
selection.edgeId
|
||||||
|
);
|
||||||
case "brushVertex":
|
case "brushVertex":
|
||||||
if (whiteboxSelectionMode !== "vertex") {
|
if (whiteboxSelectionMode !== "vertex") {
|
||||||
return {
|
return {
|
||||||
target: null,
|
target: null,
|
||||||
message: "Switch to Vertex mode to transform a selected whitebox vertex."
|
message:
|
||||||
|
"Switch to Vertex mode to transform a selected whitebox vertex."
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return createBrushVertexTransformTarget(document, selection.brushId, selection.vertexId);
|
return createBrushVertexTransformTarget(
|
||||||
|
document,
|
||||||
|
selection.brushId,
|
||||||
|
selection.vertexId
|
||||||
|
);
|
||||||
case "brushes":
|
case "brushes":
|
||||||
if (whiteboxSelectionMode !== "object") {
|
if (whiteboxSelectionMode !== "object") {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import type { ProjectAssetRecord } from "../assets/project-assets";
|
|||||||
import type { EditorSelection } from "../core/selection";
|
import type { EditorSelection } from "../core/selection";
|
||||||
import { getWhiteboxSelectionFeedbackLabel } from "../core/whitebox-selection-feedback";
|
import { getWhiteboxSelectionFeedbackLabel } from "../core/whitebox-selection-feedback";
|
||||||
import type { ToolMode } from "../core/tool-mode";
|
import type { ToolMode } from "../core/tool-mode";
|
||||||
import { getWhiteboxSelectionModeLabel, type WhiteboxSelectionMode } from "../core/whitebox-selection-mode";
|
import {
|
||||||
|
getWhiteboxSelectionModeLabel,
|
||||||
|
type WhiteboxSelectionMode
|
||||||
|
} from "../core/whitebox-selection-mode";
|
||||||
import type { Vec3 } from "../core/vector";
|
import type { Vec3 } from "../core/vector";
|
||||||
import {
|
import {
|
||||||
getTransformAxisLabel,
|
getTransformAxisLabel,
|
||||||
@@ -28,7 +31,10 @@ import {
|
|||||||
getViewportViewModeLabel,
|
getViewportViewModeLabel,
|
||||||
type ViewportViewMode
|
type ViewportViewMode
|
||||||
} from "./viewport-view-modes";
|
} from "./viewport-view-modes";
|
||||||
import type { CreationViewportToolPreview, ViewportToolPreview } from "./viewport-transient-state";
|
import type {
|
||||||
|
CreationViewportToolPreview,
|
||||||
|
ViewportToolPreview
|
||||||
|
} from "./viewport-transient-state";
|
||||||
|
|
||||||
import { ViewportHost } from "./viewport-host";
|
import { ViewportHost } from "./viewport-host";
|
||||||
|
|
||||||
@@ -97,7 +103,9 @@ export function ViewportCanvas({
|
|||||||
const hostRef = useRef<ViewportHost | null>(null);
|
const hostRef = useRef<ViewportHost | null>(null);
|
||||||
const shouldRenderPanel = layoutMode === "quad" || isActivePanel;
|
const shouldRenderPanel = layoutMode === "quad" || isActivePanel;
|
||||||
const [viewportMessage, setViewportMessage] = useState<string | null>(null);
|
const [viewportMessage, setViewportMessage] = useState<string | null>(null);
|
||||||
const [hoveredWhiteboxLabel, setHoveredWhiteboxLabel] = useState<string | null>(null);
|
const [hoveredWhiteboxLabel, setHoveredWhiteboxLabel] = useState<
|
||||||
|
string | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
@@ -119,7 +127,10 @@ export function ViewportCanvas({
|
|||||||
hostRef.current = null;
|
hostRef.current = null;
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Viewport initialization failed.";
|
const message =
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Viewport initialization failed.";
|
||||||
setViewportMessage(`Viewport initialization failed: ${message}`);
|
setViewportMessage(`Viewport initialization failed: ${message}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -138,11 +149,18 @@ export function ViewportCanvas({
|
|||||||
}, [world]);
|
}, [world]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
hostRef.current?.updateAssets(projectAssets, loadedModelAssets, loadedImageAssets);
|
hostRef.current?.updateAssets(
|
||||||
|
projectAssets,
|
||||||
|
loadedModelAssets,
|
||||||
|
loadedImageAssets
|
||||||
|
);
|
||||||
}, [projectAssets, loadedModelAssets, loadedImageAssets]);
|
}, [projectAssets, loadedModelAssets, loadedImageAssets]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
hostRef.current?.setWhiteboxSnapSettings(whiteboxSnapEnabled, whiteboxSnapStep);
|
hostRef.current?.setWhiteboxSnapSettings(
|
||||||
|
whiteboxSnapEnabled,
|
||||||
|
whiteboxSnapStep
|
||||||
|
);
|
||||||
}, [whiteboxSnapEnabled, whiteboxSnapStep]);
|
}, [whiteboxSnapEnabled, whiteboxSnapStep]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -174,7 +192,9 @@ export function ViewportCanvas({
|
|||||||
}, [onSelectionChange]);
|
}, [onSelectionChange]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
hostRef.current?.setWhiteboxHoverLabelChangeHandler(setHoveredWhiteboxLabel);
|
hostRef.current?.setWhiteboxHoverLabelChangeHandler(
|
||||||
|
setHoveredWhiteboxLabel
|
||||||
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -215,7 +235,11 @@ export function ViewportCanvas({
|
|||||||
}, [toolMode]);
|
}, [toolMode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
hostRef.current?.setCreationPreview(toolMode === "create" && toolPreview.kind === "create" ? toolPreview : null);
|
hostRef.current?.setCreationPreview(
|
||||||
|
toolMode === "create" && toolPreview.kind === "create"
|
||||||
|
? toolPreview
|
||||||
|
: null
|
||||||
|
);
|
||||||
}, [toolMode, toolPreview]);
|
}, [toolMode, toolPreview]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -230,12 +254,23 @@ export function ViewportCanvas({
|
|||||||
hostRef.current?.focusSelection(sceneDocument, focusSelection);
|
hostRef.current?.focusSelection(sceneDocument, focusSelection);
|
||||||
}, [focusRequestId, focusSelection, sceneDocument]);
|
}, [focusRequestId, focusSelection, sceneDocument]);
|
||||||
|
|
||||||
const previewVisible = toolMode === "create" && toolPreview.kind === "create" && toolPreview.center !== null;
|
const previewVisible =
|
||||||
|
toolMode === "create" &&
|
||||||
|
toolPreview.kind === "create" &&
|
||||||
|
toolPreview.center !== null;
|
||||||
const transformPreviewVisible = transformSession.kind === "active";
|
const transformPreviewVisible = transformSession.kind === "active";
|
||||||
const selectionModeVisible = toolMode === "select";
|
const selectionModeVisible = toolMode === "select";
|
||||||
const selectedWhiteboxLabel = selectionModeVisible ? getWhiteboxSelectionFeedbackLabel(sceneDocument, selection) : null;
|
const selectedWhiteboxLabel = selectionModeVisible
|
||||||
|
? getWhiteboxSelectionFeedbackLabel(sceneDocument, selection)
|
||||||
|
: null;
|
||||||
const showViewModeOverlay = layoutMode === "quad";
|
const showViewModeOverlay = layoutMode === "quad";
|
||||||
const showOverlay = showViewModeOverlay || selectionModeVisible || previewVisible || transformPreviewVisible || selectedWhiteboxLabel !== null || hoveredWhiteboxLabel !== null;
|
const showOverlay =
|
||||||
|
showViewModeOverlay ||
|
||||||
|
selectionModeVisible ||
|
||||||
|
previewVisible ||
|
||||||
|
transformPreviewVisible ||
|
||||||
|
selectedWhiteboxLabel !== null ||
|
||||||
|
hoveredWhiteboxLabel !== null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -250,14 +285,25 @@ export function ViewportCanvas({
|
|||||||
backgroundColor: "#000000",
|
backgroundColor: "#000000",
|
||||||
backgroundImage: "none"
|
backgroundImage: "none"
|
||||||
}
|
}
|
||||||
: createWorldBackgroundStyle(world.background, world.background.mode === "image" ? loadedImageAssets[world.background.assetId]?.sourceUrl ?? null : null)
|
: createWorldBackgroundStyle(
|
||||||
|
world.background,
|
||||||
|
world.background.mode === "image"
|
||||||
|
? (loadedImageAssets[world.background.assetId]?.sourceUrl ??
|
||||||
|
null)
|
||||||
|
: null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{!showOverlay ? null : (
|
{!showOverlay ? null : (
|
||||||
<div className="viewport-canvas__overlay" data-testid={`viewport-overlay-${panelId}`}>
|
<div
|
||||||
|
className="viewport-canvas__overlay"
|
||||||
|
data-testid={`viewport-overlay-${panelId}`}
|
||||||
|
>
|
||||||
{!showViewModeOverlay ? null : (
|
{!showViewModeOverlay ? null : (
|
||||||
<div className="viewport-canvas__overlay-badges">
|
<div className="viewport-canvas__overlay-badges">
|
||||||
<div className="viewport-canvas__overlay-badge viewport-canvas__overlay-badge--view">{getViewportViewModeLabel(viewMode)}</div>
|
<div className="viewport-canvas__overlay-badge viewport-canvas__overlay-badge--view">
|
||||||
|
{getViewportViewModeLabel(viewMode)}
|
||||||
|
</div>
|
||||||
{!selectionModeVisible ? null : (
|
{!selectionModeVisible ? null : (
|
||||||
<div
|
<div
|
||||||
className="viewport-canvas__overlay-badge viewport-canvas__overlay-badge--selection"
|
className="viewport-canvas__overlay-badge viewport-canvas__overlay-badge--selection"
|
||||||
@@ -279,12 +325,19 @@ export function ViewportCanvas({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!previewVisible ? null : (
|
{!previewVisible ? null : (
|
||||||
<div className="viewport-canvas__overlay-preview" data-testid={`viewport-snap-preview-${panelId}`}>
|
<div
|
||||||
Preview: {(toolPreview.center as Vec3).x}, {(toolPreview.center as Vec3).y}, {(toolPreview.center as Vec3).z}
|
className="viewport-canvas__overlay-preview"
|
||||||
|
data-testid={`viewport-snap-preview-${panelId}`}
|
||||||
|
>
|
||||||
|
Preview: {(toolPreview.center as Vec3).x},{" "}
|
||||||
|
{(toolPreview.center as Vec3).y}, {(toolPreview.center as Vec3).z}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!transformPreviewVisible ? null : (
|
{!transformPreviewVisible ? null : (
|
||||||
<div className="viewport-canvas__overlay-preview" data-testid={`viewport-transform-preview-${panelId}`}>
|
<div
|
||||||
|
className="viewport-canvas__overlay-preview"
|
||||||
|
data-testid={`viewport-transform-preview-${panelId}`}
|
||||||
|
>
|
||||||
{transformSession.kind !== "active"
|
{transformSession.kind !== "active"
|
||||||
? null
|
? null
|
||||||
: `${transformSession.operation}${
|
: `${transformSession.operation}${
|
||||||
@@ -297,12 +350,18 @@ export function ViewportCanvas({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{selectedWhiteboxLabel === null ? null : (
|
{selectedWhiteboxLabel === null ? null : (
|
||||||
<div className="viewport-canvas__overlay-preview" data-testid={`viewport-selected-whitebox-${panelId}`}>
|
<div
|
||||||
|
className="viewport-canvas__overlay-preview"
|
||||||
|
data-testid={`viewport-selected-whitebox-${panelId}`}
|
||||||
|
>
|
||||||
Selected: {selectedWhiteboxLabel}
|
Selected: {selectedWhiteboxLabel}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hoveredWhiteboxLabel === null ? null : (
|
{hoveredWhiteboxLabel === null ? null : (
|
||||||
<div className="viewport-canvas__overlay-preview" data-testid={`viewport-hovered-whitebox-${panelId}`}>
|
<div
|
||||||
|
className="viewport-canvas__overlay-preview"
|
||||||
|
data-testid={`viewport-hovered-whitebox-${panelId}`}
|
||||||
|
>
|
||||||
Hover: {hoveredWhiteboxLabel}
|
Hover: {hoveredWhiteboxLabel}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -311,7 +370,9 @@ export function ViewportCanvas({
|
|||||||
|
|
||||||
{viewportMessage === null ? null : (
|
{viewportMessage === null ? null : (
|
||||||
<div className="viewport-canvas__fallback" role="status">
|
<div className="viewport-canvas__fallback" role="status">
|
||||||
<div className="viewport-canvas__fallback-title">Viewport Unavailable</div>
|
<div className="viewport-canvas__fallback-title">
|
||||||
|
Viewport Unavailable
|
||||||
|
</div>
|
||||||
<div>{viewportMessage}</div>
|
<div>{viewportMessage}</div>
|
||||||
{toolMode !== "create" || toolPreview.kind !== "create" ? null : (
|
{toolMode !== "create" || toolPreview.kind !== "create" ? null : (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -10,14 +10,24 @@ import {
|
|||||||
type ViewportPanelId,
|
type ViewportPanelId,
|
||||||
type ViewportPanelState
|
type ViewportPanelState
|
||||||
} from "./viewport-layout";
|
} from "./viewport-layout";
|
||||||
import { VIEWPORT_VIEW_MODES, getViewportViewModeLabel, type ViewportViewMode } from "./viewport-view-modes";
|
import {
|
||||||
import type { CreationViewportToolPreview, ViewportToolPreview } from "./viewport-transient-state";
|
VIEWPORT_VIEW_MODES,
|
||||||
|
getViewportViewModeLabel,
|
||||||
|
type ViewportViewMode
|
||||||
|
} from "./viewport-view-modes";
|
||||||
|
import type {
|
||||||
|
CreationViewportToolPreview,
|
||||||
|
ViewportToolPreview
|
||||||
|
} from "./viewport-transient-state";
|
||||||
import type { LoadedModelAsset } from "../assets/gltf-model-import";
|
import type { LoadedModelAsset } from "../assets/gltf-model-import";
|
||||||
import type { LoadedImageAsset } from "../assets/image-assets";
|
import type { LoadedImageAsset } from "../assets/image-assets";
|
||||||
import type { ProjectAssetRecord } from "../assets/project-assets";
|
import type { ProjectAssetRecord } from "../assets/project-assets";
|
||||||
import type { EditorSelection } from "../core/selection";
|
import type { EditorSelection } from "../core/selection";
|
||||||
import type { WhiteboxSelectionMode } from "../core/whitebox-selection-mode";
|
import type { WhiteboxSelectionMode } from "../core/whitebox-selection-mode";
|
||||||
import type { ActiveTransformSession, TransformSessionState } from "../core/transform-session";
|
import type {
|
||||||
|
ActiveTransformSession,
|
||||||
|
TransformSessionState
|
||||||
|
} from "../core/transform-session";
|
||||||
import type { ToolMode } from "../core/tool-mode";
|
import type { ToolMode } from "../core/tool-mode";
|
||||||
import type { SceneDocument } from "../document/scene-document";
|
import type { SceneDocument } from "../document/scene-document";
|
||||||
import type { WorldSettings } from "../document/world-settings";
|
import type { WorldSettings } from "../document/world-settings";
|
||||||
@@ -46,8 +56,14 @@ interface ViewportPanelProps {
|
|||||||
focusRequestId: number;
|
focusRequestId: number;
|
||||||
focusSelection: EditorSelection;
|
focusSelection: EditorSelection;
|
||||||
onActivatePanel(panelId: ViewportPanelId): void;
|
onActivatePanel(panelId: ViewportPanelId): void;
|
||||||
onSetPanelViewMode(panelId: ViewportPanelId, viewMode: ViewportViewMode): void;
|
onSetPanelViewMode(
|
||||||
onSetPanelDisplayMode(panelId: ViewportPanelId, displayMode: ViewportDisplayMode): void;
|
panelId: ViewportPanelId,
|
||||||
|
viewMode: ViewportViewMode
|
||||||
|
): void;
|
||||||
|
onSetPanelDisplayMode(
|
||||||
|
panelId: ViewportPanelId,
|
||||||
|
displayMode: ViewportDisplayMode
|
||||||
|
): void;
|
||||||
onCommitCreation(toolPreview: CreationViewportToolPreview): boolean;
|
onCommitCreation(toolPreview: CreationViewportToolPreview): boolean;
|
||||||
onCameraStateChange(cameraState: ViewportPanelCameraState): void;
|
onCameraStateChange(cameraState: ViewportPanelCameraState): void;
|
||||||
onToolPreviewChange(toolPreview: ViewportToolPreview): void;
|
onToolPreviewChange(toolPreview: ViewportToolPreview): void;
|
||||||
@@ -110,9 +126,14 @@ export function ViewportPanel({
|
|||||||
{layoutMode !== "quad" ? null : (
|
{layoutMode !== "quad" ? null : (
|
||||||
<div className="viewport-panel__meta">
|
<div className="viewport-panel__meta">
|
||||||
<div className="viewport-panel__title-row">
|
<div className="viewport-panel__title-row">
|
||||||
<div className="viewport-panel__title">{getViewportPanelLabel(panelId)}</div>
|
<div className="viewport-panel__title">
|
||||||
|
{getViewportPanelLabel(panelId)}
|
||||||
|
</div>
|
||||||
{!isActive ? null : (
|
{!isActive ? null : (
|
||||||
<div className="viewport-panel__active-badge" data-testid={`viewport-panel-active-badge-${panelId}`}>
|
<div
|
||||||
|
className="viewport-panel__active-badge"
|
||||||
|
data-testid={`viewport-panel-active-badge-${panelId}`}
|
||||||
|
>
|
||||||
Active
|
Active
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -120,7 +141,11 @@ export function ViewportPanel({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="viewport-panel__controls">
|
<div className="viewport-panel__controls">
|
||||||
<div className="viewport-panel__control-group" role="group" aria-label={`${getViewportPanelLabel(panelId)} view mode`}>
|
<div
|
||||||
|
className="viewport-panel__control-group"
|
||||||
|
role="group"
|
||||||
|
aria-label={`${getViewportPanelLabel(panelId)} view mode`}
|
||||||
|
>
|
||||||
{VIEWPORT_VIEW_MODES.map((viewMode) => (
|
{VIEWPORT_VIEW_MODES.map((viewMode) => (
|
||||||
<button
|
<button
|
||||||
key={viewMode}
|
key={viewMode}
|
||||||
@@ -135,19 +160,25 @@ export function ViewportPanel({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="viewport-panel__control-group" role="group" aria-label={`${getViewportPanelLabel(panelId)} display mode`}>
|
<div
|
||||||
{(["normal", "authoring", "wireframe"] as const).map((displayMode) => (
|
className="viewport-panel__control-group"
|
||||||
<button
|
role="group"
|
||||||
key={displayMode}
|
aria-label={`${getViewportPanelLabel(panelId)} display mode`}
|
||||||
className={`viewport-panel__button ${panelState.displayMode === displayMode ? "viewport-panel__button--active" : ""}`}
|
>
|
||||||
type="button"
|
{(["normal", "authoring", "wireframe"] as const).map(
|
||||||
data-testid={`viewport-panel-${panelId}-display-${displayMode}`}
|
(displayMode) => (
|
||||||
aria-pressed={panelState.displayMode === displayMode}
|
<button
|
||||||
onClick={() => onSetPanelDisplayMode(panelId, displayMode)}
|
key={displayMode}
|
||||||
>
|
className={`viewport-panel__button ${panelState.displayMode === displayMode ? "viewport-panel__button--active" : ""}`}
|
||||||
{getViewportDisplayModeLabel(displayMode)}
|
type="button"
|
||||||
</button>
|
data-testid={`viewport-panel-${panelId}-display-${displayMode}`}
|
||||||
))}
|
aria-pressed={panelState.displayMode === displayMode}
|
||||||
|
onClick={() => onSetPanelDisplayMode(panelId, displayMode)}
|
||||||
|
>
|
||||||
|
{getViewportDisplayModeLabel(displayMode)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,10 @@ import { describe, expect, it } from "vitest";
|
|||||||
|
|
||||||
import { createEditorStore } from "../../src/app/editor-store";
|
import { createEditorStore } from "../../src/app/editor-store";
|
||||||
import { createModelInstance } from "../../src/assets/model-instances";
|
import { createModelInstance } from "../../src/assets/model-instances";
|
||||||
import { createProjectAssetStorageKey, type ModelAssetRecord } from "../../src/assets/project-assets";
|
import {
|
||||||
|
createProjectAssetStorageKey,
|
||||||
|
type ModelAssetRecord
|
||||||
|
} from "../../src/assets/project-assets";
|
||||||
import { createCommitTransformSessionCommand } from "../../src/commands/commit-transform-session-command";
|
import { createCommitTransformSessionCommand } from "../../src/commands/commit-transform-session-command";
|
||||||
import {
|
import {
|
||||||
createTransformSession,
|
createTransformSession,
|
||||||
@@ -11,7 +14,10 @@ import {
|
|||||||
supportsTransformAxisConstraint,
|
supportsTransformAxisConstraint,
|
||||||
supportsTransformOperation
|
supportsTransformOperation
|
||||||
} from "../../src/core/transform-session";
|
} from "../../src/core/transform-session";
|
||||||
import { cloneBoxBrushGeometry, createBoxBrush } from "../../src/document/brushes";
|
import {
|
||||||
|
cloneBoxBrushGeometry,
|
||||||
|
createBoxBrush
|
||||||
|
} from "../../src/document/brushes";
|
||||||
import { createEmptySceneDocument } from "../../src/document/scene-document";
|
import { createEmptySceneDocument } from "../../src/document/scene-document";
|
||||||
import { createPlayerStartEntity } from "../../src/entities/entity-instances";
|
import { createPlayerStartEntity } from "../../src/entities/entity-instances";
|
||||||
import { getBoxBrushLocalVertexPosition } from "../../src/geometry/box-brush-mesh";
|
import { getBoxBrushLocalVertexPosition } from "../../src/geometry/box-brush-mesh";
|
||||||
@@ -137,17 +143,62 @@ describe("transform session commit commands", () => {
|
|||||||
initialSize: brush.size
|
initialSize: brush.size
|
||||||
});
|
});
|
||||||
expect(objectResolved.target).not.toBeNull();
|
expect(objectResolved.target).not.toBeNull();
|
||||||
expect(supportsTransformOperation(objectResolved.target as NonNullable<typeof objectResolved.target>, "translate")).toBe(true);
|
expect(
|
||||||
expect(supportsTransformOperation(objectResolved.target as NonNullable<typeof objectResolved.target>, "rotate")).toBe(true);
|
supportsTransformOperation(
|
||||||
expect(supportsTransformOperation(objectResolved.target as NonNullable<typeof objectResolved.target>, "scale")).toBe(true);
|
objectResolved.target as NonNullable<typeof objectResolved.target>,
|
||||||
|
"translate"
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
supportsTransformOperation(
|
||||||
|
objectResolved.target as NonNullable<typeof objectResolved.target>,
|
||||||
|
"rotate"
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
supportsTransformOperation(
|
||||||
|
objectResolved.target as NonNullable<typeof objectResolved.target>,
|
||||||
|
"scale"
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
expect(supportsTransformOperation(faceResolved.target as NonNullable<typeof faceResolved.target>, "translate")).toBe(true);
|
expect(
|
||||||
expect(supportsTransformOperation(faceResolved.target as NonNullable<typeof faceResolved.target>, "rotate")).toBe(true);
|
supportsTransformOperation(
|
||||||
expect(supportsTransformOperation(faceResolved.target as NonNullable<typeof faceResolved.target>, "scale")).toBe(true);
|
faceResolved.target as NonNullable<typeof faceResolved.target>,
|
||||||
|
"translate"
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
supportsTransformOperation(
|
||||||
|
faceResolved.target as NonNullable<typeof faceResolved.target>,
|
||||||
|
"rotate"
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
supportsTransformOperation(
|
||||||
|
faceResolved.target as NonNullable<typeof faceResolved.target>,
|
||||||
|
"scale"
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
expect(supportsTransformOperation(vertexResolved.target as NonNullable<typeof vertexResolved.target>, "translate")).toBe(true);
|
expect(
|
||||||
expect(supportsTransformOperation(vertexResolved.target as NonNullable<typeof vertexResolved.target>, "rotate")).toBe(false);
|
supportsTransformOperation(
|
||||||
expect(supportsTransformOperation(vertexResolved.target as NonNullable<typeof vertexResolved.target>, "scale")).toBe(false);
|
vertexResolved.target as NonNullable<typeof vertexResolved.target>,
|
||||||
|
"translate"
|
||||||
|
)
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
supportsTransformOperation(
|
||||||
|
vertexResolved.target as NonNullable<typeof vertexResolved.target>,
|
||||||
|
"rotate"
|
||||||
|
)
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
supportsTransformOperation(
|
||||||
|
vertexResolved.target as NonNullable<typeof vertexResolved.target>,
|
||||||
|
"scale"
|
||||||
|
)
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("applies axis-constraint rules across object and component transform sessions", () => {
|
it("applies axis-constraint rules across object and component transform sessions", () => {
|
||||||
@@ -228,9 +279,15 @@ describe("transform session commit commands", () => {
|
|||||||
expect(supportsTransformAxisConstraint(edgeScaleSession, "y")).toBe(false);
|
expect(supportsTransformAxisConstraint(edgeScaleSession, "y")).toBe(false);
|
||||||
expect(supportsTransformAxisConstraint(edgeScaleSession, "z")).toBe(true);
|
expect(supportsTransformAxisConstraint(edgeScaleSession, "z")).toBe(true);
|
||||||
|
|
||||||
expect(supportsTransformAxisConstraint(vertexTranslateSession, "x")).toBe(true);
|
expect(supportsTransformAxisConstraint(vertexTranslateSession, "x")).toBe(
|
||||||
expect(supportsTransformAxisConstraint(vertexTranslateSession, "y")).toBe(true);
|
true
|
||||||
expect(supportsTransformAxisConstraint(vertexTranslateSession, "z")).toBe(true);
|
);
|
||||||
|
expect(supportsTransformAxisConstraint(vertexTranslateSession, "y")).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(supportsTransformAxisConstraint(vertexTranslateSession, "z")).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("only enables local axis toggling on supported transform targets", () => {
|
it("only enables local axis toggling on supported transform targets", () => {
|
||||||
@@ -311,10 +368,18 @@ describe("transform session commit commands", () => {
|
|||||||
target: entityTarget
|
target: entityTarget
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(supportsLocalTransformAxisConstraint(brushTranslateSession, "z")).toBe(true);
|
expect(
|
||||||
expect(supportsLocalTransformAxisConstraint(brushScaleSession, "z")).toBe(false);
|
supportsLocalTransformAxisConstraint(brushTranslateSession, "z")
|
||||||
expect(supportsLocalTransformAxisConstraint(faceRotateSession, "x")).toBe(false);
|
).toBe(true);
|
||||||
expect(supportsLocalTransformAxisConstraint(entityTranslateSession, "x")).toBe(true);
|
expect(supportsLocalTransformAxisConstraint(brushScaleSession, "z")).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(supportsLocalTransformAxisConstraint(faceRotateSession, "x")).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
supportsLocalTransformAxisConstraint(entityTranslateSession, "x")
|
||||||
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("commits whitebox box rotate and scale transforms with undo and redo", () => {
|
it("commits whitebox box rotate and scale transforms with undo and redo", () => {
|
||||||
@@ -371,13 +436,20 @@ describe("transform session commit commands", () => {
|
|||||||
geometry: target.initialGeometry
|
geometry: target.initialGeometry
|
||||||
};
|
};
|
||||||
|
|
||||||
store.executeCommand(createCommitTransformSessionCommand(store.getState().document, rotateSession));
|
store.executeCommand(
|
||||||
|
createCommitTransformSessionCommand(
|
||||||
|
store.getState().document,
|
||||||
|
rotateSession
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
expect(store.getState().document.brushes[brush.id].rotationDegrees).toEqual({
|
expect(store.getState().document.brushes[brush.id].rotationDegrees).toEqual(
|
||||||
x: 0,
|
{
|
||||||
y: 37.5,
|
x: 0,
|
||||||
z: 12.5
|
y: 37.5,
|
||||||
});
|
z: 12.5
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const scaleTarget = resolveTransformTarget(store.getState().document, {
|
const scaleTarget = resolveTransformTarget(store.getState().document, {
|
||||||
kind: "brushes",
|
kind: "brushes",
|
||||||
@@ -385,7 +457,9 @@ describe("transform session commit commands", () => {
|
|||||||
}).target;
|
}).target;
|
||||||
|
|
||||||
if (scaleTarget === null || scaleTarget.kind !== "brush") {
|
if (scaleTarget === null || scaleTarget.kind !== "brush") {
|
||||||
throw new Error("Expected a whitebox box transform target after rotation.");
|
throw new Error(
|
||||||
|
"Expected a whitebox box transform target after rotation."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const scaleSession = createTransformSession({
|
const scaleSession = createTransformSession({
|
||||||
@@ -413,7 +487,12 @@ describe("transform session commit commands", () => {
|
|||||||
geometry: scaleTarget.initialGeometry
|
geometry: scaleTarget.initialGeometry
|
||||||
};
|
};
|
||||||
|
|
||||||
store.executeCommand(createCommitTransformSessionCommand(store.getState().document, scaleSession));
|
store.executeCommand(
|
||||||
|
createCommitTransformSessionCommand(
|
||||||
|
store.getState().document,
|
||||||
|
scaleSession
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
expect(store.getState().document.brushes[brush.id]).toMatchObject({
|
expect(store.getState().document.brushes[brush.id]).toMatchObject({
|
||||||
rotationDegrees: {
|
rotationDegrees: {
|
||||||
@@ -504,7 +583,9 @@ describe("transform session commit commands", () => {
|
|||||||
geometry: createBoxBrush({ size: { x: 3, y: 2, z: 2 } }).geometry
|
geometry: createBoxBrush({ size: { x: 3, y: 2, z: 2 } }).geometry
|
||||||
};
|
};
|
||||||
|
|
||||||
store.executeCommand(createCommitTransformSessionCommand(store.getState().document, session));
|
store.executeCommand(
|
||||||
|
createCommitTransformSessionCommand(store.getState().document, session)
|
||||||
|
);
|
||||||
|
|
||||||
expect(store.getState().document.brushes[brush.id]).toMatchObject({
|
expect(store.getState().document.brushes[brush.id]).toMatchObject({
|
||||||
center: { x: 0.5, y: 1, z: 0 },
|
center: { x: 0.5, y: 1, z: 0 },
|
||||||
@@ -569,7 +650,9 @@ describe("transform session commit commands", () => {
|
|||||||
geometry: createBoxBrush({ size: { x: 3, y: 3, z: 3 } }).geometry
|
geometry: createBoxBrush({ size: { x: 3, y: 3, z: 3 } }).geometry
|
||||||
};
|
};
|
||||||
|
|
||||||
store.executeCommand(createCommitTransformSessionCommand(store.getState().document, session));
|
store.executeCommand(
|
||||||
|
createCommitTransformSessionCommand(store.getState().document, session)
|
||||||
|
);
|
||||||
|
|
||||||
expect(store.getState().document.brushes[brush.id]).toMatchObject({
|
expect(store.getState().document.brushes[brush.id]).toMatchObject({
|
||||||
center: { x: 0.5, y: 1.5, z: 0.5 },
|
center: { x: 0.5, y: 1.5, z: 0.5 },
|
||||||
@@ -640,11 +723,17 @@ describe("transform session commit commands", () => {
|
|||||||
geometry: deformedGeometry
|
geometry: deformedGeometry
|
||||||
};
|
};
|
||||||
|
|
||||||
store.executeCommand(createCommitTransformSessionCommand(store.getState().document, session));
|
store.executeCommand(
|
||||||
|
createCommitTransformSessionCommand(store.getState().document, session)
|
||||||
|
);
|
||||||
|
|
||||||
const committedBrush = store.getState().document.brushes[brush.id];
|
const committedBrush = store.getState().document.brushes[brush.id];
|
||||||
expect(getBoxBrushLocalVertexPosition(committedBrush, "posX_posY_posZ").x).toBe(2);
|
expect(
|
||||||
expect(getBoxBrushLocalVertexPosition(committedBrush, "posX_posY_negZ").x).toBe(1);
|
getBoxBrushLocalVertexPosition(committedBrush, "posX_posY_posZ").x
|
||||||
|
).toBe(2);
|
||||||
|
expect(
|
||||||
|
getBoxBrushLocalVertexPosition(committedBrush, "posX_posY_negZ").x
|
||||||
|
).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("commits a model instance translate/rotate/scale transform with undo and redo", () => {
|
it("commits a model instance translate/rotate/scale transform with undo and redo", () => {
|
||||||
@@ -713,9 +802,13 @@ describe("transform session commit commands", () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
store.executeCommand(createCommitTransformSessionCommand(store.getState().document, session));
|
store.executeCommand(
|
||||||
|
createCommitTransformSessionCommand(store.getState().document, session)
|
||||||
|
);
|
||||||
|
|
||||||
expect(store.getState().document.modelInstances[modelInstance.id]).toMatchObject({
|
expect(
|
||||||
|
store.getState().document.modelInstances[modelInstance.id]
|
||||||
|
).toMatchObject({
|
||||||
position: {
|
position: {
|
||||||
x: 4,
|
x: 4,
|
||||||
y: 1,
|
y: 1,
|
||||||
@@ -734,10 +827,14 @@ describe("transform session commit commands", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(store.undo()).toBe(true);
|
expect(store.undo()).toBe(true);
|
||||||
expect(store.getState().document.modelInstances[modelInstance.id]).toEqual(modelInstance);
|
expect(store.getState().document.modelInstances[modelInstance.id]).toEqual(
|
||||||
|
modelInstance
|
||||||
|
);
|
||||||
|
|
||||||
expect(store.redo()).toBe(true);
|
expect(store.redo()).toBe(true);
|
||||||
expect(store.getState().document.modelInstances[modelInstance.id]).toMatchObject({
|
expect(
|
||||||
|
store.getState().document.modelInstances[modelInstance.id]
|
||||||
|
).toMatchObject({
|
||||||
position: {
|
position: {
|
||||||
x: 4,
|
x: 4,
|
||||||
y: 1,
|
y: 1,
|
||||||
@@ -803,7 +900,9 @@ describe("transform session commit commands", () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
store.executeCommand(createCommitTransformSessionCommand(store.getState().document, session));
|
store.executeCommand(
|
||||||
|
createCommitTransformSessionCommand(store.getState().document, session)
|
||||||
|
);
|
||||||
|
|
||||||
expect(store.getState().document.entities[playerStart.id]).toMatchObject({
|
expect(store.getState().document.entities[playerStart.id]).toMatchObject({
|
||||||
position: {
|
position: {
|
||||||
@@ -815,7 +914,9 @@ describe("transform session commit commands", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(store.undo()).toBe(true);
|
expect(store.undo()).toBe(true);
|
||||||
expect(store.getState().document.entities[playerStart.id]).toEqual(playerStart);
|
expect(store.getState().document.entities[playerStart.id]).toEqual(
|
||||||
|
playerStart
|
||||||
|
);
|
||||||
|
|
||||||
expect(store.redo()).toBe(true);
|
expect(store.redo()).toBe(true);
|
||||||
expect(store.getState().document.entities[playerStart.id]).toMatchObject({
|
expect(store.getState().document.entities[playerStart.id]).toMatchObject({
|
||||||
|
|||||||
@@ -5,8 +5,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
import { App } from "../../src/app/App";
|
import { App } from "../../src/app/App";
|
||||||
import { createEditorStore } from "../../src/app/editor-store";
|
import { createEditorStore } from "../../src/app/editor-store";
|
||||||
import { createModelInstance } from "../../src/assets/model-instances";
|
import { createModelInstance } from "../../src/assets/model-instances";
|
||||||
import { createProjectAssetStorageKey, type ModelAssetRecord } from "../../src/assets/project-assets";
|
import {
|
||||||
import type { ActiveTransformSession, TransformSessionState } from "../../src/core/transform-session";
|
createProjectAssetStorageKey,
|
||||||
|
type ModelAssetRecord
|
||||||
|
} from "../../src/assets/project-assets";
|
||||||
|
import type {
|
||||||
|
ActiveTransformSession,
|
||||||
|
TransformSessionState
|
||||||
|
} from "../../src/core/transform-session";
|
||||||
import { createBoxBrush } from "../../src/document/brushes";
|
import { createBoxBrush } from "../../src/document/brushes";
|
||||||
import { createEmptySceneDocument } from "../../src/document/scene-document";
|
import { createEmptySceneDocument } from "../../src/document/scene-document";
|
||||||
import { createPlayerStartEntity } from "../../src/entities/entity-instances";
|
import { createPlayerStartEntity } from "../../src/entities/entity-instances";
|
||||||
@@ -132,7 +138,9 @@ const modelAsset = {
|
|||||||
} satisfies ModelAssetRecord;
|
} satisfies ModelAssetRecord;
|
||||||
|
|
||||||
function getTopLeftViewportHost() {
|
function getTopLeftViewportHost() {
|
||||||
const viewportHost = viewportHostInstances.find((instance) => instance.panelId === "topLeft");
|
const viewportHost = viewportHostInstances.find(
|
||||||
|
(instance) => instance.panelId === "topLeft"
|
||||||
|
);
|
||||||
|
|
||||||
if (viewportHost === undefined) {
|
if (viewportHost === undefined) {
|
||||||
throw new Error("Top-left viewport host was not mounted.");
|
throw new Error("Top-left viewport host was not mounted.");
|
||||||
@@ -193,7 +201,9 @@ async function renderTransformFixtureApp() {
|
|||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(viewportHostInstances.length).toBeGreaterThan(0);
|
expect(viewportHostInstances.length).toBeGreaterThan(0);
|
||||||
expect(getTopLeftViewportHost().setTransformCommitHandler).toHaveBeenCalled();
|
expect(
|
||||||
|
getTopLeftViewportHost().setTransformCommitHandler
|
||||||
|
).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -215,8 +225,11 @@ async function renderQuadTransformFixtureApp() {
|
|||||||
return fixture;
|
return fixture;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLatestTransformSession(store: ReturnType<typeof createEditorStore>): ActiveTransformSession {
|
function getLatestTransformSession(
|
||||||
const transformSession = store.getState().viewportTransientState.transformSession;
|
store: ReturnType<typeof createEditorStore>
|
||||||
|
): ActiveTransformSession {
|
||||||
|
const transformSession =
|
||||||
|
store.getState().viewportTransientState.transformSession;
|
||||||
|
|
||||||
if (transformSession.kind !== "active") {
|
if (transformSession.kind !== "active") {
|
||||||
throw new Error("Expected an active transform session.");
|
throw new Error("Expected an active transform session.");
|
||||||
@@ -229,7 +242,9 @@ function emitTransformPreview(
|
|||||||
viewportHost: ReturnType<typeof getTopLeftViewportHost>,
|
viewportHost: ReturnType<typeof getTopLeftViewportHost>,
|
||||||
transformSession: ActiveTransformSession
|
transformSession: ActiveTransformSession
|
||||||
) {
|
) {
|
||||||
const handler = viewportHost.setTransformSessionChangeHandler.mock.calls.at(-1)?.[0] as ((transformSession: TransformSessionState) => void) | undefined;
|
const handler = viewportHost.setTransformSessionChangeHandler.mock.calls.at(
|
||||||
|
-1
|
||||||
|
)?.[0] as ((transformSession: TransformSessionState) => void) | undefined;
|
||||||
|
|
||||||
if (handler === undefined) {
|
if (handler === undefined) {
|
||||||
throw new Error("Transform session change handler was not registered.");
|
throw new Error("Transform session change handler was not registered.");
|
||||||
@@ -240,8 +255,13 @@ function emitTransformPreview(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function commitTransform(viewportHost: ReturnType<typeof getTopLeftViewportHost>, transformSession: ActiveTransformSession) {
|
function commitTransform(
|
||||||
const handler = viewportHost.setTransformCommitHandler.mock.calls.at(-1)?.[0] as ((transformSession: ActiveTransformSession) => void) | undefined;
|
viewportHost: ReturnType<typeof getTopLeftViewportHost>,
|
||||||
|
transformSession: ActiveTransformSession
|
||||||
|
) {
|
||||||
|
const handler = viewportHost.setTransformCommitHandler.mock.calls.at(
|
||||||
|
-1
|
||||||
|
)?.[0] as ((transformSession: ActiveTransformSession) => void) | undefined;
|
||||||
|
|
||||||
if (handler === undefined) {
|
if (handler === undefined) {
|
||||||
throw new Error("Transform commit handler was not registered.");
|
throw new Error("Transform commit handler was not registered.");
|
||||||
@@ -252,8 +272,13 @@ function commitTransform(viewportHost: ReturnType<typeof getTopLeftViewportHost>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitCameraStateChange(viewportHost: ReturnType<typeof getTopLeftViewportHost>, cameraState: ViewportPanelCameraState) {
|
function emitCameraStateChange(
|
||||||
const handler = viewportHost.setCameraStateChangeHandler.mock.calls.at(-1)?.[0] as ((cameraState: ViewportPanelCameraState) => void) | undefined;
|
viewportHost: ReturnType<typeof getTopLeftViewportHost>,
|
||||||
|
cameraState: ViewportPanelCameraState
|
||||||
|
) {
|
||||||
|
const handler = viewportHost.setCameraStateChangeHandler.mock.calls.at(
|
||||||
|
-1
|
||||||
|
)?.[0] as ((cameraState: ViewportPanelCameraState) => void) | undefined;
|
||||||
|
|
||||||
if (handler === undefined) {
|
if (handler === undefined) {
|
||||||
throw new Error("Camera state change handler was not registered.");
|
throw new Error("Camera state change handler was not registered.");
|
||||||
@@ -267,7 +292,9 @@ function emitCameraStateChange(viewportHost: ReturnType<typeof getTopLeftViewpor
|
|||||||
describe("transform foundation integration", () => {
|
describe("transform foundation integration", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
viewportHostInstances.length = 0;
|
viewportHostInstances.length = 0;
|
||||||
vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockImplementation(() => ({}) as never);
|
vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockImplementation(
|
||||||
|
() => ({}) as never
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -278,7 +305,9 @@ describe("transform foundation integration", () => {
|
|||||||
const { store, brush, viewportHost } = await renderTransformFixtureApp();
|
const { store, brush, viewportHost } = await renderTransformFixtureApp();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.click(screen.getByRole("button", { name: /^Brush Transform Fixture$/ }));
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", { name: /^Brush Transform Fixture$/ })
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.keyDown(window, {
|
fireEvent.keyDown(window, {
|
||||||
@@ -286,7 +315,9 @@ describe("transform foundation integration", () => {
|
|||||||
code: "KeyG"
|
code: "KeyG"
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(store.getState().viewportTransientState.transformSession).toMatchObject({
|
expect(
|
||||||
|
store.getState().viewportTransientState.transformSession
|
||||||
|
).toMatchObject({
|
||||||
kind: "active",
|
kind: "active",
|
||||||
operation: "translate",
|
operation: "translate",
|
||||||
axisConstraint: null,
|
axisConstraint: null,
|
||||||
@@ -301,7 +332,9 @@ describe("transform foundation integration", () => {
|
|||||||
code: "KeyX"
|
code: "KeyX"
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(store.getState().viewportTransientState.transformSession).toMatchObject({
|
expect(
|
||||||
|
store.getState().viewportTransientState.transformSession
|
||||||
|
).toMatchObject({
|
||||||
kind: "active",
|
kind: "active",
|
||||||
axisConstraint: "x"
|
axisConstraint: "x"
|
||||||
});
|
});
|
||||||
@@ -342,7 +375,9 @@ describe("transform foundation integration", () => {
|
|||||||
const { store, brush, viewportHost } = await renderTransformFixtureApp();
|
const { store, brush, viewportHost } = await renderTransformFixtureApp();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.click(screen.getByRole("button", { name: /^Brush Transform Fixture$/ }));
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", { name: /^Brush Transform Fixture$/ })
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("transform-rotate-button"));
|
fireEvent.click(screen.getByTestId("transform-rotate-button"));
|
||||||
@@ -369,11 +404,13 @@ describe("transform foundation integration", () => {
|
|||||||
emitTransformPreview(viewportHost, rotatePreviewSession);
|
emitTransformPreview(viewportHost, rotatePreviewSession);
|
||||||
commitTransform(viewportHost, rotatePreviewSession);
|
commitTransform(viewportHost, rotatePreviewSession);
|
||||||
|
|
||||||
expect(store.getState().document.brushes[brush.id].rotationDegrees).toEqual({
|
expect(store.getState().document.brushes[brush.id].rotationDegrees).toEqual(
|
||||||
x: 0,
|
{
|
||||||
y: 37.5,
|
x: 0,
|
||||||
z: 12.5
|
y: 37.5,
|
||||||
});
|
z: 12.5
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("transform-scale-button"));
|
fireEvent.click(screen.getByTestId("transform-scale-button"));
|
||||||
|
|
||||||
@@ -425,7 +462,9 @@ describe("transform foundation integration", () => {
|
|||||||
const { store, brush } = await renderTransformFixtureApp();
|
const { store, brush } = await renderTransformFixtureApp();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.click(screen.getByRole("button", { name: /^Brush Transform Fixture$/ }));
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", { name: /^Brush Transform Fixture$/ })
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByTestId("transform-translate-button")).not.toBeDisabled();
|
expect(screen.getByTestId("transform-translate-button")).not.toBeDisabled();
|
||||||
@@ -480,10 +519,13 @@ describe("transform foundation integration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("moves an entity through the shared transform controller", async () => {
|
it("moves an entity through the shared transform controller", async () => {
|
||||||
const { store, playerStart, viewportHost } = await renderTransformFixtureApp();
|
const { store, playerStart, viewportHost } =
|
||||||
|
await renderTransformFixtureApp();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.click(screen.getByRole("button", { name: /^Player Start Fixture$/ }));
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", { name: /^Player Start Fixture$/ })
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.keyDown(window, {
|
fireEvent.keyDown(window, {
|
||||||
@@ -520,10 +562,13 @@ describe("transform foundation integration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("cancels an active transform with Escape without committing preview changes", async () => {
|
it("cancels an active transform with Escape without committing preview changes", async () => {
|
||||||
const { store, playerStart, viewportHost } = await renderTransformFixtureApp();
|
const { store, playerStart, viewportHost } =
|
||||||
|
await renderTransformFixtureApp();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.click(screen.getByRole("button", { name: /^Player Start Fixture$/ }));
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", { name: /^Player Start Fixture$/ })
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.keyDown(window, {
|
fireEvent.keyDown(window, {
|
||||||
@@ -555,14 +600,19 @@ describe("transform foundation integration", () => {
|
|||||||
expect(store.getState().viewportTransientState.transformSession).toEqual({
|
expect(store.getState().viewportTransientState.transformSession).toEqual({
|
||||||
kind: "none"
|
kind: "none"
|
||||||
});
|
});
|
||||||
expect(store.getState().document.entities[playerStart.id]).toEqual(playerStart);
|
expect(store.getState().document.entities[playerStart.id]).toEqual(
|
||||||
|
playerStart
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("moves a model instance through the shared transform controller", async () => {
|
it("moves a model instance through the shared transform controller", async () => {
|
||||||
const { store, modelInstance, viewportHost } = await renderTransformFixtureApp();
|
const { store, modelInstance, viewportHost } =
|
||||||
|
await renderTransformFixtureApp();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.click(screen.getByRole("button", { name: /^Model Transform Fixture$/ }));
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", { name: /^Model Transform Fixture$/ })
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.keyDown(window, {
|
fireEvent.keyDown(window, {
|
||||||
@@ -591,7 +641,9 @@ describe("transform foundation integration", () => {
|
|||||||
emitTransformPreview(viewportHost, previewSession);
|
emitTransformPreview(viewportHost, previewSession);
|
||||||
commitTransform(viewportHost, previewSession);
|
commitTransform(viewportHost, previewSession);
|
||||||
|
|
||||||
expect(store.getState().document.modelInstances[modelInstance.id]).toMatchObject({
|
expect(
|
||||||
|
store.getState().document.modelInstances[modelInstance.id]
|
||||||
|
).toMatchObject({
|
||||||
position: {
|
position: {
|
||||||
x: -1,
|
x: -1,
|
||||||
y: 0,
|
y: 0,
|
||||||
@@ -604,7 +656,9 @@ describe("transform foundation integration", () => {
|
|||||||
const { store, brush } = await renderQuadTransformFixtureApp();
|
const { store, brush } = await renderQuadTransformFixtureApp();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.click(screen.getByRole("button", { name: /^Brush Transform Fixture$/ }));
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", { name: /^Brush Transform Fixture$/ })
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.pointerMove(screen.getByTestId("viewport-panel-bottomRight"), {
|
fireEvent.pointerMove(screen.getByTestId("viewport-panel-bottomRight"), {
|
||||||
@@ -617,7 +671,9 @@ describe("transform foundation integration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(store.getState().activeViewportPanelId).toBe("bottomRight");
|
expect(store.getState().activeViewportPanelId).toBe("bottomRight");
|
||||||
expect(store.getState().viewportTransientState.transformSession).toMatchObject({
|
expect(
|
||||||
|
store.getState().viewportTransientState.transformSession
|
||||||
|
).toMatchObject({
|
||||||
kind: "active",
|
kind: "active",
|
||||||
operation: "translate",
|
operation: "translate",
|
||||||
sourcePanelId: "bottomRight",
|
sourcePanelId: "bottomRight",
|
||||||
@@ -632,7 +688,9 @@ describe("transform foundation integration", () => {
|
|||||||
const { store } = await renderTransformFixtureApp();
|
const { store } = await renderTransformFixtureApp();
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.click(screen.getByRole("button", { name: /^Brush Transform Fixture$/ }));
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", { name: /^Brush Transform Fixture$/ })
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.keyDown(window, {
|
fireEvent.keyDown(window, {
|
||||||
@@ -644,7 +702,9 @@ describe("transform foundation integration", () => {
|
|||||||
code: "KeyZ"
|
code: "KeyZ"
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(store.getState().viewportTransientState.transformSession).toMatchObject({
|
expect(
|
||||||
|
store.getState().viewportTransientState.transformSession
|
||||||
|
).toMatchObject({
|
||||||
kind: "active",
|
kind: "active",
|
||||||
axisConstraint: "z",
|
axisConstraint: "z",
|
||||||
axisConstraintSpace: "world"
|
axisConstraintSpace: "world"
|
||||||
@@ -655,12 +715,16 @@ describe("transform foundation integration", () => {
|
|||||||
code: "KeyZ"
|
code: "KeyZ"
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(store.getState().viewportTransientState.transformSession).toMatchObject({
|
expect(
|
||||||
|
store.getState().viewportTransientState.transformSession
|
||||||
|
).toMatchObject({
|
||||||
kind: "active",
|
kind: "active",
|
||||||
axisConstraint: "z",
|
axisConstraint: "z",
|
||||||
axisConstraintSpace: "local"
|
axisConstraintSpace: "local"
|
||||||
});
|
});
|
||||||
expect(screen.getByText(/constrained move to local z\./i)).toBeInTheDocument();
|
expect(
|
||||||
|
screen.getByText(/constrained move to local z\./i)
|
||||||
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps the persisted viewport camera state stable across transform commit, cancel, and delete", async () => {
|
it("keeps the persisted viewport camera state stable across transform commit, cancel, and delete", async () => {
|
||||||
@@ -681,10 +745,14 @@ describe("transform foundation integration", () => {
|
|||||||
|
|
||||||
emitCameraStateChange(viewportHost, persistedCameraState);
|
emitCameraStateChange(viewportHost, persistedCameraState);
|
||||||
|
|
||||||
expect(store.getState().viewportPanels.topLeft.cameraState).toEqual(persistedCameraState);
|
expect(store.getState().viewportPanels.topLeft.cameraState).toEqual(
|
||||||
|
persistedCameraState
|
||||||
|
);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.click(screen.getByRole("button", { name: /^Brush Transform Fixture$/ }));
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", { name: /^Brush Transform Fixture$/ })
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.keyDown(window, {
|
fireEvent.keyDown(window, {
|
||||||
@@ -713,9 +781,13 @@ describe("transform foundation integration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(viewportHost.setCameraState.mock.calls.length).toBeGreaterThan(commitCameraCallCount);
|
expect(viewportHost.setCameraState.mock.calls.length).toBeGreaterThan(
|
||||||
|
commitCameraCallCount
|
||||||
|
);
|
||||||
});
|
});
|
||||||
expect(viewportHost.setCameraState.mock.calls.at(-1)?.[0]).toEqual(persistedCameraState);
|
expect(viewportHost.setCameraState.mock.calls.at(-1)?.[0]).toEqual(
|
||||||
|
persistedCameraState
|
||||||
|
);
|
||||||
|
|
||||||
fireEvent.keyDown(window, {
|
fireEvent.keyDown(window, {
|
||||||
key: "g",
|
key: "g",
|
||||||
@@ -730,9 +802,13 @@ describe("transform foundation integration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(viewportHost.setCameraState.mock.calls.length).toBeGreaterThan(cancelCameraCallCount);
|
expect(viewportHost.setCameraState.mock.calls.length).toBeGreaterThan(
|
||||||
|
cancelCameraCallCount
|
||||||
|
);
|
||||||
});
|
});
|
||||||
expect(viewportHost.setCameraState.mock.calls.at(-1)?.[0]).toEqual(persistedCameraState);
|
expect(viewportHost.setCameraState.mock.calls.at(-1)?.[0]).toEqual(
|
||||||
|
persistedCameraState
|
||||||
|
);
|
||||||
|
|
||||||
vi.spyOn(window, "confirm").mockReturnValue(true);
|
vi.spyOn(window, "confirm").mockReturnValue(true);
|
||||||
const deleteCameraCallCount = viewportHost.setCameraState.mock.calls.length;
|
const deleteCameraCallCount = viewportHost.setCameraState.mock.calls.length;
|
||||||
@@ -743,9 +819,13 @@ describe("transform foundation integration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(viewportHost.setCameraState.mock.calls.length).toBeGreaterThan(deleteCameraCallCount);
|
expect(viewportHost.setCameraState.mock.calls.length).toBeGreaterThan(
|
||||||
|
deleteCameraCallCount
|
||||||
|
);
|
||||||
});
|
});
|
||||||
expect(viewportHost.setCameraState.mock.calls.at(-1)?.[0]).toEqual(persistedCameraState);
|
expect(viewportHost.setCameraState.mock.calls.at(-1)?.[0]).toEqual(
|
||||||
|
persistedCameraState
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("toggles viewport grid visibility through the shared viewport host path", async () => {
|
it("toggles viewport grid visibility through the shared viewport host path", async () => {
|
||||||
@@ -756,7 +836,9 @@ describe("transform foundation integration", () => {
|
|||||||
fireEvent.click(screen.getByTestId("viewport-grid-toggle"));
|
fireEvent.click(screen.getByTestId("viewport-grid-toggle"));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(viewportHost.setGridVisible.mock.calls.length).toBeGreaterThan(initialCallCount);
|
expect(viewportHost.setGridVisible.mock.calls.length).toBeGreaterThan(
|
||||||
|
initialCallCount
|
||||||
|
);
|
||||||
});
|
});
|
||||||
expect(viewportHost.setGridVisible.mock.calls.at(-1)?.[0]).toBe(false);
|
expect(viewportHost.setGridVisible.mock.calls.at(-1)?.[0]).toBe(false);
|
||||||
expect(screen.getByText(/viewport grid hidden\./i)).toBeInTheDocument();
|
expect(screen.getByText(/viewport grid hidden\./i)).toBeInTheDocument();
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
import { render, waitFor } from "@testing-library/react";
|
import { render, waitFor } from "@testing-library/react";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { createInactiveTransformSession, type ActiveTransformSession, type TransformSessionState } from "../../src/core/transform-session";
|
import {
|
||||||
|
createInactiveTransformSession,
|
||||||
|
type ActiveTransformSession,
|
||||||
|
type TransformSessionState
|
||||||
|
} from "../../src/core/transform-session";
|
||||||
import { createEmptySceneDocument } from "../../src/document/scene-document";
|
import { createEmptySceneDocument } from "../../src/document/scene-document";
|
||||||
import { ViewportCanvas } from "../../src/viewport-three/ViewportCanvas";
|
import { ViewportCanvas } from "../../src/viewport-three/ViewportCanvas";
|
||||||
import { createDefaultViewportPanelCameraState, type ViewportPanelCameraState } from "../../src/viewport-three/viewport-layout";
|
import {
|
||||||
import type { CreationViewportToolPreview, ViewportToolPreview } from "../../src/viewport-three/viewport-transient-state";
|
createDefaultViewportPanelCameraState,
|
||||||
|
type ViewportPanelCameraState
|
||||||
|
} from "../../src/viewport-three/viewport-layout";
|
||||||
|
import type {
|
||||||
|
CreationViewportToolPreview,
|
||||||
|
ViewportToolPreview
|
||||||
|
} from "../../src/viewport-three/viewport-transient-state";
|
||||||
|
|
||||||
const { MockViewportHost, viewportHostInstances } = vi.hoisted(() => {
|
const { MockViewportHost, viewportHostInstances } = vi.hoisted(() => {
|
||||||
const viewportHostInstances: Array<{
|
const viewportHostInstances: Array<{
|
||||||
@@ -81,7 +91,9 @@ vi.mock("../../src/viewport-three/viewport-host", () => ({
|
|||||||
describe("ViewportCanvas", () => {
|
describe("ViewportCanvas", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
viewportHostInstances.length = 0;
|
viewportHostInstances.length = 0;
|
||||||
vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockImplementation(() => ({}) as never);
|
vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockImplementation(
|
||||||
|
() => ({}) as never
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -100,10 +112,18 @@ describe("ViewportCanvas", () => {
|
|||||||
center: null
|
center: null
|
||||||
};
|
};
|
||||||
const onCommitCreation = vi.fn(() => true);
|
const onCommitCreation = vi.fn(() => true);
|
||||||
const onCameraStateChange = vi.fn((_cameraState: ViewportPanelCameraState) => undefined);
|
const onCameraStateChange = vi.fn(
|
||||||
const onToolPreviewChange = vi.fn((_toolPreview: ViewportToolPreview) => undefined);
|
(_cameraState: ViewportPanelCameraState) => undefined
|
||||||
const onTransformSessionChange = vi.fn((_transformSession: TransformSessionState) => undefined);
|
);
|
||||||
const onTransformCommit = vi.fn((_transformSession: ActiveTransformSession) => undefined);
|
const onToolPreviewChange = vi.fn(
|
||||||
|
(_toolPreview: ViewportToolPreview) => undefined
|
||||||
|
);
|
||||||
|
const onTransformSessionChange = vi.fn(
|
||||||
|
(_transformSession: TransformSessionState) => undefined
|
||||||
|
);
|
||||||
|
const onTransformCommit = vi.fn(
|
||||||
|
(_transformSession: ActiveTransformSession) => undefined
|
||||||
|
);
|
||||||
const onTransformCancel = vi.fn(() => undefined);
|
const onTransformCancel = vi.fn(() => undefined);
|
||||||
const onSelectionChange = vi.fn();
|
const onSelectionChange = vi.fn();
|
||||||
|
|
||||||
@@ -142,10 +162,13 @@ describe("ViewportCanvas", () => {
|
|||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(viewportHostInstances).toHaveLength(1);
|
expect(viewportHostInstances).toHaveLength(1);
|
||||||
expect(viewportHostInstances[0].setCreationCommitHandler).toHaveBeenCalledTimes(1);
|
expect(
|
||||||
|
viewportHostInstances[0].setCreationCommitHandler
|
||||||
|
).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
const registeredHandler = viewportHostInstances[0].setCreationCommitHandler.mock.calls[0][0] as (
|
const registeredHandler = viewportHostInstances[0].setCreationCommitHandler
|
||||||
|
.mock.calls[0][0] as (
|
||||||
toolPreview: CreationViewportToolPreview
|
toolPreview: CreationViewportToolPreview
|
||||||
) => boolean;
|
) => boolean;
|
||||||
|
|
||||||
@@ -156,7 +179,9 @@ describe("ViewportCanvas", () => {
|
|||||||
it("applies and subscribes to persisted camera state through the viewport host", async () => {
|
it("applies and subscribes to persisted camera state through the viewport host", async () => {
|
||||||
const sceneDocument = createEmptySceneDocument();
|
const sceneDocument = createEmptySceneDocument();
|
||||||
const cameraState = createDefaultViewportPanelCameraState();
|
const cameraState = createDefaultViewportPanelCameraState();
|
||||||
const onCameraStateChange = vi.fn((_cameraState: ViewportPanelCameraState) => undefined);
|
const onCameraStateChange = vi.fn(
|
||||||
|
(_cameraState: ViewportPanelCameraState) => undefined
|
||||||
|
);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<ViewportCanvas
|
<ViewportCanvas
|
||||||
@@ -193,8 +218,12 @@ describe("ViewportCanvas", () => {
|
|||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(viewportHostInstances).toHaveLength(1);
|
expect(viewportHostInstances).toHaveLength(1);
|
||||||
expect(viewportHostInstances[0].setCameraState).toHaveBeenCalledWith(cameraState);
|
expect(viewportHostInstances[0].setCameraState).toHaveBeenCalledWith(
|
||||||
expect(viewportHostInstances[0].setCameraStateChangeHandler).toHaveBeenCalledTimes(1);
|
cameraState
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
viewportHostInstances[0].setCameraStateChangeHandler
|
||||||
|
).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user