diff --git a/package.json b/package.json index c71412b..8d53429 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,6 @@ "vite": "^8.0.4" }, "engines": { - "node": ">=22.12.0" + "node": ">=20.19.0 || >=22.12.0" } } diff --git a/src/components/editor/EditorPage.tsx b/src/components/editor/EditorPage.tsx index a3a46ff..7ef8e4b 100644 --- a/src/components/editor/EditorPage.tsx +++ b/src/components/editor/EditorPage.tsx @@ -252,8 +252,6 @@ export function EditorPage(): React.JSX.Element { newMapNodes[nodeIndex] = updatedNode; return { ...prev, mapNodes: newMapNodes }; }); - setUndoCount((prev) => prev + 1); - console.log("Node transformed:", nodeIndex); }, [sceneData], ); diff --git a/src/components/editor/MapViewer.tsx b/src/components/editor/MapViewer.tsx index 08a9495..d733555 100644 --- a/src/components/editor/MapViewer.tsx +++ b/src/components/editor/MapViewer.tsx @@ -40,7 +40,6 @@ export default function MapViewer({ const handleTransformMouseUp = () => { isTransforming.current = false; - onTransformEnd?.(); if (selectedNodeIndex !== null) { const obj = objectsMapRef.current.get(selectedNodeIndex); @@ -53,9 +52,12 @@ export default function MapViewer({ rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z], scale: [obj.scale.x, obj.scale.y, obj.scale.z], }; + // Call onNodeTransform BEFORE onTransformEnd so history captures final state onNodeTransform?.(selectedNodeIndex, updatedNode); } } + + onTransformEnd?.(); }; const [selectedObject, setSelectedObject] = useState( @@ -91,11 +93,7 @@ export default function MapViewer({ { (e as { stopPropagation?: () => void }).stopPropagation?.(); - if ( - !(window as unknown as { isTransforming?: boolean }).isTransforming - ) { - onSelectNode(null); - } + onSelectNode(null); }} > {sceneData.mapNodes.map((node, index) => { @@ -230,11 +228,7 @@ function ModelNodeWithRef({ object={instance} onClick={(e: unknown) => { (e as { stopPropagation?: () => void }).stopPropagation?.(); - if ( - !(window as unknown as { isTransforming?: boolean }).isTransforming - ) { - onSelectNode(index); - } + onSelectNode(index); }} onPointerEnter={(e: unknown) => { (e as { stopPropagation?: () => void }).stopPropagation?.(); @@ -292,11 +286,7 @@ function FallbackNodeWithRef({ scale={node.scale} onClick={(e: unknown) => { (e as { stopPropagation?: () => void }).stopPropagation?.(); - if ( - !(window as unknown as { isTransforming?: boolean }).isTransforming - ) { - onSelectNode(index); - } + onSelectNode(index); }} onPointerEnter={(e: unknown) => { (e as { stopPropagation?: () => void }).stopPropagation?.(); diff --git a/vite.config.ts b/vite.config.ts index 038d0d6..dbdf65d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,9 +2,14 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import path from "node:path"; import fs from "node:fs"; +import { fileURLToPath } from "node:url"; import type { ViteDevServer } from "vite"; import type { IncomingMessage, ServerResponse } from "http"; +const __dirname = fileURLToPath(new URL(".", import.meta.url)); + +const MAX_MAP_PAYLOAD_BYTES = 1024 * 1024; // 1MB limit + const saveMapPlugin = () => ({ name: "save-map-api", configureServer(server: ViteDevServer) { @@ -12,20 +17,52 @@ const saveMapPlugin = () => ({ "/api/save-map", async (req: IncomingMessage, res: ServerResponse) => { if (req.method !== "POST") { - res.writeHead(405).end(); + res.writeHead(405, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Method not allowed" })); return; } let body = ""; - req.on("data", (chunk: Buffer) => (body += chunk.toString())); - req.on("end", () => { + let bodySize = 0; + let requestAborted = false; + + req.on("data", (chunk: Buffer) => { + if (requestAborted) return; + bodySize += chunk.length; + if (bodySize > MAX_MAP_PAYLOAD_BYTES) { + requestAborted = true; + res.writeHead(413, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Payload too large" })); + req.destroy(); + return; + } + body += chunk.toString(); + }); + + req.on("error", (err: Error) => { + if (!res.headersSent) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: err.message })); + } + }); + + req.on("end", async () => { + if (requestAborted) return; + try { + const parsedBody = JSON.parse(body); const mapPath = path.resolve(__dirname, "public/map.json"); - fs.writeFileSync(mapPath, body); + await fs.promises.writeFile( + mapPath, + JSON.stringify(parsedBody, null, 2), + "utf8", + ); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ success: true })); } catch (err) { - res.writeHead(500).end( + const statusCode = err instanceof SyntaxError ? 400 : 500; + res.writeHead(statusCode, { "Content-Type": "application/json" }); + res.end( JSON.stringify({ error: err instanceof Error ? err.message : "Unknown error", }), @@ -36,7 +73,6 @@ const saveMapPlugin = () => ({ ); }, }); -import { fileURLToPath } from "node:url"; export default defineConfig({ plugins: [react(), saveMapPlugin()],