feat(editor): focus selected model editing
This commit is contained in:
@@ -28,6 +28,8 @@ interface EditorControlsProps {
|
|||||||
mapNodes: MapNode[];
|
mapNodes: MapNode[];
|
||||||
nodesCount: number;
|
nodesCount: number;
|
||||||
selectedNodeName: string | null;
|
selectedNodeName: string | null;
|
||||||
|
lockTerrainSelection: boolean;
|
||||||
|
onLockTerrainSelectionChange: (locked: boolean) => void;
|
||||||
isSelectionLocked: boolean;
|
isSelectionLocked: boolean;
|
||||||
onSelectionLockToggle: () => void;
|
onSelectionLockToggle: () => void;
|
||||||
onClearSelection: () => void;
|
onClearSelection: () => void;
|
||||||
@@ -90,6 +92,8 @@ export function EditorControls({
|
|||||||
mapNodes,
|
mapNodes,
|
||||||
nodesCount,
|
nodesCount,
|
||||||
selectedNodeName,
|
selectedNodeName,
|
||||||
|
lockTerrainSelection,
|
||||||
|
onLockTerrainSelectionChange,
|
||||||
isSelectionLocked,
|
isSelectionLocked,
|
||||||
onSelectionLockToggle,
|
onSelectionLockToggle,
|
||||||
onClearSelection,
|
onClearSelection,
|
||||||
@@ -105,6 +109,9 @@ export function EditorControls({
|
|||||||
}: EditorControlsProps): React.JSX.Element {
|
}: EditorControlsProps): React.JSX.Element {
|
||||||
const viewModeLabel = isPlayerMode ? "View locked" : "Lock view";
|
const viewModeLabel = isPlayerMode ? "View locked" : "Lock view";
|
||||||
const jsonPreview = getJsonPreview(mapNodes, selectedNodeIndex);
|
const jsonPreview = getJsonPreview(mapNodes, selectedNodeIndex);
|
||||||
|
const selectedNode =
|
||||||
|
selectedNodeIndex !== null ? mapNodes[selectedNodeIndex] : null;
|
||||||
|
const transformValues = getTransformValues(selectedNode ?? null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -155,7 +162,10 @@ export function EditorControls({
|
|||||||
aria-pressed={transformMode === mode}
|
aria-pressed={transformMode === mode}
|
||||||
>
|
>
|
||||||
<Icon size={16} aria-hidden="true" />
|
<Icon size={16} aria-hidden="true" />
|
||||||
<span>{label}</span>
|
<span className="editor-transform-label">
|
||||||
|
<span>{label}</span>
|
||||||
|
<small>{transformValues[mode]}</small>
|
||||||
|
</span>
|
||||||
<kbd>{shortcut}</kbd>
|
<kbd>{shortcut}</kbd>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -257,6 +267,20 @@ export function EditorControls({
|
|||||||
{viewModeLabel}
|
{viewModeLabel}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<label className="editor-checkbox-row">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={lockTerrainSelection}
|
||||||
|
onChange={(event) =>
|
||||||
|
onLockTerrainSelectionChange(event.currentTarget.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
<strong>Lock terrain</strong>
|
||||||
|
<small>Keep terrain visible but ignore terrain clicks</small>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
@@ -329,6 +353,42 @@ export function EditorControls({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatNumber(value: number): string {
|
||||||
|
return Number.isInteger(value) ? String(value) : value.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatVector(values: readonly [number, number, number]): string {
|
||||||
|
return `X ${formatNumber(values[0])} · Y ${formatNumber(values[1])} · Z ${formatNumber(values[2])}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRotation(values: readonly [number, number, number]): string {
|
||||||
|
const degrees = values.map((value) => (value * 180) / Math.PI) as [
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
number,
|
||||||
|
];
|
||||||
|
|
||||||
|
return `X ${formatNumber(degrees[0])}° · Y ${formatNumber(degrees[1])}° · Z ${formatNumber(degrees[2])}°`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTransformValues(
|
||||||
|
node: MapNode | null,
|
||||||
|
): Record<TransformMode, string> {
|
||||||
|
if (!node) {
|
||||||
|
return {
|
||||||
|
translate: "No selection",
|
||||||
|
rotate: "No selection",
|
||||||
|
scale: "No selection",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
translate: formatVector(node.position),
|
||||||
|
rotate: formatRotation(node.rotation),
|
||||||
|
scale: formatVector(node.scale),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface JsonPreviewLine {
|
interface JsonPreviewLine {
|
||||||
number: number;
|
number: number;
|
||||||
content: string;
|
content: string;
|
||||||
|
|||||||
@@ -3,9 +3,19 @@ import { Grid, TransformControls } from "@react-three/drei";
|
|||||||
import type { ThreeEvent } from "@react-three/fiber";
|
import type { ThreeEvent } from "@react-three/fiber";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
|
|
||||||
|
import { TerrainModel } from "@/components/three/world/TerrainModel";
|
||||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||||
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||||
|
import {
|
||||||
|
normalizeMapScale,
|
||||||
|
useTerrainSnappedPosition,
|
||||||
|
} from "@/hooks/three/useTerrainHeight";
|
||||||
import type { SceneData, MapNode, TransformMode } from "@/types/editor/editor";
|
import type { SceneData, MapNode, TransformMode } from "@/types/editor/editor";
|
||||||
|
import {
|
||||||
|
isEditorVisibleMapNode,
|
||||||
|
getTerrainMapNode,
|
||||||
|
} from "@/utils/map/mapRuntimeClassification";
|
||||||
|
import { getVegetationScaleMultiplier } from "@/world/vegetation/vegetationConfig";
|
||||||
|
|
||||||
interface EditorMapProps {
|
interface EditorMapProps {
|
||||||
sceneData: SceneData;
|
sceneData: SceneData;
|
||||||
@@ -15,6 +25,7 @@ interface EditorMapProps {
|
|||||||
hoveredNodeIndex: number | null;
|
hoveredNodeIndex: number | null;
|
||||||
onHoverNode: (index: number | null) => void;
|
onHoverNode: (index: number | null) => void;
|
||||||
transformMode: TransformMode;
|
transformMode: TransformMode;
|
||||||
|
lockTerrainSelection: boolean;
|
||||||
onTransformStart: () => void;
|
onTransformStart: () => void;
|
||||||
onTransformEnd: () => void;
|
onTransformEnd: () => void;
|
||||||
onNodeTransform: (nodeIndex: number, transform: MapNode) => void;
|
onNodeTransform: (nodeIndex: number, transform: MapNode) => void;
|
||||||
@@ -138,6 +149,7 @@ export function EditorMap({
|
|||||||
hoveredNodeIndex,
|
hoveredNodeIndex,
|
||||||
onHoverNode,
|
onHoverNode,
|
||||||
transformMode,
|
transformMode,
|
||||||
|
lockTerrainSelection,
|
||||||
onTransformStart,
|
onTransformStart,
|
||||||
onTransformEnd,
|
onTransformEnd,
|
||||||
onNodeTransform,
|
onNodeTransform,
|
||||||
@@ -169,6 +181,13 @@ export function EditorMap({
|
|||||||
const [selectedObject, setSelectedObject] = useState<THREE.Object3D | null>(
|
const [selectedObject, setSelectedObject] = useState<THREE.Object3D | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
const terrainNode = getTerrainMapNode(sceneData.mapNodes);
|
||||||
|
const terrainNodeIndex = terrainNode
|
||||||
|
? sceneData.mapNodes.indexOf(terrainNode)
|
||||||
|
: -1;
|
||||||
|
const selectedNode =
|
||||||
|
selectedNodeIndex !== null ? sceneData.mapNodes[selectedNodeIndex] : null;
|
||||||
|
const selectedModelName = selectedNode?.name ?? null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedNodeIndex !== null) {
|
if (selectedNodeIndex !== null) {
|
||||||
@@ -203,7 +222,28 @@ export function EditorMap({
|
|||||||
onSelectNode(null);
|
onSelectNode(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{terrainNode ? (
|
||||||
|
<EditorTerrainNode
|
||||||
|
index={terrainNodeIndex}
|
||||||
|
node={terrainNode}
|
||||||
|
isSelected={selectedNodeIndex === terrainNodeIndex}
|
||||||
|
isHovered={hoveredNodeIndex === terrainNodeIndex}
|
||||||
|
lockTerrainSelection={lockTerrainSelection}
|
||||||
|
objectsMapRef={objectsMapRef}
|
||||||
|
onSelectNode={onSelectNode}
|
||||||
|
isSelectionLocked={isSelectionLocked}
|
||||||
|
onHoverNode={onHoverNode}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{sceneData.mapNodes.map((node, index) => {
|
{sceneData.mapNodes.map((node, index) => {
|
||||||
|
if (!isEditorVisibleMapNode(node)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedModelName && node.name !== selectedModelName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const modelUrl = sceneData.models.get(node.name);
|
const modelUrl = sceneData.models.get(node.name);
|
||||||
|
|
||||||
if (modelUrl) {
|
if (modelUrl) {
|
||||||
@@ -265,14 +305,23 @@ function EditorModelNode({
|
|||||||
modelUrl: string;
|
modelUrl: string;
|
||||||
}) {
|
}) {
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
|
const snappedPosition = useTerrainSnappedPosition(node.position);
|
||||||
|
const vegetationScaleMultiplier = getVegetationScaleMultiplier(node.name);
|
||||||
|
const normalizedScale = vegetationScaleMultiplier
|
||||||
|
? ([
|
||||||
|
vegetationScaleMultiplier,
|
||||||
|
vegetationScaleMultiplier,
|
||||||
|
vegetationScaleMultiplier,
|
||||||
|
] satisfies MapNode["scale"])
|
||||||
|
: normalizeMapScale(node.scale);
|
||||||
const originalMaterialsRef = useRef(
|
const originalMaterialsRef = useRef(
|
||||||
new Map<THREE.Mesh, THREE.Material | THREE.Material[]>(),
|
new Map<THREE.Mesh, THREE.Material | THREE.Material[]>(),
|
||||||
);
|
);
|
||||||
const { scene } = useLoggedGLTF(modelUrl, {
|
const { scene } = useLoggedGLTF(modelUrl, {
|
||||||
scope: "EditorMap.EditorModelNode",
|
scope: "EditorMap.EditorModelNode",
|
||||||
position: node.position,
|
position: snappedPosition,
|
||||||
rotation: node.rotation,
|
rotation: node.rotation,
|
||||||
scale: node.scale,
|
scale: normalizedScale,
|
||||||
});
|
});
|
||||||
const sceneInstance = useClonedObject(scene);
|
const sceneInstance = useClonedObject(scene);
|
||||||
const pointerHandlers = createEditorNodePointerHandlers(
|
const pointerHandlers = createEditorNodePointerHandlers(
|
||||||
@@ -281,7 +330,16 @@ function EditorModelNode({
|
|||||||
isSelectionLocked,
|
isSelectionLocked,
|
||||||
onHoverNode,
|
onHoverNode,
|
||||||
);
|
);
|
||||||
useRegisteredEditorNode(groupRef, index, node, objectsMapRef);
|
useRegisteredEditorNode(
|
||||||
|
groupRef,
|
||||||
|
index,
|
||||||
|
{
|
||||||
|
...node,
|
||||||
|
position: snappedPosition,
|
||||||
|
scale: normalizedScale,
|
||||||
|
},
|
||||||
|
objectsMapRef,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!groupRef.current) return;
|
if (!groupRef.current) return;
|
||||||
@@ -338,11 +396,42 @@ function EditorModelNode({
|
|||||||
<primitive
|
<primitive
|
||||||
ref={groupRef}
|
ref={groupRef}
|
||||||
object={sceneInstance}
|
object={sceneInstance}
|
||||||
|
position={snappedPosition}
|
||||||
|
rotation={node.rotation}
|
||||||
|
scale={normalizedScale}
|
||||||
|
{...pointerHandlers}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditorTerrainNode({
|
||||||
|
index,
|
||||||
|
node,
|
||||||
|
lockTerrainSelection,
|
||||||
|
objectsMapRef,
|
||||||
|
onSelectNode,
|
||||||
|
isSelectionLocked,
|
||||||
|
onHoverNode,
|
||||||
|
}: EditorNodeCommonProps & { lockTerrainSelection: boolean }) {
|
||||||
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
|
const pointerHandlers = createEditorNodePointerHandlers(
|
||||||
|
index,
|
||||||
|
onSelectNode,
|
||||||
|
isSelectionLocked,
|
||||||
|
onHoverNode,
|
||||||
|
);
|
||||||
|
useRegisteredEditorNode(groupRef, index, node, objectsMapRef);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group
|
||||||
|
ref={groupRef}
|
||||||
position={node.position}
|
position={node.position}
|
||||||
rotation={node.rotation}
|
rotation={node.rotation}
|
||||||
scale={node.scale}
|
scale={node.scale}
|
||||||
{...pointerHandlers}
|
{...(lockTerrainSelection ? {} : pointerHandlers)}
|
||||||
/>
|
>
|
||||||
|
<TerrainModel receiveShadow visible />
|
||||||
|
</group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,22 +446,33 @@ function EditorFallbackNode({
|
|||||||
onHoverNode,
|
onHoverNode,
|
||||||
}: EditorNodeCommonProps) {
|
}: EditorNodeCommonProps) {
|
||||||
const meshRef = useRef<THREE.Mesh>(null);
|
const meshRef = useRef<THREE.Mesh>(null);
|
||||||
|
const snappedPosition = useTerrainSnappedPosition(node.position);
|
||||||
|
const normalizedScale = normalizeMapScale(node.scale);
|
||||||
const pointerHandlers = createEditorNodePointerHandlers(
|
const pointerHandlers = createEditorNodePointerHandlers(
|
||||||
index,
|
index,
|
||||||
onSelectNode,
|
onSelectNode,
|
||||||
isSelectionLocked,
|
isSelectionLocked,
|
||||||
onHoverNode,
|
onHoverNode,
|
||||||
);
|
);
|
||||||
useRegisteredEditorNode(meshRef, index, node, objectsMapRef);
|
useRegisteredEditorNode(
|
||||||
|
meshRef,
|
||||||
|
index,
|
||||||
|
{
|
||||||
|
...node,
|
||||||
|
position: snappedPosition,
|
||||||
|
scale: normalizedScale,
|
||||||
|
},
|
||||||
|
objectsMapRef,
|
||||||
|
);
|
||||||
|
|
||||||
const color = getNodeHighlightColor(isSelected, isHovered) ?? "#6f6f6f";
|
const color = getNodeHighlightColor(isSelected, isHovered) ?? "#6f6f6f";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh
|
<mesh
|
||||||
ref={meshRef}
|
ref={meshRef}
|
||||||
position={node.position}
|
position={snappedPosition}
|
||||||
rotation={node.rotation}
|
rotation={node.rotation}
|
||||||
scale={node.scale}
|
scale={normalizedScale}
|
||||||
{...pointerHandlers}
|
{...pointerHandlers}
|
||||||
>
|
>
|
||||||
<boxGeometry args={[1, 1, 1]} />
|
<boxGeometry args={[1, 1, 1]} />
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ interface EditorSceneProps {
|
|||||||
hoveredNodeIndex: number | null;
|
hoveredNodeIndex: number | null;
|
||||||
onHoverNode: (index: number | null) => void;
|
onHoverNode: (index: number | null) => void;
|
||||||
transformMode: TransformMode;
|
transformMode: TransformMode;
|
||||||
|
lockTerrainSelection: boolean;
|
||||||
onTransformModeChange: (mode: TransformMode) => void;
|
onTransformModeChange: (mode: TransformMode) => void;
|
||||||
onTransformStart: () => void;
|
onTransformStart: () => void;
|
||||||
onTransformEnd: () => void;
|
onTransformEnd: () => void;
|
||||||
@@ -40,6 +41,7 @@ export function EditorScene({
|
|||||||
hoveredNodeIndex,
|
hoveredNodeIndex,
|
||||||
onHoverNode,
|
onHoverNode,
|
||||||
transformMode,
|
transformMode,
|
||||||
|
lockTerrainSelection,
|
||||||
onTransformModeChange,
|
onTransformModeChange,
|
||||||
onTransformStart,
|
onTransformStart,
|
||||||
onTransformEnd,
|
onTransformEnd,
|
||||||
@@ -126,6 +128,7 @@ export function EditorScene({
|
|||||||
hoveredNodeIndex={hoveredNodeIndex}
|
hoveredNodeIndex={hoveredNodeIndex}
|
||||||
onHoverNode={onHoverNode}
|
onHoverNode={onHoverNode}
|
||||||
transformMode={transformMode}
|
transformMode={transformMode}
|
||||||
|
lockTerrainSelection={lockTerrainSelection}
|
||||||
onTransformStart={onTransformStart}
|
onTransformStart={onTransformStart}
|
||||||
onTransformEnd={onTransformEnd}
|
onTransformEnd={onTransformEnd}
|
||||||
onNodeTransform={onNodeTransform}
|
onNodeTransform={onNodeTransform}
|
||||||
|
|||||||
+61
-1
@@ -1244,7 +1244,7 @@ canvas {
|
|||||||
|
|
||||||
.editor-transform-button {
|
.editor-transform-button {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 18px 1fr auto;
|
grid-template-columns: 18px minmax(0, 1fr) auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -1264,6 +1264,30 @@ canvas {
|
|||||||
transform 160ms ease;
|
transform 160ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-transform-label {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-transform-label span,
|
||||||
|
.editor-transform-label small {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-transform-label small {
|
||||||
|
color: #8f8f8f;
|
||||||
|
font-size: 0.64rem;
|
||||||
|
font-weight: 620;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-transform-button.active .editor-transform-label small {
|
||||||
|
color: #555555;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-transform-button.active {
|
.editor-transform-button.active {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
color: #050505;
|
color: #050505;
|
||||||
@@ -1378,6 +1402,42 @@ canvas {
|
|||||||
color: #050505;
|
color: #050505;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-checkbox-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 9px;
|
||||||
|
padding: 10px 11px;
|
||||||
|
background: #101010;
|
||||||
|
border: 1px solid #242424;
|
||||||
|
border-radius: 14px;
|
||||||
|
color: #f2f2f2;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-checkbox-row input {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
accent-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-checkbox-row span {
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-checkbox-row strong {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-checkbox-row small {
|
||||||
|
color: #8f8f8f;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
.editor-selected-info {
|
.editor-selected-info {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 17px 1fr auto;
|
grid-template-columns: 17px 1fr auto;
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
useState<TransformMode>("translate");
|
useState<TransformMode>("translate");
|
||||||
const [isPlayerMode, setIsPlayerMode] = useState(false);
|
const [isPlayerMode, setIsPlayerMode] = useState(false);
|
||||||
const [isSelectionLocked, setIsSelectionLocked] = useState(false);
|
const [isSelectionLocked, setIsSelectionLocked] = useState(false);
|
||||||
|
const [lockTerrainSelection, setLockTerrainSelection] = useState(true);
|
||||||
const [sceneLoadingState, setSceneLoadingState] = useState<SceneLoadingState>(
|
const [sceneLoadingState, setSceneLoadingState] = useState<SceneLoadingState>(
|
||||||
{
|
{
|
||||||
...INITIAL_SCENE_LOADING_STATE,
|
...INITIAL_SCENE_LOADING_STATE,
|
||||||
@@ -194,6 +195,22 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
setIsSelectionLocked((locked) => !locked);
|
setIsSelectionLocked((locked) => !locked);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleTerrainSelectionLockChange = useCallback(
|
||||||
|
(locked: boolean) => {
|
||||||
|
setLockTerrainSelection(locked);
|
||||||
|
|
||||||
|
if (!locked) return;
|
||||||
|
|
||||||
|
setSelectedNodeIndex((currentIndex) => {
|
||||||
|
if (currentIndex === null) return null;
|
||||||
|
|
||||||
|
const selectedNode = sceneData?.mapNodes[currentIndex];
|
||||||
|
return selectedNode?.name === "terrain" ? null : currentIndex;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[sceneData],
|
||||||
|
);
|
||||||
|
|
||||||
const handleHoverNode = useCallback((index: number | null) => {
|
const handleHoverNode = useCallback((index: number | null) => {
|
||||||
setHoveredNodeIndex(index);
|
setHoveredNodeIndex(index);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -351,6 +368,7 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
hoveredNodeIndex={hoveredNodeIndex}
|
hoveredNodeIndex={hoveredNodeIndex}
|
||||||
onHoverNode={handleHoverNode}
|
onHoverNode={handleHoverNode}
|
||||||
transformMode={transformMode}
|
transformMode={transformMode}
|
||||||
|
lockTerrainSelection={lockTerrainSelection}
|
||||||
onTransformModeChange={handleTransformModeChange}
|
onTransformModeChange={handleTransformModeChange}
|
||||||
onTransformStart={handleTransformStart}
|
onTransformStart={handleTransformStart}
|
||||||
onTransformEnd={handleTransformEnd}
|
onTransformEnd={handleTransformEnd}
|
||||||
@@ -378,6 +396,8 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
? sceneData.mapNodes[selectedNodeIndex].name || null
|
? sceneData.mapNodes[selectedNodeIndex].name || null
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
lockTerrainSelection={lockTerrainSelection}
|
||||||
|
onLockTerrainSelectionChange={handleTerrainSelectionLockChange}
|
||||||
isSelectionLocked={isSelectionLocked}
|
isSelectionLocked={isSelectionLocked}
|
||||||
onSelectionLockToggle={handleSelectionLockToggle}
|
onSelectionLockToggle={handleSelectionLockToggle}
|
||||||
onClearSelection={handleClearSelection}
|
onClearSelection={handleClearSelection}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import type { MapNode } from "@/types/editor/editor";
|
||||||
|
import { isInstancedMapNodeName } from "@/world/map-instancing/mapInstancingConfig";
|
||||||
|
|
||||||
|
const MAP_STRUCTURE_NODE_NAMES = new Set(["Scene", "blocking", "terrain"]);
|
||||||
|
const RUNTIME_VEGETATION_NODE_NAMES = new Set([
|
||||||
|
"arbre",
|
||||||
|
"buisson",
|
||||||
|
"champdeble",
|
||||||
|
"champdesoja",
|
||||||
|
"champsdetournesol",
|
||||||
|
"sapin",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function isRuntimeStructureMapNode(name: string): boolean {
|
||||||
|
return MAP_STRUCTURE_NODE_NAMES.has(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRuntimeSingleMapNode(node: MapNode): boolean {
|
||||||
|
if (isRuntimeStructureMapNode(node.name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.type === "Mesh") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
!RUNTIME_VEGETATION_NODE_NAMES.has(node.name) &&
|
||||||
|
!isInstancedMapNodeName(node.name)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEditorVisibleMapNode(node: MapNode): boolean {
|
||||||
|
return !isRuntimeStructureMapNode(node.name) && node.type !== "Mesh";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTerrainMapNode(nodes: readonly MapNode[]): MapNode | null {
|
||||||
|
return nodes.find((node) => node.name === "terrain") ?? null;
|
||||||
|
}
|
||||||
@@ -62,3 +62,11 @@ export const INSTANCED_MAP_EXCEPTIONS = new Set([
|
|||||||
"blocking",
|
"blocking",
|
||||||
"terrain",
|
"terrain",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export function getVegetationScaleMultiplier(name: string): number | null {
|
||||||
|
const config = Object.values(VEGETATION_TYPES).find(
|
||||||
|
(vegetationConfig) => vegetationConfig.mapName === name,
|
||||||
|
);
|
||||||
|
|
||||||
|
return config?.scaleMultiplier ?? null;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user