fix :editor

This commit is contained in:
math-pixel
2026-04-27 17:25:56 +02:00
parent 1a91fcaca0
commit bfe8c49323
3 changed files with 109 additions and 156 deletions
+27
View File
@@ -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 */
+41 -84
View File
@@ -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";
+28 -59
View File
@@ -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} />; );
} }