auto-git:
[add] playwright.config.js [add] src/app/App.js [add] src/app/editor-store.js [add] src/app/use-editor-store.js [add] src/assets/audio-assets.js [add] src/assets/gltf-model-import.js [add] src/assets/image-assets.js [add] src/assets/model-instance-labels.js [add] src/assets/model-instance-rendering.js [add] src/assets/model-instances.js [add] src/assets/project-asset-storage.js [add] src/assets/project-assets.js [add] src/commands/brush-command-helpers.js [add] src/commands/command-history.js [add] src/commands/command.js [add] src/commands/commit-transform-session-command.js [add] src/commands/create-box-brush-command.js [add] src/commands/delete-box-brush-command.js [add] src/commands/delete-entity-command.js [add] src/commands/delete-interaction-link-command.js [add] src/commands/delete-model-instance-command.js [add] src/commands/import-audio-asset-command.js [add] src/commands/import-background-image-asset-command.js [add] src/commands/import-model-asset-command.js [add] src/commands/move-box-brush-command.js [add] src/commands/resize-box-brush-command.js [add] src/commands/rotate-box-brush-command.js [add] src/commands/set-box-brush-face-material-command.js [add] src/commands/set-box-brush-face-uv-state-command.js [add] src/commands/set-box-brush-name-command.js [add] src/commands/set-box-brush-transform-command.js [add] src/commands/set-entity-name-command.js [add] src/commands/set-model-instance-name-command.js [add] src/commands/set-player-start-command.js [add] src/commands/set-scene-name-command.js [add] src/commands/set-world-settings-command.js [add] src/commands/upsert-entity-command.js [add] src/commands/upsert-interaction-link-command.js [add] src/commands/upsert-model-instance-command.js [add] src/core/ids.js [add] src/core/selection.js [add] src/core/tool-mode.js [add] src/core/transform-session.js [add] src/core/vector.js [add] src/core/whitebox-selection-feedback.js [add] src/core/whitebox-selection-mode.js [add] src/document/brushes.js [add] src/document/migrate-scene-document.js [add] src/document/scene-document-validation.js [add] src/document/scene-document.js [add] src/document/world-settings.js [add] src/entities/entity-instances.js [add] src/entities/entity-labels.js [add] src/geometry/box-brush-components.js [add] src/geometry/box-brush-mesh.js [add] src/geometry/box-brush.js [add] src/geometry/box-face-uvs.js [add] src/geometry/grid-snapping.js [add] src/geometry/model-instance-collider-debug-mesh.js [add] src/geometry/model-instance-collider-generation.js [add] src/interactions/interaction-links.js [add] src/main.js [add] src/materials/starter-material-library.js [add] src/materials/starter-material-textures.js [add] src/rendering/advanced-rendering.js [add] src/runner-web/RunnerCanvas.js [add] src/runtime-three/first-person-navigation-controller.js [add] src/runtime-three/navigation-controller.js [add] src/runtime-three/orbit-visitor-navigation-controller.js [add] src/runtime-three/player-collision.js [add] src/runtime-three/rapier-collision-world.js [add] src/runtime-three/runtime-audio-system.js [add] src/runtime-three/runtime-host.js [add] src/runtime-three/runtime-interaction-system.js [add] src/runtime-three/runtime-scene-build.js [add] src/runtime-three/runtime-scene-validation.js [add] src/serialization/local-draft-storage.js [add] src/serialization/scene-document-json.js [add] src/shared-ui/HierarchicalMenu.js [add] src/shared-ui/Panel.js [add] src/shared-ui/world-background-style.js [add] src/viewport-three/ViewportCanvas.js [add] src/viewport-three/ViewportPanel.js [add] src/viewport-three/viewport-entity-markers.js [add] src/viewport-three/viewport-focus.js [add] src/viewport-three/viewport-host.js [add] src/viewport-three/viewport-layout.js [add] src/viewport-three/viewport-transient-state.js [add] src/viewport-three/viewport-view-modes.js [add] tests/domain/box-brush-face-editing.command.test.js [add] tests/domain/build-runtime-scene.test.js [add] tests/domain/create-box-brush.command.test.js [add] tests/domain/create-empty-scene-document.test.js [add] tests/domain/editor-store.test.js [add] tests/domain/entity.command.test.js [add] tests/domain/interaction-links.validation.test.js [add] tests/domain/model-import.test.js [add] tests/domain/model-instance.command.test.js [add] tests/domain/player-start.command.test.js [add] tests/domain/rapier-collision-world.test.js [add] tests/domain/runtime-audio-system.test.js [add] tests/domain/runtime-interaction-system.test.js [add] tests/domain/runtime-scene-validation.test.js [add] tests/domain/scene-document-validation.test.js [add] tests/domain/transform-session.command.test.js [add] tests/domain/world-settings.command.test.js [add] tests/domain/world-settings.test.js [add] tests/e2e/app-smoke.e2e.js [add] tests/e2e/box-brush-authoring.e2e.js [add] tests/e2e/entities-foundation.e2e.js [add] tests/e2e/face-material-authoring.e2e.js [add] tests/e2e/first-room-workflow.e2e.js [add] tests/e2e/import-draco-model-asset.e2e.js [add] tests/e2e/import-external-model-asset.e2e.js [add] tests/e2e/import-model-asset.e2e.js [add] tests/e2e/local-lights-and-background.e2e.js [add] tests/e2e/orthographic-views.e2e.js [add] tests/e2e/runner-v1.e2e.js [add] tests/e2e/runtime-click-interaction.e2e.js [add] tests/e2e/runtime-trigger-teleport.e2e.js [add] tests/e2e/viewport-quad-layout.e2e.js [add] tests/e2e/viewport-test-helpers.js [add] tests/e2e/whitebox-component-selection.e2e.js [add] tests/e2e/world-environment.e2e.js [add] tests/geometry/box-brush-geometry.test.js [add] tests/geometry/box-face-uvs.test.js [add] tests/geometry/model-instance-collider-generation.test.js [add] tests/helpers/model-collider-fixtures.js [add] tests/serialization/local-draft-storage.test.js [add] tests/serialization/project-asset-storage.test.js [add] tests/serialization/scene-document-json.test.js [add] tests/setup/vitest.setup.js [add] tests/unit/audio-assets.test.js [add] tests/unit/entity-instances.test.js [add] tests/unit/package-scripts.test.js [add] tests/unit/transform-foundation.integration.test.js [add] tests/unit/viewport-canvas.test.js [add] tests/unit/viewport-entity-markers.test.js [add] tests/unit/viewport-focus.test.js [add] tests/unit/viewport-layout.test.js [add] tests/unit/viewport-view-modes.test.js [add] vite.config.js [add] vitest.config.js
This commit is contained in:
8
src/core/ids.js
Normal file
8
src/core/ids.js
Normal file
@@ -0,0 +1,8 @@
|
||||
let fallbackCounter = 0;
|
||||
export function createOpaqueId(prefix) {
|
||||
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
||||
return `${prefix}-${crypto.randomUUID()}`;
|
||||
}
|
||||
fallbackCounter += 1;
|
||||
return `${prefix}-${Date.now()}-${fallbackCounter}`;
|
||||
}
|
||||
134
src/core/selection.js
Normal file
134
src/core/selection.js
Normal file
@@ -0,0 +1,134 @@
|
||||
export function cloneEditorSelection(selection) {
|
||||
if (selection.kind === "none") {
|
||||
return {
|
||||
kind: "none"
|
||||
};
|
||||
}
|
||||
if (selection.kind === "brushFace") {
|
||||
return {
|
||||
kind: "brushFace",
|
||||
brushId: selection.brushId,
|
||||
faceId: selection.faceId
|
||||
};
|
||||
}
|
||||
if (selection.kind === "brushEdge") {
|
||||
return {
|
||||
kind: "brushEdge",
|
||||
brushId: selection.brushId,
|
||||
edgeId: selection.edgeId
|
||||
};
|
||||
}
|
||||
if (selection.kind === "brushVertex") {
|
||||
return {
|
||||
kind: "brushVertex",
|
||||
brushId: selection.brushId,
|
||||
vertexId: selection.vertexId
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: selection.kind,
|
||||
ids: [...selection.ids]
|
||||
};
|
||||
}
|
||||
export function areEditorSelectionsEqual(left, right) {
|
||||
if (left.kind !== right.kind) {
|
||||
return false;
|
||||
}
|
||||
switch (left.kind) {
|
||||
case "none":
|
||||
return true;
|
||||
case "brushFace":
|
||||
return right.kind === "brushFace" && left.brushId === right.brushId && left.faceId === right.faceId;
|
||||
case "brushEdge":
|
||||
return right.kind === "brushEdge" && left.brushId === right.brushId && left.edgeId === right.edgeId;
|
||||
case "brushVertex":
|
||||
return right.kind === "brushVertex" && left.brushId === right.brushId && left.vertexId === right.vertexId;
|
||||
case "brushes":
|
||||
case "entities":
|
||||
case "modelInstances":
|
||||
return right.kind === left.kind && left.ids.length === right.ids.length && left.ids.every((id, index) => id === right.ids[index]);
|
||||
}
|
||||
}
|
||||
export function getSingleSelectedBrushId(selection) {
|
||||
if (selection.kind === "brushFace" || selection.kind === "brushEdge" || selection.kind === "brushVertex") {
|
||||
return selection.brushId;
|
||||
}
|
||||
if (selection.kind !== "brushes" || selection.ids.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
return selection.ids[0];
|
||||
}
|
||||
export function getSelectedBrushFaceId(selection) {
|
||||
if (selection.kind !== "brushFace") {
|
||||
return null;
|
||||
}
|
||||
return selection.faceId;
|
||||
}
|
||||
export function getSelectedBrushEdgeId(selection) {
|
||||
if (selection.kind !== "brushEdge") {
|
||||
return null;
|
||||
}
|
||||
return selection.edgeId;
|
||||
}
|
||||
export function getSelectedBrushVertexId(selection) {
|
||||
if (selection.kind !== "brushVertex") {
|
||||
return null;
|
||||
}
|
||||
return selection.vertexId;
|
||||
}
|
||||
export function getSingleSelectedEntityId(selection) {
|
||||
if (selection.kind !== "entities" || selection.ids.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
return selection.ids[0];
|
||||
}
|
||||
export function getSingleSelectedModelInstanceId(selection) {
|
||||
if (selection.kind !== "modelInstances" || selection.ids.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
return selection.ids[0];
|
||||
}
|
||||
export function isBrushSelected(selection, brushId) {
|
||||
return ((selection.kind === "brushes" && selection.ids.includes(brushId)) ||
|
||||
((selection.kind === "brushFace" || selection.kind === "brushEdge" || selection.kind === "brushVertex") &&
|
||||
selection.brushId === brushId));
|
||||
}
|
||||
export function isBrushFaceSelected(selection, brushId, faceId) {
|
||||
return selection.kind === "brushFace" && selection.brushId === brushId && selection.faceId === faceId;
|
||||
}
|
||||
export function isBrushEdgeSelected(selection, brushId, edgeId) {
|
||||
return selection.kind === "brushEdge" && selection.brushId === brushId && selection.edgeId === edgeId;
|
||||
}
|
||||
export function isBrushVertexSelected(selection, brushId, vertexId) {
|
||||
return selection.kind === "brushVertex" && selection.brushId === brushId && selection.vertexId === vertexId;
|
||||
}
|
||||
export function isModelInstanceSelected(selection, modelInstanceId) {
|
||||
return selection.kind === "modelInstances" && selection.ids.includes(modelInstanceId);
|
||||
}
|
||||
export function normalizeSelectionForWhiteboxSelectionMode(selection, mode) {
|
||||
switch (selection.kind) {
|
||||
case "brushFace":
|
||||
return mode === "face"
|
||||
? selection
|
||||
: {
|
||||
kind: "brushes",
|
||||
ids: [selection.brushId]
|
||||
};
|
||||
case "brushEdge":
|
||||
return mode === "edge"
|
||||
? selection
|
||||
: {
|
||||
kind: "brushes",
|
||||
ids: [selection.brushId]
|
||||
};
|
||||
case "brushVertex":
|
||||
return mode === "vertex"
|
||||
? selection
|
||||
: {
|
||||
kind: "brushes",
|
||||
ids: [selection.brushId]
|
||||
};
|
||||
default:
|
||||
return selection;
|
||||
}
|
||||
}
|
||||
1
src/core/tool-mode.js
Normal file
1
src/core/tool-mode.js
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
573
src/core/transform-session.js
Normal file
573
src/core/transform-session.js
Normal file
@@ -0,0 +1,573 @@
|
||||
import { createOpaqueId } from "./ids";
|
||||
import { BOX_EDGE_LABELS, BOX_FACE_LABELS, BOX_VERTEX_LABELS } from "../document/brushes";
|
||||
import { cloneEntityInstance, getEntityKindLabel } from "../entities/entity-instances";
|
||||
import { cloneModelInstance, getModelInstanceKindLabel } from "../assets/model-instances";
|
||||
function cloneVec3(vector) {
|
||||
return {
|
||||
x: vector.x,
|
||||
y: vector.y,
|
||||
z: vector.z
|
||||
};
|
||||
}
|
||||
function areVec3Equal(left, right) {
|
||||
return left.x === right.x && left.y === right.y && left.z === right.z;
|
||||
}
|
||||
function cloneEntityTransformRotationState(rotation) {
|
||||
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)
|
||||
};
|
||||
}
|
||||
}
|
||||
function areEntityTransformRotationsEqual(left, right) {
|
||||
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":
|
||||
return right.kind === "direction" && areVec3Equal(left.direction, right.direction);
|
||||
}
|
||||
}
|
||||
export function createInactiveTransformSession() {
|
||||
return {
|
||||
kind: "none"
|
||||
};
|
||||
}
|
||||
export function cloneTransformTarget(target) {
|
||||
switch (target.kind) {
|
||||
case "brush":
|
||||
return {
|
||||
kind: "brush",
|
||||
brushId: target.brushId,
|
||||
initialCenter: cloneVec3(target.initialCenter),
|
||||
initialRotationDegrees: cloneVec3(target.initialRotationDegrees),
|
||||
initialSize: cloneVec3(target.initialSize)
|
||||
};
|
||||
case "brushFace":
|
||||
return {
|
||||
kind: "brushFace",
|
||||
brushId: target.brushId,
|
||||
faceId: target.faceId,
|
||||
initialCenter: cloneVec3(target.initialCenter),
|
||||
initialRotationDegrees: cloneVec3(target.initialRotationDegrees),
|
||||
initialSize: cloneVec3(target.initialSize)
|
||||
};
|
||||
case "brushEdge":
|
||||
return {
|
||||
kind: "brushEdge",
|
||||
brushId: target.brushId,
|
||||
edgeId: target.edgeId,
|
||||
initialCenter: cloneVec3(target.initialCenter),
|
||||
initialRotationDegrees: cloneVec3(target.initialRotationDegrees),
|
||||
initialSize: cloneVec3(target.initialSize)
|
||||
};
|
||||
case "brushVertex":
|
||||
return {
|
||||
kind: "brushVertex",
|
||||
brushId: target.brushId,
|
||||
vertexId: target.vertexId,
|
||||
initialCenter: cloneVec3(target.initialCenter),
|
||||
initialRotationDegrees: cloneVec3(target.initialRotationDegrees),
|
||||
initialSize: cloneVec3(target.initialSize)
|
||||
};
|
||||
case "modelInstance":
|
||||
return {
|
||||
kind: "modelInstance",
|
||||
modelInstanceId: target.modelInstanceId,
|
||||
assetId: target.assetId,
|
||||
initialPosition: cloneVec3(target.initialPosition),
|
||||
initialRotationDegrees: cloneVec3(target.initialRotationDegrees),
|
||||
initialScale: cloneVec3(target.initialScale)
|
||||
};
|
||||
case "entity":
|
||||
return {
|
||||
kind: "entity",
|
||||
entityId: target.entityId,
|
||||
entityKind: target.entityKind,
|
||||
initialPosition: cloneVec3(target.initialPosition),
|
||||
initialRotation: cloneEntityTransformRotationState(target.initialRotation)
|
||||
};
|
||||
}
|
||||
}
|
||||
export function cloneTransformPreview(preview) {
|
||||
switch (preview.kind) {
|
||||
case "brush":
|
||||
return {
|
||||
kind: "brush",
|
||||
center: cloneVec3(preview.center),
|
||||
rotationDegrees: cloneVec3(preview.rotationDegrees),
|
||||
size: cloneVec3(preview.size)
|
||||
};
|
||||
case "modelInstance":
|
||||
return {
|
||||
kind: "modelInstance",
|
||||
position: cloneVec3(preview.position),
|
||||
rotationDegrees: cloneVec3(preview.rotationDegrees),
|
||||
scale: cloneVec3(preview.scale)
|
||||
};
|
||||
case "entity":
|
||||
return {
|
||||
kind: "entity",
|
||||
position: cloneVec3(preview.position),
|
||||
rotation: cloneEntityTransformRotationState(preview.rotation)
|
||||
};
|
||||
}
|
||||
}
|
||||
export function cloneTransformSession(session) {
|
||||
if (session.kind === "none") {
|
||||
return session;
|
||||
}
|
||||
return {
|
||||
kind: "active",
|
||||
id: session.id,
|
||||
source: session.source,
|
||||
sourcePanelId: session.sourcePanelId,
|
||||
operation: session.operation,
|
||||
axisConstraint: session.axisConstraint,
|
||||
target: cloneTransformTarget(session.target),
|
||||
preview: cloneTransformPreview(session.preview)
|
||||
};
|
||||
}
|
||||
export function areTransformSessionsEqual(left, right) {
|
||||
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 &&
|
||||
left.axisConstraint === right.axisConstraint &&
|
||||
areTransformTargetsEqual(left.target, right.target) &&
|
||||
areTransformPreviewsEqual(left.preview, right.preview));
|
||||
}
|
||||
function areTransformTargetsEqual(left, right) {
|
||||
if (left.kind !== right.kind) {
|
||||
return false;
|
||||
}
|
||||
switch (left.kind) {
|
||||
case "brush":
|
||||
return (right.kind === "brush" &&
|
||||
left.brushId === right.brushId &&
|
||||
areVec3Equal(left.initialCenter, right.initialCenter) &&
|
||||
areVec3Equal(left.initialRotationDegrees, right.initialRotationDegrees) &&
|
||||
areVec3Equal(left.initialSize, right.initialSize));
|
||||
case "brushFace":
|
||||
return (right.kind === "brushFace" &&
|
||||
left.brushId === right.brushId &&
|
||||
left.faceId === right.faceId &&
|
||||
areVec3Equal(left.initialCenter, right.initialCenter) &&
|
||||
areVec3Equal(left.initialRotationDegrees, right.initialRotationDegrees) &&
|
||||
areVec3Equal(left.initialSize, right.initialSize));
|
||||
case "brushEdge":
|
||||
return (right.kind === "brushEdge" &&
|
||||
left.brushId === right.brushId &&
|
||||
left.edgeId === right.edgeId &&
|
||||
areVec3Equal(left.initialCenter, right.initialCenter) &&
|
||||
areVec3Equal(left.initialRotationDegrees, right.initialRotationDegrees) &&
|
||||
areVec3Equal(left.initialSize, right.initialSize));
|
||||
case "brushVertex":
|
||||
return (right.kind === "brushVertex" &&
|
||||
left.brushId === right.brushId &&
|
||||
left.vertexId === right.vertexId &&
|
||||
areVec3Equal(left.initialCenter, right.initialCenter) &&
|
||||
areVec3Equal(left.initialRotationDegrees, right.initialRotationDegrees) &&
|
||||
areVec3Equal(left.initialSize, right.initialSize));
|
||||
case "modelInstance":
|
||||
return (right.kind === "modelInstance" &&
|
||||
left.modelInstanceId === right.modelInstanceId &&
|
||||
left.assetId === right.assetId &&
|
||||
areVec3Equal(left.initialPosition, right.initialPosition) &&
|
||||
areVec3Equal(left.initialRotationDegrees, right.initialRotationDegrees) &&
|
||||
areVec3Equal(left.initialScale, right.initialScale));
|
||||
case "entity":
|
||||
return (right.kind === "entity" &&
|
||||
left.entityId === right.entityId &&
|
||||
left.entityKind === right.entityKind &&
|
||||
areVec3Equal(left.initialPosition, right.initialPosition) &&
|
||||
areEntityTransformRotationsEqual(left.initialRotation, right.initialRotation));
|
||||
}
|
||||
}
|
||||
function areTransformPreviewsEqual(left, right) {
|
||||
if (left.kind !== right.kind) {
|
||||
return false;
|
||||
}
|
||||
switch (left.kind) {
|
||||
case "brush":
|
||||
return (right.kind === "brush" &&
|
||||
areVec3Equal(left.center, right.center) &&
|
||||
areVec3Equal(left.rotationDegrees, right.rotationDegrees) &&
|
||||
areVec3Equal(left.size, right.size));
|
||||
case "modelInstance":
|
||||
return (right.kind === "modelInstance" &&
|
||||
areVec3Equal(left.position, right.position) &&
|
||||
areVec3Equal(left.rotationDegrees, right.rotationDegrees) &&
|
||||
areVec3Equal(left.scale, right.scale));
|
||||
case "entity":
|
||||
return right.kind === "entity" && areVec3Equal(left.position, right.position) && areEntityTransformRotationsEqual(left.rotation, right.rotation);
|
||||
}
|
||||
}
|
||||
export function createTransformSession(options) {
|
||||
return {
|
||||
kind: "active",
|
||||
id: createOpaqueId("transform-session"),
|
||||
source: options.source,
|
||||
sourcePanelId: options.sourcePanelId,
|
||||
operation: options.operation,
|
||||
axisConstraint: options.axisConstraint ?? null,
|
||||
target: cloneTransformTarget(options.target),
|
||||
preview: createTransformPreviewFromTarget(options.target)
|
||||
};
|
||||
}
|
||||
export function createTransformPreviewFromTarget(target) {
|
||||
switch (target.kind) {
|
||||
case "brush":
|
||||
case "brushFace":
|
||||
case "brushEdge":
|
||||
case "brushVertex":
|
||||
return {
|
||||
kind: "brush",
|
||||
center: cloneVec3(target.initialCenter),
|
||||
rotationDegrees: cloneVec3(target.initialRotationDegrees),
|
||||
size: cloneVec3(target.initialSize)
|
||||
};
|
||||
case "modelInstance":
|
||||
return {
|
||||
kind: "modelInstance",
|
||||
position: cloneVec3(target.initialPosition),
|
||||
rotationDegrees: cloneVec3(target.initialRotationDegrees),
|
||||
scale: cloneVec3(target.initialScale)
|
||||
};
|
||||
case "entity":
|
||||
return {
|
||||
kind: "entity",
|
||||
position: cloneVec3(target.initialPosition),
|
||||
rotation: cloneEntityTransformRotationState(target.initialRotation)
|
||||
};
|
||||
}
|
||||
}
|
||||
export function doesTransformSessionChangeTarget(session) {
|
||||
switch (session.target.kind) {
|
||||
case "brush":
|
||||
case "brushFace":
|
||||
case "brushEdge":
|
||||
case "brushVertex":
|
||||
return (session.preview.kind === "brush" &&
|
||||
(!areVec3Equal(session.preview.center, session.target.initialCenter) ||
|
||||
!areVec3Equal(session.preview.rotationDegrees, session.target.initialRotationDegrees) ||
|
||||
!areVec3Equal(session.preview.size, session.target.initialSize)));
|
||||
case "modelInstance":
|
||||
return (session.preview.kind === "modelInstance" &&
|
||||
(!areVec3Equal(session.preview.position, session.target.initialPosition) ||
|
||||
!areVec3Equal(session.preview.rotationDegrees, session.target.initialRotationDegrees) ||
|
||||
!areVec3Equal(session.preview.scale, session.target.initialScale)));
|
||||
case "entity":
|
||||
return (session.preview.kind === "entity" &&
|
||||
(!areVec3Equal(session.preview.position, session.target.initialPosition) ||
|
||||
!areEntityTransformRotationsEqual(session.preview.rotation, session.target.initialRotation)));
|
||||
}
|
||||
}
|
||||
export function getTransformOperationLabel(operation) {
|
||||
switch (operation) {
|
||||
case "translate":
|
||||
return "Move";
|
||||
case "rotate":
|
||||
return "Rotate";
|
||||
case "scale":
|
||||
return "Scale";
|
||||
}
|
||||
}
|
||||
export function getTransformAxisLabel(axis) {
|
||||
return axis.toUpperCase();
|
||||
}
|
||||
export function getTransformTargetLabel(target) {
|
||||
switch (target.kind) {
|
||||
case "brush":
|
||||
return "Whitebox Box";
|
||||
case "brushFace":
|
||||
return `Whitebox Face (${BOX_FACE_LABELS[target.faceId]})`;
|
||||
case "brushEdge":
|
||||
return `Whitebox Edge (${BOX_EDGE_LABELS[target.edgeId]})`;
|
||||
case "brushVertex":
|
||||
return `Whitebox Vertex (${BOX_VERTEX_LABELS[target.vertexId]})`;
|
||||
case "modelInstance":
|
||||
return getModelInstanceKindLabel();
|
||||
case "entity":
|
||||
return getEntityKindLabel(target.entityKind);
|
||||
}
|
||||
}
|
||||
export function getSupportedTransformOperations(target) {
|
||||
switch (target.kind) {
|
||||
case "brush":
|
||||
case "brushFace":
|
||||
case "brushEdge":
|
||||
return ["translate", "rotate", "scale"];
|
||||
case "brushVertex":
|
||||
return ["translate"];
|
||||
case "modelInstance":
|
||||
return ["translate", "rotate", "scale"];
|
||||
case "entity":
|
||||
return target.initialRotation.kind === "none" ? ["translate"] : ["translate", "rotate"];
|
||||
}
|
||||
}
|
||||
export function supportsTransformOperation(target, operation) {
|
||||
return getSupportedTransformOperations(target).includes(operation);
|
||||
}
|
||||
export function supportsTransformAxisConstraint(session, axis) {
|
||||
switch (session.operation) {
|
||||
case "translate":
|
||||
return true;
|
||||
case "scale":
|
||||
if (session.target.kind === "modelInstance" || session.target.kind === "brush" || session.target.kind === "brushVertex") {
|
||||
return session.target.kind !== "brushVertex";
|
||||
}
|
||||
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";
|
||||
return axis === normalAxis;
|
||||
}
|
||||
if (session.target.kind === "brushEdge") {
|
||||
if (session.target.edgeId.startsWith("edgeX_")) {
|
||||
return axis !== "x";
|
||||
}
|
||||
if (session.target.edgeId.startsWith("edgeY_")) {
|
||||
return axis !== "y";
|
||||
}
|
||||
return axis !== "z";
|
||||
}
|
||||
return false;
|
||||
case "rotate":
|
||||
if (session.target.kind === "entity" && session.target.initialRotation.kind === "yaw") {
|
||||
return axis === "y";
|
||||
}
|
||||
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";
|
||||
return axis === normalAxis;
|
||||
}
|
||||
if (session.target.kind === "brushEdge") {
|
||||
if (session.target.edgeId.startsWith("edgeX_")) {
|
||||
return axis === "x";
|
||||
}
|
||||
if (session.target.edgeId.startsWith("edgeY_")) {
|
||||
return axis === "y";
|
||||
}
|
||||
return axis === "z";
|
||||
}
|
||||
if (session.target.kind === "brushVertex") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
function resolveEntityRotation(entity) {
|
||||
switch (entity.kind) {
|
||||
case "playerStart":
|
||||
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"
|
||||
};
|
||||
}
|
||||
}
|
||||
function createBrushTransformTarget(document, brushId) {
|
||||
const brush = document.brushes[brushId];
|
||||
if (brush === undefined || brush.kind !== "box") {
|
||||
return {
|
||||
target: null,
|
||||
message: "Select a supported whitebox box before transforming it."
|
||||
};
|
||||
}
|
||||
return {
|
||||
target: {
|
||||
kind: "brush",
|
||||
brushId: brush.id,
|
||||
initialCenter: cloneVec3(brush.center),
|
||||
initialRotationDegrees: cloneVec3(brush.rotationDegrees),
|
||||
initialSize: cloneVec3(brush.size)
|
||||
},
|
||||
message: null
|
||||
};
|
||||
}
|
||||
function createBrushFaceTransformTarget(document, brushId, faceId) {
|
||||
const brushResolution = createBrushTransformTarget(document, brushId);
|
||||
if (brushResolution.target === null || brushResolution.target.kind !== "brush") {
|
||||
return brushResolution;
|
||||
}
|
||||
return {
|
||||
target: {
|
||||
kind: "brushFace",
|
||||
brushId,
|
||||
faceId,
|
||||
initialCenter: cloneVec3(brushResolution.target.initialCenter),
|
||||
initialRotationDegrees: cloneVec3(brushResolution.target.initialRotationDegrees),
|
||||
initialSize: cloneVec3(brushResolution.target.initialSize)
|
||||
},
|
||||
message: null
|
||||
};
|
||||
}
|
||||
function createBrushEdgeTransformTarget(document, brushId, edgeId) {
|
||||
const brushResolution = createBrushTransformTarget(document, brushId);
|
||||
if (brushResolution.target === null || brushResolution.target.kind !== "brush") {
|
||||
return brushResolution;
|
||||
}
|
||||
return {
|
||||
target: {
|
||||
kind: "brushEdge",
|
||||
brushId,
|
||||
edgeId,
|
||||
initialCenter: cloneVec3(brushResolution.target.initialCenter),
|
||||
initialRotationDegrees: cloneVec3(brushResolution.target.initialRotationDegrees),
|
||||
initialSize: cloneVec3(brushResolution.target.initialSize)
|
||||
},
|
||||
message: null
|
||||
};
|
||||
}
|
||||
function createBrushVertexTransformTarget(document, brushId, vertexId) {
|
||||
const brushResolution = createBrushTransformTarget(document, brushId);
|
||||
if (brushResolution.target === null || brushResolution.target.kind !== "brush") {
|
||||
return brushResolution;
|
||||
}
|
||||
return {
|
||||
target: {
|
||||
kind: "brushVertex",
|
||||
brushId,
|
||||
vertexId,
|
||||
initialCenter: cloneVec3(brushResolution.target.initialCenter),
|
||||
initialRotationDegrees: cloneVec3(brushResolution.target.initialRotationDegrees),
|
||||
initialSize: cloneVec3(brushResolution.target.initialSize)
|
||||
},
|
||||
message: null
|
||||
};
|
||||
}
|
||||
function createEntityTransformTarget(document, entityId) {
|
||||
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
|
||||
};
|
||||
}
|
||||
function createModelInstanceTransformTarget(document, modelInstanceId) {
|
||||
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
|
||||
};
|
||||
}
|
||||
export function resolveTransformTarget(document, selection, whiteboxSelectionMode = "object") {
|
||||
switch (selection.kind) {
|
||||
case "none":
|
||||
return {
|
||||
target: null,
|
||||
message: "Select a single brush, entity, or model instance before transforming it."
|
||||
};
|
||||
case "brushFace":
|
||||
if (whiteboxSelectionMode !== "face") {
|
||||
return {
|
||||
target: null,
|
||||
message: "Switch to Face mode to transform a selected whitebox face."
|
||||
};
|
||||
}
|
||||
return createBrushFaceTransformTarget(document, selection.brushId, selection.faceId);
|
||||
case "brushEdge":
|
||||
if (whiteboxSelectionMode !== "edge") {
|
||||
return {
|
||||
target: null,
|
||||
message: "Switch to Edge mode to transform a selected whitebox edge."
|
||||
};
|
||||
}
|
||||
return createBrushEdgeTransformTarget(document, selection.brushId, selection.edgeId);
|
||||
case "brushVertex":
|
||||
if (whiteboxSelectionMode !== "vertex") {
|
||||
return {
|
||||
target: null,
|
||||
message: "Switch to Vertex mode to transform a selected whitebox vertex."
|
||||
};
|
||||
}
|
||||
return createBrushVertexTransformTarget(document, selection.brushId, selection.vertexId);
|
||||
case "brushes":
|
||||
if (whiteboxSelectionMode !== "object") {
|
||||
return {
|
||||
target: null,
|
||||
message: "Switch to Object mode to transform the whole whitebox box."
|
||||
};
|
||||
}
|
||||
if (selection.ids.length !== 1) {
|
||||
return {
|
||||
target: null,
|
||||
message: "Select a single brush before transforming it."
|
||||
};
|
||||
}
|
||||
return createBrushTransformTarget(document, selection.ids[0]);
|
||||
case "entities":
|
||||
if (selection.ids.length !== 1) {
|
||||
return {
|
||||
target: null,
|
||||
message: "Select a single entity before transforming it."
|
||||
};
|
||||
}
|
||||
return createEntityTransformTarget(document, selection.ids[0]);
|
||||
case "modelInstances":
|
||||
if (selection.ids.length !== 1) {
|
||||
return {
|
||||
target: null,
|
||||
message: "Select a single model instance before transforming it."
|
||||
};
|
||||
}
|
||||
return createModelInstanceTransformTarget(document, selection.ids[0]);
|
||||
}
|
||||
}
|
||||
5
src/core/vector.js
Normal file
5
src/core/vector.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export const DEFAULT_SUN_DIRECTION = {
|
||||
x: -0.6,
|
||||
y: 1,
|
||||
z: 0.35
|
||||
};
|
||||
23
src/core/whitebox-selection-feedback.js
Normal file
23
src/core/whitebox-selection-feedback.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { BOX_EDGE_LABELS, BOX_FACE_LABELS, BOX_VERTEX_LABELS } from "../document/brushes";
|
||||
function getBrushDisplayLabel(document, brushId) {
|
||||
const brushes = Object.values(document.brushes);
|
||||
const brushIndex = brushes.findIndex((brush) => brush.id === brushId);
|
||||
if (brushIndex === -1) {
|
||||
return "Whitebox Box";
|
||||
}
|
||||
return brushes[brushIndex].name ?? `Whitebox Box ${brushIndex + 1}`;
|
||||
}
|
||||
export function getWhiteboxSelectionFeedbackLabel(document, selection) {
|
||||
switch (selection.kind) {
|
||||
case "brushes":
|
||||
return selection.ids.length === 1 ? `Solid · ${getBrushDisplayLabel(document, selection.ids[0])}` : null;
|
||||
case "brushFace":
|
||||
return `Face · ${BOX_FACE_LABELS[selection.faceId]} · ${getBrushDisplayLabel(document, selection.brushId)}`;
|
||||
case "brushEdge":
|
||||
return `Edge · ${BOX_EDGE_LABELS[selection.edgeId]} · ${getBrushDisplayLabel(document, selection.brushId)}`;
|
||||
case "brushVertex":
|
||||
return `Vertex · ${BOX_VERTEX_LABELS[selection.vertexId]} · ${getBrushDisplayLabel(document, selection.brushId)}`;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
10
src/core/whitebox-selection-mode.js
Normal file
10
src/core/whitebox-selection-mode.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export const WHITEBOX_SELECTION_MODES = ["object", "face", "edge", "vertex"];
|
||||
export const WHITEBOX_SELECTION_MODE_LABELS = {
|
||||
object: "Object",
|
||||
face: "Face",
|
||||
edge: "Edge",
|
||||
vertex: "Vertex"
|
||||
};
|
||||
export function getWhiteboxSelectionModeLabel(mode) {
|
||||
return WHITEBOX_SELECTION_MODE_LABELS[mode];
|
||||
}
|
||||
Reference in New Issue
Block a user