fix :editor
This commit is contained in:
@@ -282,8 +282,35 @@ export function EditorPage(): React.JSX.Element {
|
|||||||
try {
|
try {
|
||||||
const modelUrl = `/models/${modelName}/model.gltf`;
|
const modelUrl = `/models/${modelName}/model.gltf`;
|
||||||
const modelResponse = await fetch(modelUrl);
|
const modelResponse = await fetch(modelUrl);
|
||||||
|
|
||||||
if (modelResponse.ok) {
|
if (modelResponse.ok) {
|
||||||
|
const contentType =
|
||||||
|
modelResponse.headers.get("content-type") || "";
|
||||||
|
|
||||||
|
if (
|
||||||
|
contentType.includes("gltf") ||
|
||||||
|
contentType.includes("json") ||
|
||||||
|
contentType.includes("model")
|
||||||
|
) {
|
||||||
|
const text = await modelResponse.text();
|
||||||
|
if (
|
||||||
|
text.includes('"glTF"') ||
|
||||||
|
text.includes('"scene"') ||
|
||||||
|
text.includes('"nodes"')
|
||||||
|
) {
|
||||||
models.set(modelName, modelUrl);
|
models.set(modelName, modelUrl);
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
`Invalid GLTF content for ${modelName}:`,
|
||||||
|
text.substring(0, 100),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
`Invalid Content-Type for ${modelName}:`,
|
||||||
|
contentType,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* empty */
|
/* empty */
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
import {
|
import { useMemo, useRef, useEffect, useState } from "react";
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
Suspense,
|
|
||||||
Component,
|
|
||||||
type ReactNode,
|
|
||||||
} from "react";
|
|
||||||
import { useGLTF } from "@react-three/drei";
|
import { useGLTF } from "@react-three/drei";
|
||||||
import { Grid, TransformControls } from "@react-three/drei";
|
import { Grid, TransformControls } from "@react-three/drei";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
@@ -25,33 +17,6 @@ interface MapViewerProps {
|
|||||||
onNodeTransform: (nodeIndex: number, transform: MapNode) => void;
|
onNodeTransform: (nodeIndex: number, transform: MapNode) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const clonedScenesCache = new Map<string, THREE.Group>();
|
|
||||||
|
|
||||||
class ErrorBoundary extends Component<
|
|
||||||
{ children: ReactNode; fallback?: ReactNode },
|
|
||||||
{ hasError: boolean }
|
|
||||||
> {
|
|
||||||
constructor(props: { children: ReactNode; fallback?: ReactNode }) {
|
|
||||||
super(props);
|
|
||||||
this.state = { hasError: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
static getDerivedStateFromError(): { hasError: boolean } {
|
|
||||||
return { hasError: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidCatch(error: Error, _errorInfo: React.ErrorInfo): void {
|
|
||||||
console.warn("Model loading error:", error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.state.hasError) {
|
|
||||||
return this.props.fallback || null;
|
|
||||||
}
|
|
||||||
return this.props.children;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MapViewer({
|
export default function MapViewer({
|
||||||
sceneData,
|
sceneData,
|
||||||
selectedNodeIndex,
|
selectedNodeIndex,
|
||||||
@@ -63,17 +28,13 @@ export default function MapViewer({
|
|||||||
onTransformEnd,
|
onTransformEnd,
|
||||||
onNodeTransform,
|
onNodeTransform,
|
||||||
}: MapViewerProps): React.JSX.Element {
|
}: MapViewerProps): React.JSX.Element {
|
||||||
const isTransforming = useRef(false);
|
|
||||||
const objectsMapRef = useRef<Map<number, THREE.Object3D>>(new Map());
|
const objectsMapRef = useRef<Map<number, THREE.Object3D>>(new Map());
|
||||||
|
|
||||||
const handleTransformMouseDown = () => {
|
const handleTransformMouseDown = () => {
|
||||||
isTransforming.current = true;
|
|
||||||
onTransformStart?.();
|
onTransformStart?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTransformMouseUp = () => {
|
const handleTransformMouseUp = () => {
|
||||||
isTransforming.current = false;
|
|
||||||
|
|
||||||
if (selectedNodeIndex !== null) {
|
if (selectedNodeIndex !== null) {
|
||||||
const obj = objectsMapRef.current.get(selectedNodeIndex);
|
const obj = objectsMapRef.current.get(selectedNodeIndex);
|
||||||
if (!obj) return;
|
if (!obj) return;
|
||||||
@@ -88,7 +49,6 @@ export default function MapViewer({
|
|||||||
onNodeTransform?.(selectedNodeIndex, updatedNode);
|
onNodeTransform?.(selectedNodeIndex, updatedNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onTransformEnd?.();
|
onTransformEnd?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -133,9 +93,8 @@ export default function MapViewer({
|
|||||||
|
|
||||||
if (modelUrl) {
|
if (modelUrl) {
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary key={index} fallback={null}>
|
|
||||||
<Suspense fallback={null}>
|
|
||||||
<ModelNodeWithRef
|
<ModelNodeWithRef
|
||||||
|
key={index}
|
||||||
index={index}
|
index={index}
|
||||||
node={node}
|
node={node}
|
||||||
modelUrl={modelUrl}
|
modelUrl={modelUrl}
|
||||||
@@ -145,8 +104,6 @@ export default function MapViewer({
|
|||||||
onSelectNode={onSelectNode}
|
onSelectNode={onSelectNode}
|
||||||
onHoverNode={onHoverNode}
|
onHoverNode={onHoverNode}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
|
||||||
</ErrorBoundary>
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
@@ -199,14 +156,7 @@ function ModelNodeWithRef({
|
|||||||
const groupRef = useRef<THREE.Group>(null);
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
const { scene } = useGLTF(modelUrl);
|
const { scene } = useGLTF(modelUrl);
|
||||||
|
|
||||||
const clonedScene = useMemo(() => {
|
const sceneInstance = useMemo(() => scene.clone(true), [scene]);
|
||||||
if (!clonedScenesCache.has(modelUrl)) {
|
|
||||||
const clone = scene.clone(true);
|
|
||||||
clonedScenesCache.set(modelUrl, clone);
|
|
||||||
return clone;
|
|
||||||
}
|
|
||||||
return clonedScenesCache.get(modelUrl)!;
|
|
||||||
}, [modelUrl, scene]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (groupRef.current) {
|
if (groupRef.current) {
|
||||||
@@ -221,46 +171,45 @@ function ModelNodeWithRef({
|
|||||||
return () => {
|
return () => {
|
||||||
currentMap.delete(currentIndex);
|
currentMap.delete(currentIndex);
|
||||||
};
|
};
|
||||||
}, [index, node, objectsMapRef]);
|
}, [index]);
|
||||||
|
|
||||||
const instance = useMemo(() => {
|
useEffect(() => {
|
||||||
const inst = clonedScene.clone(true);
|
if (groupRef.current) {
|
||||||
|
groupRef.current.position.set(...node.position);
|
||||||
|
groupRef.current.rotation.set(...node.rotation);
|
||||||
|
groupRef.current.scale.set(...node.scale);
|
||||||
|
}
|
||||||
|
}, [node.position, node.rotation, node.scale]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!groupRef.current) return;
|
||||||
|
|
||||||
|
groupRef.current.traverse((child) => {
|
||||||
|
if ((child as THREE.Mesh).isMesh) {
|
||||||
|
const mesh = child as THREE.Mesh;
|
||||||
|
if (
|
||||||
|
mesh.material &&
|
||||||
|
mesh.material instanceof THREE.MeshStandardMaterial
|
||||||
|
) {
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
inst.traverse((child) => {
|
mesh.material = mesh.material.clone();
|
||||||
if ((child as THREE.Mesh).isMesh && (child as THREE.Mesh).material) {
|
(mesh.material as THREE.MeshStandardMaterial).color.set("#ff6600");
|
||||||
const mesh = child as THREE.Mesh;
|
|
||||||
const mat = mesh.material as unknown as THREE.MeshStandardMaterial;
|
|
||||||
mesh.material = mat.clone();
|
|
||||||
(mesh.material as unknown as THREE.MeshStandardMaterial).color.set(
|
|
||||||
"#ff6600",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (isHovered) {
|
} else if (isHovered) {
|
||||||
inst.traverse((child) => {
|
mesh.material = mesh.material.clone();
|
||||||
if ((child as THREE.Mesh).isMesh && (child as THREE.Mesh).material) {
|
(mesh.material as THREE.MeshStandardMaterial).color.set("#ff9900");
|
||||||
const mesh = child as THREE.Mesh;
|
}
|
||||||
const mat = mesh.material as unknown as THREE.MeshStandardMaterial;
|
}
|
||||||
mesh.material = mat.clone();
|
|
||||||
(mesh.material as unknown as THREE.MeshStandardMaterial).color.set(
|
|
||||||
"#ff9900",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}, [isSelected, isHovered]);
|
||||||
|
|
||||||
inst.position.set(...node.position);
|
|
||||||
inst.rotation.set(...node.rotation);
|
|
||||||
inst.scale.set(...node.scale);
|
|
||||||
|
|
||||||
return inst;
|
|
||||||
}, [clonedScene, node, isSelected, isHovered]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<primitive
|
<primitive
|
||||||
ref={groupRef}
|
ref={groupRef}
|
||||||
object={instance}
|
object={sceneInstance}
|
||||||
|
position={node.position}
|
||||||
|
rotation={node.rotation}
|
||||||
|
scale={node.scale}
|
||||||
onClick={(e: unknown) => {
|
onClick={(e: unknown) => {
|
||||||
(e as { stopPropagation?: () => void }).stopPropagation?.();
|
(e as { stopPropagation?: () => void }).stopPropagation?.();
|
||||||
onSelectNode(index);
|
onSelectNode(index);
|
||||||
@@ -309,7 +258,15 @@ function FallbackNodeWithRef({
|
|||||||
return () => {
|
return () => {
|
||||||
currentMap.delete(currentIndex);
|
currentMap.delete(currentIndex);
|
||||||
};
|
};
|
||||||
}, [index, node, objectsMapRef]);
|
}, [index]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (meshRef.current) {
|
||||||
|
meshRef.current.position.set(...node.position);
|
||||||
|
meshRef.current.rotation.set(...node.rotation);
|
||||||
|
meshRef.current.scale.set(...node.scale);
|
||||||
|
}
|
||||||
|
}, [node.position, node.rotation, node.scale]);
|
||||||
|
|
||||||
const color = isSelected ? "#ff6600" : isHovered ? "#ff9900" : "#cccccc";
|
const color = isSelected ? "#ff6600" : isHovered ? "#ff9900" : "#cccccc";
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
import {
|
import { useEffect, useState, useMemo, useRef } from "react";
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
Suspense,
|
|
||||||
Component,
|
|
||||||
type ReactNode,
|
|
||||||
} from "react";
|
|
||||||
import { useGLTF } from "@react-three/drei";
|
import { useGLTF } from "@react-three/drei";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { useOctreeGraphNode } from "@/hooks/useOctreeGraphNode";
|
import { useOctreeGraphNode } from "@/hooks/useOctreeGraphNode";
|
||||||
@@ -22,33 +14,6 @@ interface MapNode {
|
|||||||
|
|
||||||
const MAP_JSON_PATH = "/map.json";
|
const MAP_JSON_PATH = "/map.json";
|
||||||
|
|
||||||
const clonedScenesCache = new Map<string, THREE.Group>();
|
|
||||||
|
|
||||||
class GameMapErrorBoundary extends Component<
|
|
||||||
{ children: ReactNode; fallback?: ReactNode },
|
|
||||||
{ hasError: boolean }
|
|
||||||
> {
|
|
||||||
constructor(props: { children: ReactNode; fallback?: ReactNode }) {
|
|
||||||
super(props);
|
|
||||||
this.state = { hasError: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
static getDerivedStateFromError(): { hasError: boolean } {
|
|
||||||
return { hasError: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidCatch(error: Error, _errorInfo: React.ErrorInfo): void {
|
|
||||||
console.warn("GameMap model loading error:", error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.state.hasError) {
|
|
||||||
return this.props.fallback || null;
|
|
||||||
}
|
|
||||||
return this.props.children;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GameMapProps {
|
interface GameMapProps {
|
||||||
onOctreeReady: OctreeReadyHandler;
|
onOctreeReady: OctreeReadyHandler;
|
||||||
}
|
}
|
||||||
@@ -113,11 +78,7 @@ export function GameMap({ onOctreeReady }: GameMapProps): React.JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<group ref={groupRef}>
|
<group ref={groupRef}>
|
||||||
{nodesToRender.map((node, index) => (
|
{nodesToRender.map((node, index) => (
|
||||||
<GameMapErrorBoundary key={index} fallback={null}>
|
<ModelInstance key={index} node={node} />
|
||||||
<Suspense fallback={null}>
|
|
||||||
<ModelInstance node={node} />
|
|
||||||
</Suspense>
|
|
||||||
</GameMapErrorBoundary>
|
|
||||||
))}
|
))}
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
@@ -125,26 +86,34 @@ export function GameMap({ onOctreeReady }: GameMapProps): React.JSX.Element {
|
|||||||
|
|
||||||
function ModelInstance({ node }: { node: MapNode }): React.JSX.Element {
|
function ModelInstance({ node }: { node: MapNode }): React.JSX.Element {
|
||||||
const modelPath = `/models/${node.name}/model.gltf`;
|
const modelPath = `/models/${node.name}/model.gltf`;
|
||||||
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
const { scene } = useGLTF(modelPath);
|
const { scene } = useGLTF(modelPath);
|
||||||
|
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
useEffect(() => {
|
||||||
|
if (groupRef.current) {
|
||||||
const clonedScene = useMemo(() => {
|
groupRef.current.position.set(...node.position);
|
||||||
if (!clonedScenesCache.has(modelPath)) {
|
groupRef.current.rotation.set(...node.rotation);
|
||||||
const clone = scene.clone(true);
|
groupRef.current.scale.set(...node.scale);
|
||||||
clonedScenesCache.set(modelPath, clone);
|
|
||||||
return clone;
|
|
||||||
}
|
}
|
||||||
return clonedScenesCache.get(modelPath)!;
|
}, [
|
||||||
}, [modelPath, scene]);
|
node.position[0],
|
||||||
|
node.position[1],
|
||||||
|
node.position[2],
|
||||||
|
node.rotation[0],
|
||||||
|
node.rotation[1],
|
||||||
|
node.rotation[2],
|
||||||
|
node.scale[0],
|
||||||
|
node.scale[1],
|
||||||
|
node.scale[2],
|
||||||
|
]);
|
||||||
|
|
||||||
const instance = useMemo(() => {
|
return (
|
||||||
const inst = clonedScene.clone(true);
|
<primitive
|
||||||
inst.position.set(...node.position);
|
ref={groupRef}
|
||||||
inst.rotation.set(...node.rotation);
|
object={scene}
|
||||||
inst.scale.set(...node.scale);
|
position={node.position}
|
||||||
return inst;
|
rotation={node.rotation}
|
||||||
}, [clonedScene, node.position, node.rotation, node.scale]);
|
scale={node.scale}
|
||||||
|
/>
|
||||||
return <primitive ref={groupRef} object={instance} />;
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user