2026-04-03 02:09:14 +02:00
|
|
|
import { createOpaqueId } from "./ids";
|
2026-04-15 14:39:35 +02:00
|
|
|
import {
|
|
|
|
|
resolveSelectionActiveId,
|
|
|
|
|
type EditorSelection
|
|
|
|
|
} from "./selection";
|
2026-04-04 20:07:32 +02:00
|
|
|
import type { WhiteboxSelectionMode } from "./whitebox-selection-mode";
|
2026-04-03 02:09:14 +02:00
|
|
|
import type { Vec3 } from "./vector";
|
2026-04-05 01:55:52 +02:00
|
|
|
import {
|
2026-04-15 07:46:58 +02:00
|
|
|
cloneBrushGeometry,
|
|
|
|
|
createBoxBrush,
|
2026-04-15 09:14:19 +02:00
|
|
|
createConeBrush,
|
2026-04-15 07:46:58 +02:00
|
|
|
createRadialPrismBrush,
|
2026-04-15 09:14:19 +02:00
|
|
|
createTorusBrush,
|
2026-04-15 07:46:58 +02:00
|
|
|
createWedgeBrush,
|
|
|
|
|
type Brush,
|
|
|
|
|
type BrushGeometry,
|
|
|
|
|
type BrushKind,
|
2026-04-15 07:57:00 +02:00
|
|
|
type BoxBrushGeometry,
|
2026-04-15 09:14:19 +02:00
|
|
|
type ConeBrushGeometry,
|
2026-04-15 07:57:00 +02:00
|
|
|
type RadialPrismBrushGeometry,
|
2026-04-15 09:14:19 +02:00
|
|
|
type TorusBrushGeometry,
|
2026-04-15 07:57:00 +02:00
|
|
|
type WedgeBrushGeometry,
|
2026-04-15 07:46:58 +02:00
|
|
|
type WhiteboxEdgeId,
|
|
|
|
|
type WhiteboxFaceId,
|
|
|
|
|
type WhiteboxVertexId
|
2026-04-05 01:55:52 +02:00
|
|
|
} from "../document/brushes";
|
2026-04-13 22:26:00 +02:00
|
|
|
import { getScenePathPoint } from "../document/paths";
|
2026-04-03 02:09:14 +02:00
|
|
|
import type { SceneDocument } from "../document/scene-document";
|
|
|
|
|
import {
|
|
|
|
|
cloneEntityInstance,
|
|
|
|
|
getEntityKindLabel,
|
|
|
|
|
type EntityInstance,
|
|
|
|
|
type EntityKind
|
|
|
|
|
} from "../entities/entity-instances";
|
2026-04-11 02:44:48 +02:00
|
|
|
import {
|
|
|
|
|
cloneModelInstance,
|
|
|
|
|
getModelInstanceKindLabel
|
|
|
|
|
} from "../assets/model-instances";
|
2026-04-03 02:09:14 +02:00
|
|
|
import type { ViewportPanelId } from "../viewport-three/viewport-layout";
|
2026-04-15 07:46:58 +02:00
|
|
|
import {
|
|
|
|
|
getBrushEdgeLabel,
|
|
|
|
|
getBrushFaceLabel,
|
|
|
|
|
getBrushKindLabel,
|
|
|
|
|
getBrushVertexLabel
|
|
|
|
|
} from "../geometry/whitebox-topology";
|
2026-04-15 07:52:56 +02:00
|
|
|
import {
|
|
|
|
|
getBrushEdgeAxis,
|
|
|
|
|
getBrushEdgeScaleAxes,
|
|
|
|
|
getBrushFaceAxis
|
|
|
|
|
} from "../geometry/whitebox-brush";
|
2026-04-03 02:09:14 +02:00
|
|
|
|
|
|
|
|
export type TransformOperation = "translate" | "rotate" | "scale";
|
|
|
|
|
export type TransformAxis = "x" | "y" | "z";
|
2026-04-11 02:37:36 +02:00
|
|
|
export type TransformAxisSpace = "world" | "local";
|
2026-04-03 02:09:14 +02:00
|
|
|
export type TransformSessionSource = "keyboard" | "gizmo" | "toolbar";
|
|
|
|
|
|
|
|
|
|
export interface YawEntityRotationState {
|
|
|
|
|
kind: "yaw";
|
|
|
|
|
yawDegrees: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface DirectionEntityRotationState {
|
|
|
|
|
kind: "direction";
|
|
|
|
|
direction: Vec3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface NoEntityRotationState {
|
|
|
|
|
kind: "none";
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
export type EntityTransformRotationState =
|
|
|
|
|
| NoEntityRotationState
|
|
|
|
|
| YawEntityRotationState
|
|
|
|
|
| DirectionEntityRotationState;
|
2026-04-03 02:09:14 +02:00
|
|
|
|
|
|
|
|
export interface BrushTransformTarget {
|
|
|
|
|
kind: "brush";
|
|
|
|
|
brushId: string;
|
2026-04-15 07:46:58 +02:00
|
|
|
brushKind: BrushKind;
|
|
|
|
|
sideCount?: number;
|
2026-04-15 09:14:19 +02:00
|
|
|
majorSegmentCount?: number;
|
|
|
|
|
tubeSegmentCount?: number;
|
2026-04-03 02:09:14 +02:00
|
|
|
initialCenter: Vec3;
|
2026-04-04 19:26:06 +02:00
|
|
|
initialRotationDegrees: Vec3;
|
|
|
|
|
initialSize: Vec3;
|
2026-04-15 07:46:58 +02:00
|
|
|
initialGeometry: BrushGeometry;
|
2026-04-03 02:09:14 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-05 01:55:52 +02:00
|
|
|
export interface BrushFaceTransformTarget {
|
|
|
|
|
kind: "brushFace";
|
|
|
|
|
brushId: string;
|
2026-04-15 07:46:58 +02:00
|
|
|
brushKind: BrushKind;
|
|
|
|
|
sideCount?: number;
|
2026-04-15 09:14:19 +02:00
|
|
|
majorSegmentCount?: number;
|
|
|
|
|
tubeSegmentCount?: number;
|
2026-04-15 07:46:58 +02:00
|
|
|
faceId: WhiteboxFaceId;
|
2026-04-05 01:55:52 +02:00
|
|
|
initialCenter: Vec3;
|
|
|
|
|
initialRotationDegrees: Vec3;
|
|
|
|
|
initialSize: Vec3;
|
2026-04-15 07:46:58 +02:00
|
|
|
initialGeometry: BrushGeometry;
|
2026-04-05 01:55:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface BrushEdgeTransformTarget {
|
|
|
|
|
kind: "brushEdge";
|
|
|
|
|
brushId: string;
|
2026-04-15 07:46:58 +02:00
|
|
|
brushKind: BrushKind;
|
|
|
|
|
sideCount?: number;
|
2026-04-15 09:14:19 +02:00
|
|
|
majorSegmentCount?: number;
|
|
|
|
|
tubeSegmentCount?: number;
|
2026-04-15 07:46:58 +02:00
|
|
|
edgeId: WhiteboxEdgeId;
|
2026-04-05 01:55:52 +02:00
|
|
|
initialCenter: Vec3;
|
|
|
|
|
initialRotationDegrees: Vec3;
|
|
|
|
|
initialSize: Vec3;
|
2026-04-15 07:46:58 +02:00
|
|
|
initialGeometry: BrushGeometry;
|
2026-04-05 01:55:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface BrushVertexTransformTarget {
|
|
|
|
|
kind: "brushVertex";
|
|
|
|
|
brushId: string;
|
2026-04-15 07:46:58 +02:00
|
|
|
brushKind: BrushKind;
|
|
|
|
|
sideCount?: number;
|
2026-04-15 09:14:19 +02:00
|
|
|
majorSegmentCount?: number;
|
|
|
|
|
tubeSegmentCount?: number;
|
2026-04-15 07:46:58 +02:00
|
|
|
vertexId: WhiteboxVertexId;
|
2026-04-05 01:55:52 +02:00
|
|
|
initialCenter: Vec3;
|
|
|
|
|
initialRotationDegrees: Vec3;
|
|
|
|
|
initialSize: Vec3;
|
2026-04-15 07:46:58 +02:00
|
|
|
initialGeometry: BrushGeometry;
|
2026-04-05 01:55:52 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-03 02:09:14 +02:00
|
|
|
export interface ModelInstanceTransformTarget {
|
|
|
|
|
kind: "modelInstance";
|
|
|
|
|
modelInstanceId: string;
|
|
|
|
|
assetId: string;
|
|
|
|
|
initialPosition: Vec3;
|
|
|
|
|
initialRotationDegrees: Vec3;
|
|
|
|
|
initialScale: Vec3;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 22:26:00 +02:00
|
|
|
export interface PathPointTransformTarget {
|
|
|
|
|
kind: "pathPoint";
|
|
|
|
|
pathId: string;
|
|
|
|
|
pointId: string;
|
|
|
|
|
initialPosition: Vec3;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 02:09:14 +02:00
|
|
|
export interface EntityTransformTarget {
|
|
|
|
|
kind: "entity";
|
|
|
|
|
entityId: string;
|
|
|
|
|
entityKind: EntityKind;
|
|
|
|
|
initialPosition: Vec3;
|
|
|
|
|
initialRotation: EntityTransformRotationState;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 14:39:35 +02:00
|
|
|
export interface BrushesTransformTarget {
|
|
|
|
|
kind: "brushes";
|
|
|
|
|
activeBrushId: string;
|
|
|
|
|
initialPivot: Vec3;
|
|
|
|
|
items: BrushTransformTarget[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ModelInstancesTransformTarget {
|
|
|
|
|
kind: "modelInstances";
|
|
|
|
|
activeModelInstanceId: string;
|
|
|
|
|
initialPivot: Vec3;
|
|
|
|
|
items: ModelInstanceTransformTarget[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface EntitiesTransformTarget {
|
|
|
|
|
kind: "entities";
|
|
|
|
|
activeEntityId: string;
|
|
|
|
|
initialPivot: Vec3;
|
|
|
|
|
items: EntityTransformTarget[];
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 01:55:52 +02:00
|
|
|
export type TransformTarget =
|
|
|
|
|
| BrushTransformTarget
|
2026-04-15 14:39:35 +02:00
|
|
|
| BrushesTransformTarget
|
2026-04-05 01:55:52 +02:00
|
|
|
| BrushFaceTransformTarget
|
|
|
|
|
| BrushEdgeTransformTarget
|
|
|
|
|
| BrushVertexTransformTarget
|
|
|
|
|
| ModelInstanceTransformTarget
|
2026-04-15 14:39:35 +02:00
|
|
|
| ModelInstancesTransformTarget
|
2026-04-13 22:26:00 +02:00
|
|
|
| PathPointTransformTarget
|
2026-04-15 14:39:35 +02:00
|
|
|
| EntityTransformTarget
|
|
|
|
|
| EntitiesTransformTarget;
|
2026-04-03 02:09:14 +02:00
|
|
|
|
|
|
|
|
export interface BrushTransformPreview {
|
|
|
|
|
kind: "brush";
|
|
|
|
|
center: Vec3;
|
2026-04-04 19:26:06 +02:00
|
|
|
rotationDegrees: Vec3;
|
|
|
|
|
size: Vec3;
|
2026-04-15 07:46:58 +02:00
|
|
|
geometry: BrushGeometry;
|
2026-04-05 02:54:12 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
function areBrushGeometriesEqual(
|
2026-04-15 07:46:58 +02:00
|
|
|
left: BrushGeometry,
|
|
|
|
|
right: BrushGeometry
|
2026-04-11 02:44:48 +02:00
|
|
|
): boolean {
|
2026-04-15 07:46:58 +02:00
|
|
|
const leftVertexIds = Object.keys(left.vertices);
|
|
|
|
|
const rightVertexIds = Object.keys(right.vertices);
|
|
|
|
|
|
|
|
|
|
if (leftVertexIds.length !== rightVertexIds.length) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return leftVertexIds.every((vertexId) => {
|
2026-04-05 02:54:12 +02:00
|
|
|
const leftVertex = left.vertices[vertexId];
|
|
|
|
|
const rightVertex = right.vertices[vertexId];
|
2026-04-15 07:46:58 +02:00
|
|
|
return rightVertex !== undefined && areVec3Equal(leftVertex, rightVertex);
|
2026-04-05 02:54:12 +02:00
|
|
|
});
|
2026-04-03 02:09:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ModelInstanceTransformPreview {
|
|
|
|
|
kind: "modelInstance";
|
|
|
|
|
position: Vec3;
|
|
|
|
|
rotationDegrees: Vec3;
|
|
|
|
|
scale: Vec3;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 22:26:00 +02:00
|
|
|
export interface PathPointTransformPreview {
|
|
|
|
|
kind: "pathPoint";
|
|
|
|
|
position: Vec3;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 02:09:14 +02:00
|
|
|
export interface EntityTransformPreview {
|
|
|
|
|
kind: "entity";
|
|
|
|
|
position: Vec3;
|
|
|
|
|
rotation: EntityTransformRotationState;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 14:39:35 +02:00
|
|
|
export interface BrushTransformPreviewItem {
|
|
|
|
|
brushId: string;
|
|
|
|
|
center: Vec3;
|
|
|
|
|
rotationDegrees: Vec3;
|
|
|
|
|
size: Vec3;
|
|
|
|
|
geometry: BrushGeometry;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface BrushesTransformPreview {
|
|
|
|
|
kind: "brushes";
|
|
|
|
|
pivot: Vec3;
|
|
|
|
|
items: BrushTransformPreviewItem[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ModelInstanceTransformPreviewItem {
|
|
|
|
|
modelInstanceId: string;
|
|
|
|
|
position: Vec3;
|
|
|
|
|
rotationDegrees: Vec3;
|
|
|
|
|
scale: Vec3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ModelInstancesTransformPreview {
|
|
|
|
|
kind: "modelInstances";
|
|
|
|
|
pivot: Vec3;
|
|
|
|
|
items: ModelInstanceTransformPreviewItem[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface EntityTransformPreviewItem {
|
|
|
|
|
entityId: string;
|
|
|
|
|
position: Vec3;
|
|
|
|
|
rotation: EntityTransformRotationState;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface EntitiesTransformPreview {
|
|
|
|
|
kind: "entities";
|
|
|
|
|
pivot: Vec3;
|
|
|
|
|
items: EntityTransformPreviewItem[];
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
export type TransformPreview =
|
|
|
|
|
| BrushTransformPreview
|
2026-04-15 14:39:35 +02:00
|
|
|
| BrushesTransformPreview
|
2026-04-11 02:44:48 +02:00
|
|
|
| ModelInstanceTransformPreview
|
2026-04-15 14:39:35 +02:00
|
|
|
| ModelInstancesTransformPreview
|
2026-04-13 22:26:00 +02:00
|
|
|
| PathPointTransformPreview
|
2026-04-15 14:39:35 +02:00
|
|
|
| EntityTransformPreview
|
|
|
|
|
| EntitiesTransformPreview;
|
2026-04-03 02:09:14 +02:00
|
|
|
|
|
|
|
|
export interface ActiveTransformSession {
|
|
|
|
|
kind: "active";
|
|
|
|
|
id: string;
|
|
|
|
|
source: TransformSessionSource;
|
|
|
|
|
sourcePanelId: ViewportPanelId;
|
|
|
|
|
operation: TransformOperation;
|
2026-04-18 19:31:30 +02:00
|
|
|
surfaceSnapEnabled: boolean;
|
2026-04-03 02:09:14 +02:00
|
|
|
axisConstraint: TransformAxis | null;
|
2026-04-11 02:37:36 +02:00
|
|
|
axisConstraintSpace: TransformAxisSpace;
|
2026-04-03 02:09:14 +02:00
|
|
|
target: TransformTarget;
|
|
|
|
|
preview: TransformPreview;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type TransformSessionState = ActiveTransformSession | { kind: "none" };
|
|
|
|
|
|
|
|
|
|
export interface TransformTargetResolution {
|
|
|
|
|
target: TransformTarget | null;
|
|
|
|
|
message: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cloneVec3(vector: Vec3): Vec3 {
|
|
|
|
|
return {
|
|
|
|
|
x: vector.x,
|
|
|
|
|
y: vector.y,
|
|
|
|
|
z: vector.z
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function areVec3Equal(left: Vec3, right: Vec3): boolean {
|
|
|
|
|
return left.x === right.x && left.y === right.y && left.z === right.z;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 14:39:52 +02:00
|
|
|
function averageVec3(vectors: readonly Vec3[]): Vec3 {
|
|
|
|
|
if (vectors.length === 0) {
|
|
|
|
|
return {
|
|
|
|
|
x: 0,
|
|
|
|
|
y: 0,
|
|
|
|
|
z: 0
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const totals = vectors.reduce(
|
|
|
|
|
(accumulator, vector) => ({
|
|
|
|
|
x: accumulator.x + vector.x,
|
|
|
|
|
y: accumulator.y + vector.y,
|
|
|
|
|
z: accumulator.z + vector.z
|
|
|
|
|
}),
|
|
|
|
|
{
|
|
|
|
|
x: 0,
|
|
|
|
|
y: 0,
|
|
|
|
|
z: 0
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
x: totals.x / vectors.length,
|
|
|
|
|
y: totals.y / vectors.length,
|
|
|
|
|
z: totals.z / vectors.length
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
function cloneEntityTransformRotationState(
|
|
|
|
|
rotation: EntityTransformRotationState
|
|
|
|
|
): EntityTransformRotationState {
|
2026-04-03 02:09:14 +02:00
|
|
|
switch (rotation.kind) {
|
|
|
|
|
case "none":
|
|
|
|
|
return {
|
|
|
|
|
kind: "none"
|
|
|
|
|
};
|
|
|
|
|
case "yaw":
|
|
|
|
|
return {
|
|
|
|
|
kind: "yaw",
|
|
|
|
|
yawDegrees: rotation.yawDegrees
|
|
|
|
|
};
|
|
|
|
|
case "direction":
|
|
|
|
|
return {
|
|
|
|
|
kind: "direction",
|
|
|
|
|
direction: cloneVec3(rotation.direction)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 14:39:52 +02:00
|
|
|
function cloneBrushTransformPreviewItem(
|
|
|
|
|
preview: BrushTransformPreviewItem
|
|
|
|
|
): BrushTransformPreviewItem {
|
|
|
|
|
return {
|
|
|
|
|
brushId: preview.brushId,
|
|
|
|
|
center: cloneVec3(preview.center),
|
|
|
|
|
rotationDegrees: cloneVec3(preview.rotationDegrees),
|
|
|
|
|
size: cloneVec3(preview.size),
|
|
|
|
|
geometry: cloneBrushGeometry(preview.geometry)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cloneModelInstanceTransformPreviewItem(
|
|
|
|
|
preview: ModelInstanceTransformPreviewItem
|
|
|
|
|
): ModelInstanceTransformPreviewItem {
|
|
|
|
|
return {
|
|
|
|
|
modelInstanceId: preview.modelInstanceId,
|
|
|
|
|
position: cloneVec3(preview.position),
|
|
|
|
|
rotationDegrees: cloneVec3(preview.rotationDegrees),
|
|
|
|
|
scale: cloneVec3(preview.scale)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cloneEntityTransformPreviewItem(
|
|
|
|
|
preview: EntityTransformPreviewItem
|
|
|
|
|
): EntityTransformPreviewItem {
|
|
|
|
|
return {
|
|
|
|
|
entityId: preview.entityId,
|
|
|
|
|
position: cloneVec3(preview.position),
|
|
|
|
|
rotation: cloneEntityTransformRotationState(preview.rotation)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function areBrushTransformPreviewItemsEqual(
|
|
|
|
|
left: BrushTransformPreviewItem,
|
|
|
|
|
right: BrushTransformPreviewItem
|
|
|
|
|
): boolean {
|
|
|
|
|
return (
|
|
|
|
|
left.brushId === right.brushId &&
|
|
|
|
|
areVec3Equal(left.center, right.center) &&
|
|
|
|
|
areVec3Equal(left.rotationDegrees, right.rotationDegrees) &&
|
|
|
|
|
areVec3Equal(left.size, right.size) &&
|
|
|
|
|
areBrushGeometriesEqual(left.geometry, right.geometry)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function areModelInstanceTransformPreviewItemsEqual(
|
|
|
|
|
left: ModelInstanceTransformPreviewItem,
|
|
|
|
|
right: ModelInstanceTransformPreviewItem
|
|
|
|
|
): boolean {
|
|
|
|
|
return (
|
|
|
|
|
left.modelInstanceId === right.modelInstanceId &&
|
|
|
|
|
areVec3Equal(left.position, right.position) &&
|
|
|
|
|
areVec3Equal(left.rotationDegrees, right.rotationDegrees) &&
|
|
|
|
|
areVec3Equal(left.scale, right.scale)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function areEntityTransformPreviewItemsEqual(
|
|
|
|
|
left: EntityTransformPreviewItem,
|
|
|
|
|
right: EntityTransformPreviewItem
|
|
|
|
|
): boolean {
|
|
|
|
|
return (
|
|
|
|
|
left.entityId === right.entityId &&
|
|
|
|
|
areVec3Equal(left.position, right.position) &&
|
|
|
|
|
areEntityTransformRotationsEqual(left.rotation, right.rotation)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 07:46:58 +02:00
|
|
|
function createBrushSnapshotFromTarget(
|
|
|
|
|
target:
|
|
|
|
|
| BrushTransformTarget
|
|
|
|
|
| BrushFaceTransformTarget
|
|
|
|
|
| BrushEdgeTransformTarget
|
|
|
|
|
| BrushVertexTransformTarget
|
|
|
|
|
): Brush {
|
|
|
|
|
switch (target.brushKind) {
|
|
|
|
|
case "box":
|
|
|
|
|
return createBoxBrush({
|
|
|
|
|
id: target.brushId,
|
|
|
|
|
center: target.initialCenter,
|
|
|
|
|
rotationDegrees: target.initialRotationDegrees,
|
|
|
|
|
size: target.initialSize,
|
2026-04-15 07:57:00 +02:00
|
|
|
geometry: target.initialGeometry as BoxBrushGeometry
|
2026-04-15 07:46:58 +02:00
|
|
|
});
|
|
|
|
|
case "wedge":
|
|
|
|
|
return createWedgeBrush({
|
|
|
|
|
id: target.brushId,
|
|
|
|
|
center: target.initialCenter,
|
|
|
|
|
rotationDegrees: target.initialRotationDegrees,
|
|
|
|
|
size: target.initialSize,
|
2026-04-15 07:57:00 +02:00
|
|
|
geometry: target.initialGeometry as WedgeBrushGeometry
|
2026-04-15 07:46:58 +02:00
|
|
|
});
|
|
|
|
|
case "radialPrism":
|
|
|
|
|
return createRadialPrismBrush({
|
|
|
|
|
id: target.brushId,
|
|
|
|
|
center: target.initialCenter,
|
|
|
|
|
rotationDegrees: target.initialRotationDegrees,
|
|
|
|
|
size: target.initialSize,
|
|
|
|
|
sideCount: target.sideCount,
|
2026-04-15 07:57:00 +02:00
|
|
|
geometry: target.initialGeometry as RadialPrismBrushGeometry
|
2026-04-15 07:46:58 +02:00
|
|
|
});
|
2026-04-15 09:14:27 +02:00
|
|
|
case "cone":
|
|
|
|
|
return createConeBrush({
|
|
|
|
|
id: target.brushId,
|
|
|
|
|
center: target.initialCenter,
|
|
|
|
|
rotationDegrees: target.initialRotationDegrees,
|
|
|
|
|
size: target.initialSize,
|
|
|
|
|
sideCount: target.sideCount,
|
|
|
|
|
geometry: target.initialGeometry as ConeBrushGeometry
|
|
|
|
|
});
|
|
|
|
|
case "torus":
|
|
|
|
|
return createTorusBrush({
|
|
|
|
|
id: target.brushId,
|
|
|
|
|
center: target.initialCenter,
|
|
|
|
|
rotationDegrees: target.initialRotationDegrees,
|
|
|
|
|
size: target.initialSize,
|
|
|
|
|
majorSegmentCount: target.majorSegmentCount,
|
|
|
|
|
tubeSegmentCount: target.tubeSegmentCount,
|
|
|
|
|
geometry: target.initialGeometry as TorusBrushGeometry
|
|
|
|
|
});
|
2026-04-15 07:46:58 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
function areEntityTransformRotationsEqual(
|
|
|
|
|
left: EntityTransformRotationState,
|
|
|
|
|
right: EntityTransformRotationState
|
|
|
|
|
): boolean {
|
2026-04-03 02:09:14 +02:00
|
|
|
if (left.kind !== right.kind) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (left.kind) {
|
|
|
|
|
case "none":
|
|
|
|
|
return true;
|
|
|
|
|
case "yaw":
|
|
|
|
|
return right.kind === "yaw" && left.yawDegrees === right.yawDegrees;
|
|
|
|
|
case "direction":
|
2026-04-11 02:44:48 +02:00
|
|
|
return (
|
|
|
|
|
right.kind === "direction" &&
|
|
|
|
|
areVec3Equal(left.direction, right.direction)
|
|
|
|
|
);
|
2026-04-03 02:09:14 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function createInactiveTransformSession(): TransformSessionState {
|
|
|
|
|
return {
|
|
|
|
|
kind: "none"
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function cloneTransformTarget(target: TransformTarget): TransformTarget {
|
|
|
|
|
switch (target.kind) {
|
|
|
|
|
case "brush":
|
|
|
|
|
return {
|
|
|
|
|
kind: "brush",
|
|
|
|
|
brushId: target.brushId,
|
2026-04-15 07:46:58 +02:00
|
|
|
brushKind: target.brushKind,
|
|
|
|
|
sideCount: target.sideCount,
|
2026-04-15 09:14:53 +02:00
|
|
|
majorSegmentCount: target.majorSegmentCount,
|
|
|
|
|
tubeSegmentCount: target.tubeSegmentCount,
|
2026-04-04 19:26:06 +02:00
|
|
|
initialCenter: cloneVec3(target.initialCenter),
|
|
|
|
|
initialRotationDegrees: cloneVec3(target.initialRotationDegrees),
|
2026-04-05 02:54:12 +02:00
|
|
|
initialSize: cloneVec3(target.initialSize),
|
2026-04-15 07:46:58 +02:00
|
|
|
initialGeometry: cloneBrushGeometry(target.initialGeometry)
|
2026-04-03 02:09:14 +02:00
|
|
|
};
|
2026-04-15 14:40:03 +02:00
|
|
|
case "brushes":
|
|
|
|
|
return {
|
|
|
|
|
kind: "brushes",
|
|
|
|
|
activeBrushId: target.activeBrushId,
|
|
|
|
|
initialPivot: cloneVec3(target.initialPivot),
|
|
|
|
|
items: target.items.map(
|
|
|
|
|
(item) => cloneTransformTarget(item) as BrushTransformTarget
|
|
|
|
|
)
|
|
|
|
|
};
|
2026-04-05 01:55:52 +02:00
|
|
|
case "brushFace":
|
|
|
|
|
return {
|
|
|
|
|
kind: "brushFace",
|
|
|
|
|
brushId: target.brushId,
|
2026-04-15 07:46:58 +02:00
|
|
|
brushKind: target.brushKind,
|
|
|
|
|
sideCount: target.sideCount,
|
2026-04-15 09:14:53 +02:00
|
|
|
majorSegmentCount: target.majorSegmentCount,
|
|
|
|
|
tubeSegmentCount: target.tubeSegmentCount,
|
2026-04-05 01:55:52 +02:00
|
|
|
faceId: target.faceId,
|
|
|
|
|
initialCenter: cloneVec3(target.initialCenter),
|
|
|
|
|
initialRotationDegrees: cloneVec3(target.initialRotationDegrees),
|
2026-04-05 02:54:12 +02:00
|
|
|
initialSize: cloneVec3(target.initialSize),
|
2026-04-15 07:46:58 +02:00
|
|
|
initialGeometry: cloneBrushGeometry(target.initialGeometry)
|
2026-04-05 01:55:52 +02:00
|
|
|
};
|
|
|
|
|
case "brushEdge":
|
|
|
|
|
return {
|
|
|
|
|
kind: "brushEdge",
|
|
|
|
|
brushId: target.brushId,
|
2026-04-15 07:46:58 +02:00
|
|
|
brushKind: target.brushKind,
|
|
|
|
|
sideCount: target.sideCount,
|
2026-04-15 09:14:53 +02:00
|
|
|
majorSegmentCount: target.majorSegmentCount,
|
|
|
|
|
tubeSegmentCount: target.tubeSegmentCount,
|
2026-04-05 01:55:52 +02:00
|
|
|
edgeId: target.edgeId,
|
|
|
|
|
initialCenter: cloneVec3(target.initialCenter),
|
|
|
|
|
initialRotationDegrees: cloneVec3(target.initialRotationDegrees),
|
2026-04-05 02:54:12 +02:00
|
|
|
initialSize: cloneVec3(target.initialSize),
|
2026-04-15 07:46:58 +02:00
|
|
|
initialGeometry: cloneBrushGeometry(target.initialGeometry)
|
2026-04-05 01:55:52 +02:00
|
|
|
};
|
|
|
|
|
case "brushVertex":
|
|
|
|
|
return {
|
|
|
|
|
kind: "brushVertex",
|
|
|
|
|
brushId: target.brushId,
|
2026-04-15 07:46:58 +02:00
|
|
|
brushKind: target.brushKind,
|
|
|
|
|
sideCount: target.sideCount,
|
2026-04-15 09:14:53 +02:00
|
|
|
majorSegmentCount: target.majorSegmentCount,
|
|
|
|
|
tubeSegmentCount: target.tubeSegmentCount,
|
2026-04-05 01:55:52 +02:00
|
|
|
vertexId: target.vertexId,
|
|
|
|
|
initialCenter: cloneVec3(target.initialCenter),
|
|
|
|
|
initialRotationDegrees: cloneVec3(target.initialRotationDegrees),
|
2026-04-05 02:54:12 +02:00
|
|
|
initialSize: cloneVec3(target.initialSize),
|
2026-04-15 07:46:58 +02:00
|
|
|
initialGeometry: cloneBrushGeometry(target.initialGeometry)
|
2026-04-05 01:55:52 +02:00
|
|
|
};
|
2026-04-03 02:09:14 +02:00
|
|
|
case "modelInstance":
|
|
|
|
|
return {
|
|
|
|
|
kind: "modelInstance",
|
|
|
|
|
modelInstanceId: target.modelInstanceId,
|
|
|
|
|
assetId: target.assetId,
|
|
|
|
|
initialPosition: cloneVec3(target.initialPosition),
|
|
|
|
|
initialRotationDegrees: cloneVec3(target.initialRotationDegrees),
|
|
|
|
|
initialScale: cloneVec3(target.initialScale)
|
|
|
|
|
};
|
2026-04-15 14:40:03 +02:00
|
|
|
case "modelInstances":
|
|
|
|
|
return {
|
|
|
|
|
kind: "modelInstances",
|
|
|
|
|
activeModelInstanceId: target.activeModelInstanceId,
|
|
|
|
|
initialPivot: cloneVec3(target.initialPivot),
|
|
|
|
|
items: target.items.map(
|
|
|
|
|
(item) => cloneTransformTarget(item) as ModelInstanceTransformTarget
|
|
|
|
|
)
|
|
|
|
|
};
|
2026-04-13 22:26:00 +02:00
|
|
|
case "pathPoint":
|
|
|
|
|
return {
|
|
|
|
|
kind: "pathPoint",
|
|
|
|
|
pathId: target.pathId,
|
|
|
|
|
pointId: target.pointId,
|
|
|
|
|
initialPosition: cloneVec3(target.initialPosition)
|
|
|
|
|
};
|
2026-04-03 02:09:14 +02:00
|
|
|
case "entity":
|
|
|
|
|
return {
|
|
|
|
|
kind: "entity",
|
|
|
|
|
entityId: target.entityId,
|
|
|
|
|
entityKind: target.entityKind,
|
|
|
|
|
initialPosition: cloneVec3(target.initialPosition),
|
2026-04-11 02:44:48 +02:00
|
|
|
initialRotation: cloneEntityTransformRotationState(
|
|
|
|
|
target.initialRotation
|
|
|
|
|
)
|
2026-04-03 02:09:14 +02:00
|
|
|
};
|
2026-04-15 14:40:03 +02:00
|
|
|
case "entities":
|
|
|
|
|
return {
|
|
|
|
|
kind: "entities",
|
|
|
|
|
activeEntityId: target.activeEntityId,
|
|
|
|
|
initialPivot: cloneVec3(target.initialPivot),
|
|
|
|
|
items: target.items.map(
|
|
|
|
|
(item) => cloneTransformTarget(item) as EntityTransformTarget
|
|
|
|
|
)
|
|
|
|
|
};
|
2026-04-03 02:09:14 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
export function cloneTransformPreview(
|
|
|
|
|
preview: TransformPreview
|
|
|
|
|
): TransformPreview {
|
2026-04-03 02:09:14 +02:00
|
|
|
switch (preview.kind) {
|
|
|
|
|
case "brush":
|
|
|
|
|
return {
|
|
|
|
|
kind: "brush",
|
2026-04-04 19:26:06 +02:00
|
|
|
center: cloneVec3(preview.center),
|
|
|
|
|
rotationDegrees: cloneVec3(preview.rotationDegrees),
|
2026-04-05 02:54:12 +02:00
|
|
|
size: cloneVec3(preview.size),
|
2026-04-15 07:46:58 +02:00
|
|
|
geometry: cloneBrushGeometry(preview.geometry)
|
2026-04-03 02:09:14 +02:00
|
|
|
};
|
2026-04-15 14:40:12 +02:00
|
|
|
case "brushes":
|
|
|
|
|
return {
|
|
|
|
|
kind: "brushes",
|
|
|
|
|
pivot: cloneVec3(preview.pivot),
|
|
|
|
|
items: preview.items.map(cloneBrushTransformPreviewItem)
|
|
|
|
|
};
|
2026-04-03 02:09:14 +02:00
|
|
|
case "modelInstance":
|
|
|
|
|
return {
|
|
|
|
|
kind: "modelInstance",
|
|
|
|
|
position: cloneVec3(preview.position),
|
|
|
|
|
rotationDegrees: cloneVec3(preview.rotationDegrees),
|
|
|
|
|
scale: cloneVec3(preview.scale)
|
|
|
|
|
};
|
2026-04-15 14:40:12 +02:00
|
|
|
case "modelInstances":
|
|
|
|
|
return {
|
|
|
|
|
kind: "modelInstances",
|
|
|
|
|
pivot: cloneVec3(preview.pivot),
|
|
|
|
|
items: preview.items.map(cloneModelInstanceTransformPreviewItem)
|
|
|
|
|
};
|
2026-04-13 22:26:00 +02:00
|
|
|
case "pathPoint":
|
|
|
|
|
return {
|
|
|
|
|
kind: "pathPoint",
|
|
|
|
|
position: cloneVec3(preview.position)
|
|
|
|
|
};
|
2026-04-03 02:09:14 +02:00
|
|
|
case "entity":
|
|
|
|
|
return {
|
|
|
|
|
kind: "entity",
|
|
|
|
|
position: cloneVec3(preview.position),
|
|
|
|
|
rotation: cloneEntityTransformRotationState(preview.rotation)
|
|
|
|
|
};
|
2026-04-15 14:40:12 +02:00
|
|
|
case "entities":
|
|
|
|
|
return {
|
|
|
|
|
kind: "entities",
|
|
|
|
|
pivot: cloneVec3(preview.pivot),
|
|
|
|
|
items: preview.items.map(cloneEntityTransformPreviewItem)
|
|
|
|
|
};
|
2026-04-03 02:09:14 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
export function cloneTransformSession(
|
|
|
|
|
session: TransformSessionState
|
|
|
|
|
): TransformSessionState {
|
2026-04-03 02:09:14 +02:00
|
|
|
if (session.kind === "none") {
|
|
|
|
|
return session;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
kind: "active",
|
|
|
|
|
id: session.id,
|
|
|
|
|
source: session.source,
|
|
|
|
|
sourcePanelId: session.sourcePanelId,
|
|
|
|
|
operation: session.operation,
|
2026-04-18 19:31:30 +02:00
|
|
|
surfaceSnapEnabled: session.surfaceSnapEnabled,
|
2026-04-03 02:09:14 +02:00
|
|
|
axisConstraint: session.axisConstraint,
|
2026-04-11 02:37:36 +02:00
|
|
|
axisConstraintSpace: session.axisConstraintSpace,
|
2026-04-03 02:09:14 +02:00
|
|
|
target: cloneTransformTarget(session.target),
|
|
|
|
|
preview: cloneTransformPreview(session.preview)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
export function areTransformSessionsEqual(
|
|
|
|
|
left: TransformSessionState,
|
|
|
|
|
right: TransformSessionState
|
|
|
|
|
): boolean {
|
2026-04-03 02:09:14 +02:00
|
|
|
if (left.kind !== right.kind) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (left.kind === "none" || right.kind === "none") {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
left.id === right.id &&
|
|
|
|
|
left.source === right.source &&
|
|
|
|
|
left.sourcePanelId === right.sourcePanelId &&
|
|
|
|
|
left.operation === right.operation &&
|
2026-04-18 19:31:30 +02:00
|
|
|
left.surfaceSnapEnabled === right.surfaceSnapEnabled &&
|
2026-04-03 02:09:14 +02:00
|
|
|
left.axisConstraint === right.axisConstraint &&
|
2026-04-11 02:37:36 +02:00
|
|
|
left.axisConstraintSpace === right.axisConstraintSpace &&
|
2026-04-03 02:09:14 +02:00
|
|
|
areTransformTargetsEqual(left.target, right.target) &&
|
|
|
|
|
areTransformPreviewsEqual(left.preview, right.preview)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
function areTransformTargetsEqual(
|
|
|
|
|
left: TransformTarget,
|
|
|
|
|
right: TransformTarget
|
|
|
|
|
): boolean {
|
2026-04-03 02:09:14 +02:00
|
|
|
if (left.kind !== right.kind) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (left.kind) {
|
|
|
|
|
case "brush":
|
2026-04-04 19:26:06 +02:00
|
|
|
return (
|
|
|
|
|
right.kind === "brush" &&
|
|
|
|
|
left.brushId === right.brushId &&
|
2026-04-15 07:46:58 +02:00
|
|
|
left.brushKind === right.brushKind &&
|
|
|
|
|
left.sideCount === right.sideCount &&
|
2026-04-15 09:14:53 +02:00
|
|
|
left.majorSegmentCount === right.majorSegmentCount &&
|
|
|
|
|
left.tubeSegmentCount === right.tubeSegmentCount &&
|
2026-04-04 19:26:06 +02:00
|
|
|
areVec3Equal(left.initialCenter, right.initialCenter) &&
|
2026-04-11 02:44:48 +02:00
|
|
|
areVec3Equal(
|
|
|
|
|
left.initialRotationDegrees,
|
|
|
|
|
right.initialRotationDegrees
|
|
|
|
|
) &&
|
2026-04-05 02:54:12 +02:00
|
|
|
areVec3Equal(left.initialSize, right.initialSize) &&
|
|
|
|
|
areBrushGeometriesEqual(left.initialGeometry, right.initialGeometry)
|
2026-04-04 19:26:06 +02:00
|
|
|
);
|
2026-04-15 14:41:19 +02:00
|
|
|
case "brushes":
|
|
|
|
|
return (
|
|
|
|
|
right.kind === "brushes" &&
|
|
|
|
|
left.activeBrushId === right.activeBrushId &&
|
|
|
|
|
areVec3Equal(left.initialPivot, right.initialPivot) &&
|
|
|
|
|
left.items.length === right.items.length &&
|
|
|
|
|
left.items.every((item, index) =>
|
|
|
|
|
areTransformTargetsEqual(item, right.items[index])
|
|
|
|
|
)
|
|
|
|
|
);
|
2026-04-05 01:55:52 +02:00
|
|
|
case "brushFace":
|
|
|
|
|
return (
|
|
|
|
|
right.kind === "brushFace" &&
|
|
|
|
|
left.brushId === right.brushId &&
|
2026-04-15 07:46:58 +02:00
|
|
|
left.brushKind === right.brushKind &&
|
|
|
|
|
left.sideCount === right.sideCount &&
|
2026-04-15 09:14:53 +02:00
|
|
|
left.majorSegmentCount === right.majorSegmentCount &&
|
|
|
|
|
left.tubeSegmentCount === right.tubeSegmentCount &&
|
2026-04-05 01:55:52 +02:00
|
|
|
left.faceId === right.faceId &&
|
|
|
|
|
areVec3Equal(left.initialCenter, right.initialCenter) &&
|
2026-04-11 02:44:48 +02:00
|
|
|
areVec3Equal(
|
|
|
|
|
left.initialRotationDegrees,
|
|
|
|
|
right.initialRotationDegrees
|
|
|
|
|
) &&
|
2026-04-05 02:54:12 +02:00
|
|
|
areVec3Equal(left.initialSize, right.initialSize) &&
|
|
|
|
|
areBrushGeometriesEqual(left.initialGeometry, right.initialGeometry)
|
2026-04-05 01:55:52 +02:00
|
|
|
);
|
|
|
|
|
case "brushEdge":
|
|
|
|
|
return (
|
|
|
|
|
right.kind === "brushEdge" &&
|
|
|
|
|
left.brushId === right.brushId &&
|
2026-04-15 07:46:58 +02:00
|
|
|
left.brushKind === right.brushKind &&
|
|
|
|
|
left.sideCount === right.sideCount &&
|
2026-04-15 09:14:53 +02:00
|
|
|
left.majorSegmentCount === right.majorSegmentCount &&
|
|
|
|
|
left.tubeSegmentCount === right.tubeSegmentCount &&
|
2026-04-05 01:55:52 +02:00
|
|
|
left.edgeId === right.edgeId &&
|
|
|
|
|
areVec3Equal(left.initialCenter, right.initialCenter) &&
|
2026-04-11 02:44:48 +02:00
|
|
|
areVec3Equal(
|
|
|
|
|
left.initialRotationDegrees,
|
|
|
|
|
right.initialRotationDegrees
|
|
|
|
|
) &&
|
2026-04-05 02:54:12 +02:00
|
|
|
areVec3Equal(left.initialSize, right.initialSize) &&
|
|
|
|
|
areBrushGeometriesEqual(left.initialGeometry, right.initialGeometry)
|
2026-04-05 01:55:52 +02:00
|
|
|
);
|
|
|
|
|
case "brushVertex":
|
|
|
|
|
return (
|
|
|
|
|
right.kind === "brushVertex" &&
|
|
|
|
|
left.brushId === right.brushId &&
|
2026-04-15 07:46:58 +02:00
|
|
|
left.brushKind === right.brushKind &&
|
|
|
|
|
left.sideCount === right.sideCount &&
|
2026-04-15 09:14:53 +02:00
|
|
|
left.majorSegmentCount === right.majorSegmentCount &&
|
|
|
|
|
left.tubeSegmentCount === right.tubeSegmentCount &&
|
2026-04-05 01:55:52 +02:00
|
|
|
left.vertexId === right.vertexId &&
|
|
|
|
|
areVec3Equal(left.initialCenter, right.initialCenter) &&
|
2026-04-11 02:44:48 +02:00
|
|
|
areVec3Equal(
|
|
|
|
|
left.initialRotationDegrees,
|
|
|
|
|
right.initialRotationDegrees
|
|
|
|
|
) &&
|
2026-04-05 02:54:12 +02:00
|
|
|
areVec3Equal(left.initialSize, right.initialSize) &&
|
|
|
|
|
areBrushGeometriesEqual(left.initialGeometry, right.initialGeometry)
|
2026-04-05 01:55:52 +02:00
|
|
|
);
|
2026-04-03 02:09:14 +02:00
|
|
|
case "modelInstance":
|
|
|
|
|
return (
|
|
|
|
|
right.kind === "modelInstance" &&
|
|
|
|
|
left.modelInstanceId === right.modelInstanceId &&
|
|
|
|
|
left.assetId === right.assetId &&
|
|
|
|
|
areVec3Equal(left.initialPosition, right.initialPosition) &&
|
2026-04-11 02:44:48 +02:00
|
|
|
areVec3Equal(
|
|
|
|
|
left.initialRotationDegrees,
|
|
|
|
|
right.initialRotationDegrees
|
|
|
|
|
) &&
|
2026-04-03 02:09:14 +02:00
|
|
|
areVec3Equal(left.initialScale, right.initialScale)
|
|
|
|
|
);
|
2026-04-15 14:41:19 +02:00
|
|
|
case "modelInstances":
|
|
|
|
|
return (
|
|
|
|
|
right.kind === "modelInstances" &&
|
|
|
|
|
left.activeModelInstanceId === right.activeModelInstanceId &&
|
|
|
|
|
areVec3Equal(left.initialPivot, right.initialPivot) &&
|
|
|
|
|
left.items.length === right.items.length &&
|
|
|
|
|
left.items.every((item, index) =>
|
|
|
|
|
areTransformTargetsEqual(item, right.items[index])
|
|
|
|
|
)
|
|
|
|
|
);
|
2026-04-13 22:26:00 +02:00
|
|
|
case "pathPoint":
|
|
|
|
|
return (
|
|
|
|
|
right.kind === "pathPoint" &&
|
|
|
|
|
left.pathId === right.pathId &&
|
|
|
|
|
left.pointId === right.pointId &&
|
|
|
|
|
areVec3Equal(left.initialPosition, right.initialPosition)
|
|
|
|
|
);
|
2026-04-03 02:09:14 +02:00
|
|
|
case "entity":
|
|
|
|
|
return (
|
|
|
|
|
right.kind === "entity" &&
|
|
|
|
|
left.entityId === right.entityId &&
|
|
|
|
|
left.entityKind === right.entityKind &&
|
|
|
|
|
areVec3Equal(left.initialPosition, right.initialPosition) &&
|
2026-04-11 02:44:48 +02:00
|
|
|
areEntityTransformRotationsEqual(
|
|
|
|
|
left.initialRotation,
|
|
|
|
|
right.initialRotation
|
|
|
|
|
)
|
2026-04-03 02:09:14 +02:00
|
|
|
);
|
2026-04-15 14:41:19 +02:00
|
|
|
case "entities":
|
|
|
|
|
return (
|
|
|
|
|
right.kind === "entities" &&
|
|
|
|
|
left.activeEntityId === right.activeEntityId &&
|
|
|
|
|
areVec3Equal(left.initialPivot, right.initialPivot) &&
|
|
|
|
|
left.items.length === right.items.length &&
|
|
|
|
|
left.items.every((item, index) =>
|
|
|
|
|
areTransformTargetsEqual(item, right.items[index])
|
|
|
|
|
)
|
|
|
|
|
);
|
2026-04-03 02:09:14 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
function areTransformPreviewsEqual(
|
|
|
|
|
left: TransformPreview,
|
|
|
|
|
right: TransformPreview
|
|
|
|
|
): boolean {
|
2026-04-03 02:09:14 +02:00
|
|
|
if (left.kind !== right.kind) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (left.kind) {
|
|
|
|
|
case "brush":
|
2026-04-04 19:26:06 +02:00
|
|
|
return (
|
|
|
|
|
right.kind === "brush" &&
|
|
|
|
|
areVec3Equal(left.center, right.center) &&
|
|
|
|
|
areVec3Equal(left.rotationDegrees, right.rotationDegrees) &&
|
2026-04-05 02:54:12 +02:00
|
|
|
areVec3Equal(left.size, right.size) &&
|
|
|
|
|
areBrushGeometriesEqual(left.geometry, right.geometry)
|
2026-04-04 19:26:06 +02:00
|
|
|
);
|
2026-04-15 14:41:19 +02:00
|
|
|
case "brushes":
|
|
|
|
|
return (
|
|
|
|
|
right.kind === "brushes" &&
|
|
|
|
|
areVec3Equal(left.pivot, right.pivot) &&
|
|
|
|
|
left.items.length === right.items.length &&
|
|
|
|
|
left.items.every((item, index) =>
|
|
|
|
|
areBrushTransformPreviewItemsEqual(item, right.items[index])
|
|
|
|
|
)
|
|
|
|
|
);
|
2026-04-03 02:09:14 +02:00
|
|
|
case "modelInstance":
|
|
|
|
|
return (
|
|
|
|
|
right.kind === "modelInstance" &&
|
|
|
|
|
areVec3Equal(left.position, right.position) &&
|
|
|
|
|
areVec3Equal(left.rotationDegrees, right.rotationDegrees) &&
|
|
|
|
|
areVec3Equal(left.scale, right.scale)
|
|
|
|
|
);
|
2026-04-15 14:41:19 +02:00
|
|
|
case "modelInstances":
|
|
|
|
|
return (
|
|
|
|
|
right.kind === "modelInstances" &&
|
|
|
|
|
areVec3Equal(left.pivot, right.pivot) &&
|
|
|
|
|
left.items.length === right.items.length &&
|
|
|
|
|
left.items.every((item, index) =>
|
|
|
|
|
areModelInstanceTransformPreviewItemsEqual(item, right.items[index])
|
|
|
|
|
)
|
|
|
|
|
);
|
2026-04-13 22:26:00 +02:00
|
|
|
case "pathPoint":
|
|
|
|
|
return (
|
|
|
|
|
right.kind === "pathPoint" &&
|
|
|
|
|
areVec3Equal(left.position, right.position)
|
|
|
|
|
);
|
2026-04-03 02:09:14 +02:00
|
|
|
case "entity":
|
2026-04-11 02:44:48 +02:00
|
|
|
return (
|
|
|
|
|
right.kind === "entity" &&
|
|
|
|
|
areVec3Equal(left.position, right.position) &&
|
|
|
|
|
areEntityTransformRotationsEqual(left.rotation, right.rotation)
|
|
|
|
|
);
|
2026-04-15 14:41:19 +02:00
|
|
|
case "entities":
|
|
|
|
|
return (
|
|
|
|
|
right.kind === "entities" &&
|
|
|
|
|
areVec3Equal(left.pivot, right.pivot) &&
|
|
|
|
|
left.items.length === right.items.length &&
|
|
|
|
|
left.items.every((item, index) =>
|
|
|
|
|
areEntityTransformPreviewItemsEqual(item, right.items[index])
|
|
|
|
|
)
|
|
|
|
|
);
|
2026-04-03 02:09:14 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function createTransformSession(options: {
|
|
|
|
|
source: TransformSessionSource;
|
|
|
|
|
sourcePanelId: ViewportPanelId;
|
|
|
|
|
operation: TransformOperation;
|
2026-04-18 19:31:30 +02:00
|
|
|
surfaceSnapEnabled?: boolean;
|
2026-04-03 02:09:14 +02:00
|
|
|
axisConstraint?: TransformAxis | null;
|
2026-04-11 02:37:36 +02:00
|
|
|
axisConstraintSpace?: TransformAxisSpace;
|
2026-04-03 02:09:14 +02:00
|
|
|
target: TransformTarget;
|
|
|
|
|
}): ActiveTransformSession {
|
|
|
|
|
return {
|
|
|
|
|
kind: "active",
|
|
|
|
|
id: createOpaqueId("transform-session"),
|
|
|
|
|
source: options.source,
|
|
|
|
|
sourcePanelId: options.sourcePanelId,
|
|
|
|
|
operation: options.operation,
|
2026-04-18 19:31:30 +02:00
|
|
|
surfaceSnapEnabled: options.surfaceSnapEnabled ?? false,
|
2026-04-03 02:09:14 +02:00
|
|
|
axisConstraint: options.axisConstraint ?? null,
|
2026-04-11 02:37:36 +02:00
|
|
|
axisConstraintSpace: options.axisConstraintSpace ?? "world",
|
2026-04-03 02:09:14 +02:00
|
|
|
target: cloneTransformTarget(options.target),
|
|
|
|
|
preview: createTransformPreviewFromTarget(options.target)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
export function createTransformPreviewFromTarget(
|
|
|
|
|
target: TransformTarget
|
|
|
|
|
): TransformPreview {
|
2026-04-03 02:09:14 +02:00
|
|
|
switch (target.kind) {
|
|
|
|
|
case "brush":
|
2026-04-05 01:55:52 +02:00
|
|
|
case "brushFace":
|
|
|
|
|
case "brushEdge":
|
|
|
|
|
case "brushVertex":
|
2026-04-03 02:09:14 +02:00
|
|
|
return {
|
|
|
|
|
kind: "brush",
|
2026-04-04 19:26:06 +02:00
|
|
|
center: cloneVec3(target.initialCenter),
|
|
|
|
|
rotationDegrees: cloneVec3(target.initialRotationDegrees),
|
2026-04-05 02:54:12 +02:00
|
|
|
size: cloneVec3(target.initialSize),
|
2026-04-15 07:46:58 +02:00
|
|
|
geometry: cloneBrushGeometry(target.initialGeometry)
|
2026-04-03 02:09:14 +02:00
|
|
|
};
|
2026-04-15 14:41:19 +02:00
|
|
|
case "brushes":
|
|
|
|
|
return {
|
|
|
|
|
kind: "brushes",
|
|
|
|
|
pivot: cloneVec3(target.initialPivot),
|
|
|
|
|
items: target.items.map((item) => ({
|
|
|
|
|
brushId: item.brushId,
|
|
|
|
|
center: cloneVec3(item.initialCenter),
|
|
|
|
|
rotationDegrees: cloneVec3(item.initialRotationDegrees),
|
|
|
|
|
size: cloneVec3(item.initialSize),
|
|
|
|
|
geometry: cloneBrushGeometry(item.initialGeometry)
|
|
|
|
|
}))
|
|
|
|
|
};
|
2026-04-03 02:09:14 +02:00
|
|
|
case "modelInstance":
|
|
|
|
|
return {
|
|
|
|
|
kind: "modelInstance",
|
|
|
|
|
position: cloneVec3(target.initialPosition),
|
|
|
|
|
rotationDegrees: cloneVec3(target.initialRotationDegrees),
|
|
|
|
|
scale: cloneVec3(target.initialScale)
|
|
|
|
|
};
|
2026-04-15 14:41:19 +02:00
|
|
|
case "modelInstances":
|
|
|
|
|
return {
|
|
|
|
|
kind: "modelInstances",
|
|
|
|
|
pivot: cloneVec3(target.initialPivot),
|
|
|
|
|
items: target.items.map((item) => ({
|
|
|
|
|
modelInstanceId: item.modelInstanceId,
|
|
|
|
|
position: cloneVec3(item.initialPosition),
|
|
|
|
|
rotationDegrees: cloneVec3(item.initialRotationDegrees),
|
|
|
|
|
scale: cloneVec3(item.initialScale)
|
|
|
|
|
}))
|
|
|
|
|
};
|
2026-04-13 22:26:00 +02:00
|
|
|
case "pathPoint":
|
|
|
|
|
return {
|
|
|
|
|
kind: "pathPoint",
|
|
|
|
|
position: cloneVec3(target.initialPosition)
|
|
|
|
|
};
|
2026-04-03 02:09:14 +02:00
|
|
|
case "entity":
|
|
|
|
|
return {
|
|
|
|
|
kind: "entity",
|
|
|
|
|
position: cloneVec3(target.initialPosition),
|
|
|
|
|
rotation: cloneEntityTransformRotationState(target.initialRotation)
|
|
|
|
|
};
|
2026-04-15 14:41:19 +02:00
|
|
|
case "entities":
|
|
|
|
|
return {
|
|
|
|
|
kind: "entities",
|
|
|
|
|
pivot: cloneVec3(target.initialPivot),
|
|
|
|
|
items: target.items.map((item) => ({
|
|
|
|
|
entityId: item.entityId,
|
|
|
|
|
position: cloneVec3(item.initialPosition),
|
|
|
|
|
rotation: cloneEntityTransformRotationState(item.initialRotation)
|
|
|
|
|
}))
|
|
|
|
|
};
|
2026-04-03 02:09:14 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
export function doesTransformSessionChangeTarget(
|
|
|
|
|
session: ActiveTransformSession
|
|
|
|
|
): boolean {
|
2026-04-03 02:09:14 +02:00
|
|
|
switch (session.target.kind) {
|
|
|
|
|
case "brush":
|
2026-04-05 01:55:52 +02:00
|
|
|
case "brushFace":
|
|
|
|
|
case "brushEdge":
|
|
|
|
|
case "brushVertex":
|
2026-04-04 19:26:06 +02:00
|
|
|
return (
|
|
|
|
|
session.preview.kind === "brush" &&
|
|
|
|
|
(!areVec3Equal(session.preview.center, session.target.initialCenter) ||
|
2026-04-11 02:44:48 +02:00
|
|
|
!areVec3Equal(
|
|
|
|
|
session.preview.rotationDegrees,
|
|
|
|
|
session.target.initialRotationDegrees
|
|
|
|
|
) ||
|
2026-04-05 02:54:12 +02:00
|
|
|
!areVec3Equal(session.preview.size, session.target.initialSize) ||
|
2026-04-11 02:44:48 +02:00
|
|
|
!areBrushGeometriesEqual(
|
|
|
|
|
session.preview.geometry,
|
|
|
|
|
session.target.initialGeometry
|
|
|
|
|
))
|
2026-04-04 19:26:06 +02:00
|
|
|
);
|
2026-04-15 14:41:58 +02:00
|
|
|
case "brushes":
|
2026-04-15 15:00:34 +02:00
|
|
|
const brushesTarget = session.target;
|
2026-04-15 14:41:58 +02:00
|
|
|
return (
|
|
|
|
|
session.preview.kind === "brushes" &&
|
2026-04-15 15:00:34 +02:00
|
|
|
(!areVec3Equal(session.preview.pivot, brushesTarget.initialPivot) ||
|
|
|
|
|
session.preview.items.length !== brushesTarget.items.length ||
|
2026-04-15 14:41:58 +02:00
|
|
|
session.preview.items.some((item, index) => {
|
2026-04-15 15:00:34 +02:00
|
|
|
const targetItem = brushesTarget.items[index];
|
2026-04-15 14:41:58 +02:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
item.brushId !== targetItem.brushId ||
|
|
|
|
|
!areVec3Equal(item.center, targetItem.initialCenter) ||
|
|
|
|
|
!areVec3Equal(
|
|
|
|
|
item.rotationDegrees,
|
|
|
|
|
targetItem.initialRotationDegrees
|
|
|
|
|
) ||
|
|
|
|
|
!areVec3Equal(item.size, targetItem.initialSize) ||
|
|
|
|
|
!areBrushGeometriesEqual(item.geometry, targetItem.initialGeometry)
|
|
|
|
|
);
|
|
|
|
|
}))
|
|
|
|
|
);
|
2026-04-03 02:09:14 +02:00
|
|
|
case "modelInstance":
|
|
|
|
|
return (
|
|
|
|
|
session.preview.kind === "modelInstance" &&
|
2026-04-11 02:44:48 +02:00
|
|
|
(!areVec3Equal(
|
|
|
|
|
session.preview.position,
|
|
|
|
|
session.target.initialPosition
|
|
|
|
|
) ||
|
|
|
|
|
!areVec3Equal(
|
|
|
|
|
session.preview.rotationDegrees,
|
|
|
|
|
session.target.initialRotationDegrees
|
|
|
|
|
) ||
|
2026-04-03 02:09:14 +02:00
|
|
|
!areVec3Equal(session.preview.scale, session.target.initialScale))
|
|
|
|
|
);
|
2026-04-15 14:41:58 +02:00
|
|
|
case "modelInstances":
|
2026-04-15 15:00:34 +02:00
|
|
|
const modelInstancesTarget = session.target;
|
2026-04-15 14:41:58 +02:00
|
|
|
return (
|
|
|
|
|
session.preview.kind === "modelInstances" &&
|
2026-04-15 15:00:34 +02:00
|
|
|
(!areVec3Equal(
|
|
|
|
|
session.preview.pivot,
|
|
|
|
|
modelInstancesTarget.initialPivot
|
|
|
|
|
) ||
|
|
|
|
|
session.preview.items.length !== modelInstancesTarget.items.length ||
|
2026-04-15 14:41:58 +02:00
|
|
|
session.preview.items.some((item, index) => {
|
2026-04-15 15:00:34 +02:00
|
|
|
const targetItem = modelInstancesTarget.items[index];
|
2026-04-15 14:41:58 +02:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
item.modelInstanceId !== targetItem.modelInstanceId ||
|
|
|
|
|
!areVec3Equal(item.position, targetItem.initialPosition) ||
|
|
|
|
|
!areVec3Equal(
|
|
|
|
|
item.rotationDegrees,
|
|
|
|
|
targetItem.initialRotationDegrees
|
|
|
|
|
) ||
|
|
|
|
|
!areVec3Equal(item.scale, targetItem.initialScale)
|
|
|
|
|
);
|
|
|
|
|
}))
|
|
|
|
|
);
|
2026-04-13 22:26:00 +02:00
|
|
|
case "pathPoint":
|
|
|
|
|
return (
|
|
|
|
|
session.preview.kind === "pathPoint" &&
|
|
|
|
|
!areVec3Equal(
|
|
|
|
|
session.preview.position,
|
|
|
|
|
session.target.initialPosition
|
|
|
|
|
)
|
|
|
|
|
);
|
2026-04-03 02:09:14 +02:00
|
|
|
case "entity":
|
|
|
|
|
return (
|
|
|
|
|
session.preview.kind === "entity" &&
|
2026-04-11 02:44:48 +02:00
|
|
|
(!areVec3Equal(
|
|
|
|
|
session.preview.position,
|
|
|
|
|
session.target.initialPosition
|
|
|
|
|
) ||
|
|
|
|
|
!areEntityTransformRotationsEqual(
|
|
|
|
|
session.preview.rotation,
|
|
|
|
|
session.target.initialRotation
|
|
|
|
|
))
|
2026-04-03 02:09:14 +02:00
|
|
|
);
|
2026-04-15 14:41:58 +02:00
|
|
|
case "entities":
|
2026-04-15 15:00:34 +02:00
|
|
|
const entitiesTarget = session.target;
|
2026-04-15 14:41:58 +02:00
|
|
|
return (
|
|
|
|
|
session.preview.kind === "entities" &&
|
2026-04-15 15:00:34 +02:00
|
|
|
(!areVec3Equal(session.preview.pivot, entitiesTarget.initialPivot) ||
|
|
|
|
|
session.preview.items.length !== entitiesTarget.items.length ||
|
2026-04-15 14:41:58 +02:00
|
|
|
session.preview.items.some((item, index) => {
|
2026-04-15 15:00:34 +02:00
|
|
|
const targetItem = entitiesTarget.items[index];
|
2026-04-15 14:41:58 +02:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
item.entityId !== targetItem.entityId ||
|
|
|
|
|
!areVec3Equal(item.position, targetItem.initialPosition) ||
|
|
|
|
|
!areEntityTransformRotationsEqual(
|
|
|
|
|
item.rotation,
|
|
|
|
|
targetItem.initialRotation
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}))
|
|
|
|
|
);
|
2026-04-03 02:09:14 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
export function getTransformOperationLabel(
|
|
|
|
|
operation: TransformOperation
|
|
|
|
|
): string {
|
2026-04-03 02:09:14 +02:00
|
|
|
switch (operation) {
|
|
|
|
|
case "translate":
|
|
|
|
|
return "Move";
|
|
|
|
|
case "rotate":
|
|
|
|
|
return "Rotate";
|
|
|
|
|
case "scale":
|
|
|
|
|
return "Scale";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function getTransformAxisLabel(axis: TransformAxis): string {
|
|
|
|
|
return axis.toUpperCase();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
export function getTransformAxisSpaceLabel(
|
|
|
|
|
axisSpace: TransformAxisSpace
|
|
|
|
|
): string {
|
2026-04-11 02:37:36 +02:00
|
|
|
switch (axisSpace) {
|
|
|
|
|
case "world":
|
|
|
|
|
return "World";
|
|
|
|
|
case "local":
|
|
|
|
|
return "Local";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 02:09:14 +02:00
|
|
|
export function getTransformTargetLabel(target: TransformTarget): string {
|
|
|
|
|
switch (target.kind) {
|
|
|
|
|
case "brush":
|
2026-04-15 07:46:58 +02:00
|
|
|
return getBrushKindLabel(createBrushSnapshotFromTarget(target));
|
2026-04-15 14:42:34 +02:00
|
|
|
case "brushes":
|
|
|
|
|
return `${target.items.length} Whitebox Solid${target.items.length === 1 ? "" : "s"}`;
|
2026-04-05 01:55:52 +02:00
|
|
|
case "brushFace":
|
2026-04-15 07:46:58 +02:00
|
|
|
return `Whitebox Face (${getBrushFaceLabel(createBrushSnapshotFromTarget(target), target.faceId)})`;
|
2026-04-05 01:55:52 +02:00
|
|
|
case "brushEdge":
|
2026-04-15 07:46:58 +02:00
|
|
|
return `Whitebox Edge (${getBrushEdgeLabel(createBrushSnapshotFromTarget(target), target.edgeId)})`;
|
2026-04-05 01:55:52 +02:00
|
|
|
case "brushVertex":
|
2026-04-15 07:46:58 +02:00
|
|
|
return `Whitebox Vertex (${getBrushVertexLabel(createBrushSnapshotFromTarget(target), target.vertexId)})`;
|
2026-04-03 02:09:14 +02:00
|
|
|
case "modelInstance":
|
|
|
|
|
return getModelInstanceKindLabel();
|
2026-04-15 14:42:34 +02:00
|
|
|
case "modelInstances":
|
|
|
|
|
return `${target.items.length} ${getModelInstanceKindLabel()}${target.items.length === 1 ? "" : "s"}`;
|
2026-04-13 22:26:00 +02:00
|
|
|
case "pathPoint":
|
|
|
|
|
return "Path Point";
|
2026-04-03 02:09:14 +02:00
|
|
|
case "entity":
|
|
|
|
|
return getEntityKindLabel(target.entityKind);
|
2026-04-15 14:42:34 +02:00
|
|
|
case "entities":
|
|
|
|
|
return `${target.items.length} Entit${target.items.length === 1 ? "y" : "ies"}`;
|
2026-04-03 02:09:14 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
export function getSupportedTransformOperations(
|
|
|
|
|
target: TransformTarget
|
|
|
|
|
): TransformOperation[] {
|
2026-04-03 02:09:14 +02:00
|
|
|
switch (target.kind) {
|
|
|
|
|
case "brush":
|
2026-04-15 14:42:34 +02:00
|
|
|
case "brushes":
|
2026-04-05 01:55:52 +02:00
|
|
|
case "brushFace":
|
|
|
|
|
case "brushEdge":
|
2026-04-04 19:26:06 +02:00
|
|
|
return ["translate", "rotate", "scale"];
|
2026-04-05 01:55:52 +02:00
|
|
|
case "brushVertex":
|
2026-04-13 22:26:00 +02:00
|
|
|
case "pathPoint":
|
2026-04-05 01:55:52 +02:00
|
|
|
return ["translate"];
|
2026-04-03 02:09:14 +02:00
|
|
|
case "modelInstance":
|
2026-04-15 14:42:34 +02:00
|
|
|
case "modelInstances":
|
2026-04-03 02:09:14 +02:00
|
|
|
return ["translate", "rotate", "scale"];
|
|
|
|
|
case "entity":
|
2026-04-11 02:44:48 +02:00
|
|
|
return target.initialRotation.kind === "none"
|
|
|
|
|
? ["translate"]
|
|
|
|
|
: ["translate", "rotate"];
|
2026-04-15 14:42:34 +02:00
|
|
|
case "entities":
|
|
|
|
|
return target.items.every(
|
|
|
|
|
(item) => getSupportedTransformOperations(item).includes("rotate")
|
|
|
|
|
)
|
|
|
|
|
? ["translate", "rotate"]
|
|
|
|
|
: ["translate"];
|
2026-04-03 02:09:14 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
export function supportsTransformOperation(
|
|
|
|
|
target: TransformTarget,
|
|
|
|
|
operation: TransformOperation
|
|
|
|
|
): boolean {
|
2026-04-03 02:09:14 +02:00
|
|
|
return getSupportedTransformOperations(target).includes(operation);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-18 19:31:30 +02:00
|
|
|
export function supportsTransformSurfaceSnapTarget(
|
|
|
|
|
target: TransformTarget
|
|
|
|
|
): boolean {
|
|
|
|
|
switch (target.kind) {
|
|
|
|
|
case "brush":
|
|
|
|
|
case "brushes":
|
|
|
|
|
case "modelInstance":
|
|
|
|
|
case "modelInstances":
|
|
|
|
|
return true;
|
|
|
|
|
case "entity":
|
|
|
|
|
return target.entityKind === "triggerVolume";
|
|
|
|
|
case "entities":
|
|
|
|
|
return target.items.every((item) => item.entityKind === "triggerVolume");
|
|
|
|
|
case "brushFace":
|
|
|
|
|
case "brushEdge":
|
|
|
|
|
case "brushVertex":
|
|
|
|
|
case "pathPoint":
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
export function supportsTransformAxisConstraint(
|
|
|
|
|
session: ActiveTransformSession,
|
|
|
|
|
axis: TransformAxis
|
|
|
|
|
): boolean {
|
2026-04-15 07:52:56 +02:00
|
|
|
const brushTarget =
|
|
|
|
|
session.target.kind === "brush" ||
|
2026-04-15 14:42:34 +02:00
|
|
|
session.target.kind === "brushes" ||
|
2026-04-15 07:52:56 +02:00
|
|
|
session.target.kind === "brushFace" ||
|
|
|
|
|
session.target.kind === "brushEdge" ||
|
|
|
|
|
session.target.kind === "brushVertex"
|
2026-04-15 14:42:34 +02:00
|
|
|
? session.target.kind === "brushes"
|
|
|
|
|
? createBrushSnapshotFromTarget(session.target.items[0])
|
|
|
|
|
: createBrushSnapshotFromTarget(session.target)
|
2026-04-15 07:52:56 +02:00
|
|
|
: null;
|
|
|
|
|
|
2026-04-03 02:09:14 +02:00
|
|
|
switch (session.operation) {
|
|
|
|
|
case "translate":
|
|
|
|
|
return true;
|
|
|
|
|
case "scale":
|
2026-04-11 02:44:48 +02:00
|
|
|
if (
|
|
|
|
|
session.target.kind === "modelInstance" ||
|
2026-04-15 14:42:34 +02:00
|
|
|
session.target.kind === "modelInstances" ||
|
2026-04-13 22:29:57 +02:00
|
|
|
session.target.kind === "brush"
|
2026-04-15 14:42:34 +02:00
|
|
|
||
|
|
|
|
|
session.target.kind === "brushes"
|
2026-04-13 22:29:57 +02:00
|
|
|
) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
2026-04-13 22:26:00 +02:00
|
|
|
session.target.kind === "brushVertex" ||
|
|
|
|
|
session.target.kind === "pathPoint"
|
2026-04-11 02:44:48 +02:00
|
|
|
) {
|
2026-04-13 22:29:57 +02:00
|
|
|
return false;
|
2026-04-05 01:55:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (session.target.kind === "brushFace") {
|
2026-04-15 07:52:56 +02:00
|
|
|
if (brushTarget === null) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return axis === getBrushFaceAxis(brushTarget, session.target.faceId);
|
2026-04-05 01:55:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (session.target.kind === "brushEdge") {
|
2026-04-15 07:52:56 +02:00
|
|
|
if (brushTarget === null) {
|
|
|
|
|
return false;
|
2026-04-05 01:55:52 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-15 07:52:56 +02:00
|
|
|
return getBrushEdgeScaleAxes(brushTarget, session.target.edgeId).includes(
|
|
|
|
|
axis
|
|
|
|
|
);
|
2026-04-05 01:55:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
2026-04-03 02:09:14 +02:00
|
|
|
case "rotate":
|
2026-04-11 02:44:48 +02:00
|
|
|
if (
|
|
|
|
|
session.target.kind === "entity" &&
|
|
|
|
|
session.target.initialRotation.kind === "yaw"
|
|
|
|
|
) {
|
2026-04-03 02:09:14 +02:00
|
|
|
return axis === "y";
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 14:42:34 +02:00
|
|
|
if (session.target.kind === "entities") {
|
|
|
|
|
return session.target.items.every((item) =>
|
|
|
|
|
item.initialRotation.kind === "none"
|
|
|
|
|
? false
|
|
|
|
|
: item.initialRotation.kind === "yaw"
|
|
|
|
|
? axis === "y"
|
|
|
|
|
: true
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 01:55:52 +02:00
|
|
|
if (session.target.kind === "brushFace") {
|
2026-04-15 07:52:56 +02:00
|
|
|
if (brushTarget === null) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return axis === getBrushFaceAxis(brushTarget, session.target.faceId);
|
2026-04-05 01:55:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (session.target.kind === "brushEdge") {
|
2026-04-15 07:52:56 +02:00
|
|
|
if (brushTarget === null) {
|
|
|
|
|
return false;
|
2026-04-05 01:55:52 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-15 07:52:56 +02:00
|
|
|
return axis === getBrushEdgeAxis(brushTarget, session.target.edgeId);
|
2026-04-05 01:55:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (session.target.kind === "brushVertex") {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 19:26:06 +02:00
|
|
|
return true;
|
2026-04-03 02:09:14 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
export function supportsLocalTransformAxisConstraint(
|
|
|
|
|
session: ActiveTransformSession,
|
|
|
|
|
axis: TransformAxis
|
|
|
|
|
): boolean {
|
2026-04-11 02:37:36 +02:00
|
|
|
if (!supportsTransformAxisConstraint(session, axis)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (session.operation === "scale") {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (session.target.kind) {
|
|
|
|
|
case "brush":
|
2026-04-15 14:42:34 +02:00
|
|
|
case "brushes":
|
2026-04-11 02:37:36 +02:00
|
|
|
case "modelInstance":
|
2026-04-15 14:42:34 +02:00
|
|
|
case "modelInstances":
|
2026-04-11 02:37:36 +02:00
|
|
|
return true;
|
|
|
|
|
case "entity":
|
|
|
|
|
return session.target.initialRotation.kind !== "none";
|
2026-04-15 14:42:34 +02:00
|
|
|
case "entities":
|
|
|
|
|
return session.target.items.every(
|
|
|
|
|
(item) => item.initialRotation.kind !== "none"
|
|
|
|
|
);
|
2026-04-11 02:37:36 +02:00
|
|
|
case "brushFace":
|
2026-04-12 00:42:26 +02:00
|
|
|
case "brushEdge":
|
2026-04-12 00:45:06 +02:00
|
|
|
case "brushVertex":
|
2026-04-12 00:42:26 +02:00
|
|
|
return session.operation === "translate";
|
2026-04-13 22:26:00 +02:00
|
|
|
case "pathPoint":
|
|
|
|
|
return false;
|
2026-04-11 02:37:36 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
function resolveEntityRotation(
|
|
|
|
|
entity: EntityInstance
|
|
|
|
|
): EntityTransformRotationState {
|
2026-04-03 02:09:14 +02:00
|
|
|
switch (entity.kind) {
|
2026-04-22 16:59:02 +02:00
|
|
|
case "cameraRig":
|
|
|
|
|
return {
|
|
|
|
|
kind: "none"
|
|
|
|
|
};
|
2026-04-03 02:09:14 +02:00
|
|
|
case "playerStart":
|
2026-04-11 04:39:28 +02:00
|
|
|
case "sceneEntry":
|
2026-04-13 16:18:38 +02:00
|
|
|
case "npc":
|
2026-04-03 02:09:14 +02:00
|
|
|
case "teleportTarget":
|
|
|
|
|
return {
|
|
|
|
|
kind: "yaw",
|
|
|
|
|
yawDegrees: entity.yawDegrees
|
|
|
|
|
};
|
|
|
|
|
case "spotLight":
|
|
|
|
|
return {
|
|
|
|
|
kind: "direction",
|
|
|
|
|
direction: cloneVec3(entity.direction)
|
|
|
|
|
};
|
|
|
|
|
case "pointLight":
|
|
|
|
|
case "soundEmitter":
|
|
|
|
|
case "triggerVolume":
|
|
|
|
|
case "interactable":
|
|
|
|
|
return {
|
|
|
|
|
kind: "none"
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
function createBrushTransformTarget(
|
|
|
|
|
document: SceneDocument,
|
|
|
|
|
brushId: string
|
|
|
|
|
): TransformTargetResolution {
|
2026-04-03 02:09:14 +02:00
|
|
|
const brush = document.brushes[brushId];
|
|
|
|
|
|
2026-04-15 07:46:58 +02:00
|
|
|
if (brush === undefined) {
|
2026-04-03 02:09:14 +02:00
|
|
|
return {
|
|
|
|
|
target: null,
|
2026-04-15 07:46:58 +02:00
|
|
|
message: "Select a supported whitebox solid before transforming it."
|
2026-04-03 02:09:14 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
target: {
|
|
|
|
|
kind: "brush",
|
|
|
|
|
brushId: brush.id,
|
2026-04-15 07:46:58 +02:00
|
|
|
brushKind: brush.kind,
|
2026-04-15 09:15:09 +02:00
|
|
|
sideCount:
|
|
|
|
|
brush.kind === "radialPrism" || brush.kind === "cone"
|
|
|
|
|
? brush.sideCount
|
|
|
|
|
: undefined,
|
2026-04-15 09:15:01 +02:00
|
|
|
majorSegmentCount:
|
|
|
|
|
brush.kind === "torus" ? brush.majorSegmentCount : undefined,
|
|
|
|
|
tubeSegmentCount:
|
|
|
|
|
brush.kind === "torus" ? brush.tubeSegmentCount : undefined,
|
2026-04-04 19:26:06 +02:00
|
|
|
initialCenter: cloneVec3(brush.center),
|
|
|
|
|
initialRotationDegrees: cloneVec3(brush.rotationDegrees),
|
2026-04-05 02:54:12 +02:00
|
|
|
initialSize: cloneVec3(brush.size),
|
2026-04-15 07:46:58 +02:00
|
|
|
initialGeometry: cloneBrushGeometry(brush.geometry)
|
2026-04-03 02:09:14 +02:00
|
|
|
},
|
|
|
|
|
message: null
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 14:43:27 +02:00
|
|
|
function resolveActiveTransformTargetId(
|
|
|
|
|
ids: readonly string[],
|
|
|
|
|
activeSelectionId: string | null
|
|
|
|
|
): string | null {
|
|
|
|
|
if (activeSelectionId !== null && ids.includes(activeSelectionId)) {
|
|
|
|
|
return activeSelectionId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ids.at(-1) ?? null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createBrushesTransformTarget(
|
|
|
|
|
document: SceneDocument,
|
|
|
|
|
brushIds: string[],
|
|
|
|
|
activeSelectionId: string | null
|
|
|
|
|
): TransformTargetResolution {
|
|
|
|
|
const items: BrushTransformTarget[] = [];
|
|
|
|
|
|
|
|
|
|
for (const brushId of brushIds) {
|
|
|
|
|
const itemResolution = createBrushTransformTarget(document, brushId);
|
|
|
|
|
|
|
|
|
|
if (itemResolution.target === null || itemResolution.target.kind !== "brush") {
|
|
|
|
|
return itemResolution;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
items.push(itemResolution.target);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const activeBrushId = resolveActiveTransformTargetId(
|
|
|
|
|
brushIds,
|
|
|
|
|
activeSelectionId
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (activeBrushId === null) {
|
|
|
|
|
return {
|
|
|
|
|
target: null,
|
|
|
|
|
message: "Select at least one whitebox solid before transforming it."
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
target: {
|
|
|
|
|
kind: "brushes",
|
|
|
|
|
activeBrushId,
|
|
|
|
|
initialPivot: averageVec3(items.map((item) => item.initialCenter)),
|
|
|
|
|
items
|
|
|
|
|
},
|
|
|
|
|
message: null
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
function createBrushFaceTransformTarget(
|
|
|
|
|
document: SceneDocument,
|
|
|
|
|
brushId: string,
|
2026-04-15 07:46:58 +02:00
|
|
|
faceId: WhiteboxFaceId
|
2026-04-11 02:44:48 +02:00
|
|
|
): TransformTargetResolution {
|
2026-04-05 01:55:52 +02:00
|
|
|
const brushResolution = createBrushTransformTarget(document, brushId);
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
if (
|
|
|
|
|
brushResolution.target === null ||
|
|
|
|
|
brushResolution.target.kind !== "brush"
|
|
|
|
|
) {
|
2026-04-05 01:55:52 +02:00
|
|
|
return brushResolution;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
target: {
|
|
|
|
|
kind: "brushFace",
|
|
|
|
|
brushId,
|
2026-04-15 07:46:58 +02:00
|
|
|
brushKind: brushResolution.target.brushKind,
|
|
|
|
|
sideCount: brushResolution.target.sideCount,
|
2026-04-15 09:15:18 +02:00
|
|
|
majorSegmentCount: brushResolution.target.majorSegmentCount,
|
|
|
|
|
tubeSegmentCount: brushResolution.target.tubeSegmentCount,
|
2026-04-05 01:55:52 +02:00
|
|
|
faceId,
|
|
|
|
|
initialCenter: cloneVec3(brushResolution.target.initialCenter),
|
2026-04-11 02:44:48 +02:00
|
|
|
initialRotationDegrees: cloneVec3(
|
|
|
|
|
brushResolution.target.initialRotationDegrees
|
|
|
|
|
),
|
2026-04-05 02:54:12 +02:00
|
|
|
initialSize: cloneVec3(brushResolution.target.initialSize),
|
2026-04-15 07:46:58 +02:00
|
|
|
initialGeometry: cloneBrushGeometry(
|
2026-04-11 02:44:48 +02:00
|
|
|
brushResolution.target.initialGeometry
|
|
|
|
|
)
|
2026-04-05 01:55:52 +02:00
|
|
|
},
|
|
|
|
|
message: null
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
function createBrushEdgeTransformTarget(
|
|
|
|
|
document: SceneDocument,
|
|
|
|
|
brushId: string,
|
2026-04-15 07:46:58 +02:00
|
|
|
edgeId: WhiteboxEdgeId
|
2026-04-11 02:44:48 +02:00
|
|
|
): TransformTargetResolution {
|
2026-04-05 01:55:52 +02:00
|
|
|
const brushResolution = createBrushTransformTarget(document, brushId);
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
if (
|
|
|
|
|
brushResolution.target === null ||
|
|
|
|
|
brushResolution.target.kind !== "brush"
|
|
|
|
|
) {
|
2026-04-05 01:55:52 +02:00
|
|
|
return brushResolution;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
target: {
|
|
|
|
|
kind: "brushEdge",
|
|
|
|
|
brushId,
|
2026-04-15 07:46:58 +02:00
|
|
|
brushKind: brushResolution.target.brushKind,
|
|
|
|
|
sideCount: brushResolution.target.sideCount,
|
2026-04-15 09:15:18 +02:00
|
|
|
majorSegmentCount: brushResolution.target.majorSegmentCount,
|
|
|
|
|
tubeSegmentCount: brushResolution.target.tubeSegmentCount,
|
2026-04-05 01:55:52 +02:00
|
|
|
edgeId,
|
|
|
|
|
initialCenter: cloneVec3(brushResolution.target.initialCenter),
|
2026-04-11 02:44:48 +02:00
|
|
|
initialRotationDegrees: cloneVec3(
|
|
|
|
|
brushResolution.target.initialRotationDegrees
|
|
|
|
|
),
|
2026-04-05 02:54:12 +02:00
|
|
|
initialSize: cloneVec3(brushResolution.target.initialSize),
|
2026-04-15 07:46:58 +02:00
|
|
|
initialGeometry: cloneBrushGeometry(
|
2026-04-11 02:44:48 +02:00
|
|
|
brushResolution.target.initialGeometry
|
|
|
|
|
)
|
2026-04-05 01:55:52 +02:00
|
|
|
},
|
|
|
|
|
message: null
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
function createBrushVertexTransformTarget(
|
|
|
|
|
document: SceneDocument,
|
|
|
|
|
brushId: string,
|
2026-04-15 07:46:58 +02:00
|
|
|
vertexId: WhiteboxVertexId
|
2026-04-11 02:44:48 +02:00
|
|
|
): TransformTargetResolution {
|
2026-04-05 01:55:52 +02:00
|
|
|
const brushResolution = createBrushTransformTarget(document, brushId);
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
if (
|
|
|
|
|
brushResolution.target === null ||
|
|
|
|
|
brushResolution.target.kind !== "brush"
|
|
|
|
|
) {
|
2026-04-05 01:55:52 +02:00
|
|
|
return brushResolution;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
target: {
|
|
|
|
|
kind: "brushVertex",
|
|
|
|
|
brushId,
|
2026-04-15 07:46:58 +02:00
|
|
|
brushKind: brushResolution.target.brushKind,
|
|
|
|
|
sideCount: brushResolution.target.sideCount,
|
2026-04-15 09:15:18 +02:00
|
|
|
majorSegmentCount: brushResolution.target.majorSegmentCount,
|
|
|
|
|
tubeSegmentCount: brushResolution.target.tubeSegmentCount,
|
2026-04-05 01:55:52 +02:00
|
|
|
vertexId,
|
|
|
|
|
initialCenter: cloneVec3(brushResolution.target.initialCenter),
|
2026-04-11 02:44:48 +02:00
|
|
|
initialRotationDegrees: cloneVec3(
|
|
|
|
|
brushResolution.target.initialRotationDegrees
|
|
|
|
|
),
|
2026-04-05 02:54:12 +02:00
|
|
|
initialSize: cloneVec3(brushResolution.target.initialSize),
|
2026-04-15 07:46:58 +02:00
|
|
|
initialGeometry: cloneBrushGeometry(
|
2026-04-11 02:44:48 +02:00
|
|
|
brushResolution.target.initialGeometry
|
|
|
|
|
)
|
2026-04-05 01:55:52 +02:00
|
|
|
},
|
|
|
|
|
message: null
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
function createEntityTransformTarget(
|
|
|
|
|
document: SceneDocument,
|
|
|
|
|
entityId: string
|
|
|
|
|
): TransformTargetResolution {
|
2026-04-03 02:09:14 +02:00
|
|
|
const entity = document.entities[entityId];
|
|
|
|
|
|
|
|
|
|
if (entity === undefined) {
|
|
|
|
|
return {
|
|
|
|
|
target: null,
|
|
|
|
|
message: "Select an authored entity before transforming it."
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const clonedEntity = cloneEntityInstance(entity);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
target: {
|
|
|
|
|
kind: "entity",
|
|
|
|
|
entityId: clonedEntity.id,
|
|
|
|
|
entityKind: clonedEntity.kind,
|
|
|
|
|
initialPosition: cloneVec3(clonedEntity.position),
|
|
|
|
|
initialRotation: resolveEntityRotation(clonedEntity)
|
|
|
|
|
},
|
|
|
|
|
message: null
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 14:43:27 +02:00
|
|
|
function createEntitiesTransformTarget(
|
|
|
|
|
document: SceneDocument,
|
|
|
|
|
entityIds: string[],
|
|
|
|
|
activeSelectionId: string | null
|
|
|
|
|
): TransformTargetResolution {
|
|
|
|
|
const items: EntityTransformTarget[] = [];
|
|
|
|
|
|
|
|
|
|
for (const entityId of entityIds) {
|
|
|
|
|
const itemResolution = createEntityTransformTarget(document, entityId);
|
|
|
|
|
|
|
|
|
|
if (itemResolution.target === null || itemResolution.target.kind !== "entity") {
|
|
|
|
|
return itemResolution;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
items.push(itemResolution.target);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const activeEntityId = resolveActiveTransformTargetId(
|
|
|
|
|
entityIds,
|
|
|
|
|
activeSelectionId
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (activeEntityId === null) {
|
|
|
|
|
return {
|
|
|
|
|
target: null,
|
|
|
|
|
message: "Select at least one authored entity before transforming it."
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
target: {
|
|
|
|
|
kind: "entities",
|
|
|
|
|
activeEntityId,
|
|
|
|
|
initialPivot: averageVec3(items.map((item) => item.initialPosition)),
|
|
|
|
|
items
|
|
|
|
|
},
|
|
|
|
|
message: null
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
function createModelInstanceTransformTarget(
|
|
|
|
|
document: SceneDocument,
|
|
|
|
|
modelInstanceId: string
|
|
|
|
|
): TransformTargetResolution {
|
2026-04-03 02:09:14 +02:00
|
|
|
const modelInstance = document.modelInstances[modelInstanceId];
|
|
|
|
|
|
|
|
|
|
if (modelInstance === undefined) {
|
|
|
|
|
return {
|
|
|
|
|
target: null,
|
|
|
|
|
message: "Select a model instance before transforming it."
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const clonedModelInstance = cloneModelInstance(modelInstance);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
target: {
|
|
|
|
|
kind: "modelInstance",
|
|
|
|
|
modelInstanceId: clonedModelInstance.id,
|
|
|
|
|
assetId: clonedModelInstance.assetId,
|
|
|
|
|
initialPosition: cloneVec3(clonedModelInstance.position),
|
|
|
|
|
initialRotationDegrees: cloneVec3(clonedModelInstance.rotationDegrees),
|
|
|
|
|
initialScale: cloneVec3(clonedModelInstance.scale)
|
|
|
|
|
},
|
|
|
|
|
message: null
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 14:43:27 +02:00
|
|
|
function createModelInstancesTransformTarget(
|
|
|
|
|
document: SceneDocument,
|
|
|
|
|
modelInstanceIds: string[],
|
|
|
|
|
activeSelectionId: string | null
|
|
|
|
|
): TransformTargetResolution {
|
|
|
|
|
const items: ModelInstanceTransformTarget[] = [];
|
|
|
|
|
|
|
|
|
|
for (const modelInstanceId of modelInstanceIds) {
|
|
|
|
|
const itemResolution = createModelInstanceTransformTarget(
|
|
|
|
|
document,
|
|
|
|
|
modelInstanceId
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
itemResolution.target === null ||
|
|
|
|
|
itemResolution.target.kind !== "modelInstance"
|
|
|
|
|
) {
|
|
|
|
|
return itemResolution;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
items.push(itemResolution.target);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const activeModelInstanceId = resolveActiveTransformTargetId(
|
|
|
|
|
modelInstanceIds,
|
|
|
|
|
activeSelectionId
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (activeModelInstanceId === null) {
|
|
|
|
|
return {
|
|
|
|
|
target: null,
|
|
|
|
|
message: "Select at least one model instance before transforming it."
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
target: {
|
|
|
|
|
kind: "modelInstances",
|
|
|
|
|
activeModelInstanceId,
|
|
|
|
|
initialPivot: averageVec3(items.map((item) => item.initialPosition)),
|
|
|
|
|
items
|
|
|
|
|
},
|
|
|
|
|
message: null
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 22:26:00 +02:00
|
|
|
function createPathPointTransformTarget(
|
|
|
|
|
document: SceneDocument,
|
|
|
|
|
pathId: string,
|
|
|
|
|
pointId: string
|
|
|
|
|
): TransformTargetResolution {
|
|
|
|
|
const path = document.paths[pathId];
|
|
|
|
|
|
|
|
|
|
if (path === undefined) {
|
|
|
|
|
return {
|
|
|
|
|
target: null,
|
|
|
|
|
message: "Select a path point before transforming it."
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const point = getScenePathPoint(path, pointId);
|
|
|
|
|
|
|
|
|
|
if (point === null) {
|
|
|
|
|
return {
|
|
|
|
|
target: null,
|
|
|
|
|
message: "Select a valid path point before transforming it."
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
target: {
|
|
|
|
|
kind: "pathPoint",
|
|
|
|
|
pathId,
|
|
|
|
|
pointId,
|
|
|
|
|
initialPosition: cloneVec3(point.position)
|
|
|
|
|
},
|
|
|
|
|
message: null
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 20:07:32 +02:00
|
|
|
export function resolveTransformTarget(
|
|
|
|
|
document: SceneDocument,
|
|
|
|
|
selection: EditorSelection,
|
2026-04-15 14:43:27 +02:00
|
|
|
whiteboxSelectionMode: WhiteboxSelectionMode = "object",
|
|
|
|
|
activeSelectionId: string | null = resolveSelectionActiveId(selection, null)
|
2026-04-04 20:07:32 +02:00
|
|
|
): TransformTargetResolution {
|
2026-04-03 02:09:14 +02:00
|
|
|
switch (selection.kind) {
|
|
|
|
|
case "none":
|
|
|
|
|
return {
|
|
|
|
|
target: null,
|
2026-04-11 02:44:48 +02:00
|
|
|
message:
|
2026-04-13 22:26:18 +02:00
|
|
|
"Select a single brush, path point, entity, or model instance before transforming it."
|
2026-04-03 02:09:14 +02:00
|
|
|
};
|
|
|
|
|
case "brushFace":
|
2026-04-05 01:55:52 +02:00
|
|
|
if (whiteboxSelectionMode !== "face") {
|
|
|
|
|
return {
|
|
|
|
|
target: null,
|
|
|
|
|
message: "Switch to Face mode to transform a selected whitebox face."
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
return createBrushFaceTransformTarget(
|
|
|
|
|
document,
|
|
|
|
|
selection.brushId,
|
|
|
|
|
selection.faceId
|
|
|
|
|
);
|
2026-04-04 20:07:32 +02:00
|
|
|
case "brushEdge":
|
2026-04-05 01:55:52 +02:00
|
|
|
if (whiteboxSelectionMode !== "edge") {
|
|
|
|
|
return {
|
|
|
|
|
target: null,
|
|
|
|
|
message: "Switch to Edge mode to transform a selected whitebox edge."
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
return createBrushEdgeTransformTarget(
|
|
|
|
|
document,
|
|
|
|
|
selection.brushId,
|
|
|
|
|
selection.edgeId
|
|
|
|
|
);
|
2026-04-04 20:07:32 +02:00
|
|
|
case "brushVertex":
|
2026-04-05 01:55:52 +02:00
|
|
|
if (whiteboxSelectionMode !== "vertex") {
|
|
|
|
|
return {
|
|
|
|
|
target: null,
|
2026-04-11 02:44:48 +02:00
|
|
|
message:
|
|
|
|
|
"Switch to Vertex mode to transform a selected whitebox vertex."
|
2026-04-05 01:55:52 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 02:44:48 +02:00
|
|
|
return createBrushVertexTransformTarget(
|
|
|
|
|
document,
|
|
|
|
|
selection.brushId,
|
|
|
|
|
selection.vertexId
|
|
|
|
|
);
|
2026-04-03 02:09:14 +02:00
|
|
|
case "brushes":
|
2026-04-04 20:07:32 +02:00
|
|
|
if (whiteboxSelectionMode !== "object") {
|
|
|
|
|
return {
|
|
|
|
|
target: null,
|
2026-04-15 07:52:56 +02:00
|
|
|
message:
|
|
|
|
|
"Switch to Object mode to transform the whole whitebox solid."
|
2026-04-04 20:07:32 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 14:43:27 +02:00
|
|
|
if (selection.ids.length === 0) {
|
2026-04-03 02:09:14 +02:00
|
|
|
return {
|
|
|
|
|
target: null,
|
2026-04-15 14:43:27 +02:00
|
|
|
message: "Select at least one brush before transforming it."
|
2026-04-03 02:09:14 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 14:43:27 +02:00
|
|
|
return selection.ids.length === 1
|
|
|
|
|
? createBrushTransformTarget(document, selection.ids[0])
|
|
|
|
|
: createBrushesTransformTarget(document, selection.ids, activeSelectionId);
|
2026-04-18 19:55:54 +02:00
|
|
|
case "terrains":
|
|
|
|
|
return {
|
|
|
|
|
target: null,
|
|
|
|
|
message:
|
|
|
|
|
"Terrain transforms and editing tools land in a later inspector slice."
|
|
|
|
|
};
|
2026-04-03 02:09:14 +02:00
|
|
|
case "entities":
|
2026-04-15 14:43:27 +02:00
|
|
|
if (selection.ids.length === 0) {
|
2026-04-03 02:09:14 +02:00
|
|
|
return {
|
|
|
|
|
target: null,
|
2026-04-15 14:43:27 +02:00
|
|
|
message: "Select at least one entity before transforming it."
|
2026-04-03 02:09:14 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 14:43:27 +02:00
|
|
|
return selection.ids.length === 1
|
|
|
|
|
? createEntityTransformTarget(document, selection.ids[0])
|
|
|
|
|
: createEntitiesTransformTarget(document, selection.ids, activeSelectionId);
|
2026-04-13 21:21:40 +02:00
|
|
|
case "paths":
|
|
|
|
|
return {
|
|
|
|
|
target: null,
|
|
|
|
|
message:
|
2026-04-13 22:26:18 +02:00
|
|
|
"Select a path point before transforming a path."
|
2026-04-13 21:21:40 +02:00
|
|
|
};
|
2026-04-13 22:26:00 +02:00
|
|
|
case "pathPoint":
|
|
|
|
|
return createPathPointTransformTarget(
|
|
|
|
|
document,
|
|
|
|
|
selection.pathId,
|
|
|
|
|
selection.pointId
|
|
|
|
|
);
|
2026-04-03 02:09:14 +02:00
|
|
|
case "modelInstances":
|
2026-04-15 14:43:27 +02:00
|
|
|
if (selection.ids.length === 0) {
|
2026-04-03 02:09:14 +02:00
|
|
|
return {
|
|
|
|
|
target: null,
|
2026-04-15 14:43:27 +02:00
|
|
|
message: "Select at least one model instance before transforming it."
|
2026-04-03 02:09:14 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-15 14:43:27 +02:00
|
|
|
return selection.ids.length === 1
|
|
|
|
|
? createModelInstanceTransformTarget(document, selection.ids[0])
|
|
|
|
|
: createModelInstancesTransformTarget(
|
|
|
|
|
document,
|
|
|
|
|
selection.ids,
|
|
|
|
|
activeSelectionId
|
|
|
|
|
);
|
2026-04-03 02:09:14 +02:00
|
|
|
}
|
|
|
|
|
}
|