最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to display content of dwg file in my NextJS app? - Stack Overflow

programmeradmin3浏览0评论

I am desperate. I am building NextJS app with AdonisJS backend. This app will work like warehouse planner, user should be able to put some elements like floor, wall etc. on grid and move it around, resize, put together with another elements etc. It was working well until I came to request from my user to add import of dwg files. I should be able to upload dwg file and then use it like another elements on the grid, so I should be able to move it, resize it etc. I was able to do some work, I can upload that dwg file to my backend, which respond with svg, but I am unable to display content of that svg on my grid, just something like this

I am sharing my frontend code and also my backend code, I hope it is even possible to do this.

Btw, dwg files are aroung 5MB.

import_controller.ts (backend)

const execFileAsync = promisify(execFile)

export default class ImportController {
  public async importDwg({ request, response }: HttpContext) {
    try {
      const file = request.file('file')
      if (!file) return response.badRequest({ message: 'No file uploaded' })

      const tmpDir = path.join(process.cwd(), 'tmp')
      await fs.mkdir(tmpDir, { recursive: true })

      const timestamp = Date.now()
      const dwgPath = path.join(tmpDir, `${timestamp}_${file.clientName}`)

      await file.move(tmpDir, { name: path.basename(dwgPath) })

      // Convert DWG to SVG
      const dwg2svgPath = '/usr/local/bin/dwg2svg'
      const { stdout, stderr } = await execFileAsync(dwg2svgPath, [dwgPath], {
        maxBuffer: 1024 * 1024 * 64, // 64 MB
      })

      console.log(`dwg2svg stderr: ${stderr}`)

      let svgString = stdout

      // Fix malformed width/height line
      svgString = svgString.replace(/width="([^"]+)\s+height="([^"]+)"/, 'width="$1" height="$2"');

      // Ensure viewBox exists
      const viewBoxMatch = svgString.match(/viewBox="([^"]+)"/)
      if (!viewBoxMatch) {
        const width = svgString.match(/width="([^"]+)"/)?.[1] ?? '2000'
        const height = svgString.match(/height="([^"]+)"/)?.[1] ?? '1000'
        // svgString = svgString.replace(/<svg([^>]*)>/, `<svg$1 viewBox="0 0 ${width} ${height}">`)
      }

      // Constrain to iframe view
      svgString = svgString
        .replace(/width="[^"]+"/, 'width="100%"')
        .replace(/height="[^"]+"/, 'height="100%"')

      await fs.writeFile(path.join(tmpDir, 'debug_output.svg'), svgString)

      const base64Svg = `data:image/svg+xml;base64,${Buffer.from(svgString).toString('base64')}`

      return response.ok({
        message: 'DWG converted successfully',
        image: base64Svg,
        elements: [
          {
            id: Date.now(),
            type: 'dwg',
            x: 0,
            y: 0,
            width: 800,
            height: 600,
            content: base64Svg,
            originalFileName: file.clientName,
          }
        ]
      })

    } catch (error: any) {
      console.error('Error importing DWG:', error)
      return response.internalServerError({
        message: 'Failed to import DWG',
        error: error.message,
      })
    }
  }
}

Here is my warehouse-grid.tsx file

"use client";

export const WarehouseGrid = forwardRef<
  { fitAllElementsToView: () => void },
  WarehouseGridProps
>(function WarehouseGrid(
  {
    selectedTool,
    setSelectedTool,
    lang = "sk",
    is3DMode,
    elements,
    onElementsChange,
  }: WarehouseGridProps,
  ref
) {
  return (
    <div className="flex flex-col h-full">
      {/* Controls section - always visible in both modes */}
      <div className="flex items-center gap-4 p-2 bg-white border-b">
        <div className="flex items-center gap-2">
          <Button
            variant={selectedTool === "move" ? "default" : "ghost"}
            onClick={() => setSelectedTool("move")}
            size="icon"
            title={t(lang, "moveTool")}
          >
            <Move className="h-4 w-4" />
          </Button>
          <Button
            variant={selectedTool === "resize" ? "default" : "ghost"}
            onClick={() => setSelectedTool("resize")}
            size="icon"
            title={t(lang, "resizeTool")}
          >
            <Maximize className="h-4 w-4" />
          </Button>
          <Button
            variant={selectedTool === "pan" ? "default" : "ghost"}
            onClick={() => setSelectedTool("pan")}
            size="icon"
            title={t(lang, "panTool")}
          >
            <Hand className="h-4 w-4" />
          </Button>
          <Separator orientation="vertical" className="h-4" />
          <Button
            variant="outline"
            size="icon"
            title={t(lang, "zoomOut")}
            onClick={handleZoomOut}
          >
            <Minus className="h-4 w-4" />
          </Button>
          <Button
            variant="outline"
            size="icon"
            title={t(lang, "zoomIn")}
            onClick={handleZoomIn}
          >
            <Plus className="h-4 w-4" />
          </Button>
          <Button
            variant="outline"
            size="icon"
            title={t(lang, "resetZoom")}
            onClick={handleZoomReset}
          >
            <Maximize2 className="h-4 w-4" />
          </Button>
          <Button
            variant="ghost"
            size="icon"
            title={t(lang, "fitToScreen")}
            onClick={fitAllElementsToView}
          >
            <Maximize2 className="h-4 w-4" />
          </Button>
        </div>
      </div>
  
      {/* Main content area */}
      <div className="flex-1 relative">
        {is3DMode ? (
          // 3D Mode
          <div ref={containerRef} className="w-full h-full" />
        ) : (
          // 2D Mode
          <div className="flex h-full">
            {/* Grid container */}
            <div
              ref={gridRef}
              className={cn(
                "relative flex-1 bg-gray-50 overflow-hidden",
                {
                  "cursor-grab": selectedTool === "pan",
                  "cursor-grabbing": isDraggingViewport,
                  "cursor-default": selectedTool === "move" || selectedTool === "resize",
                }
              )}
              onClick={handleGridClick}
              onMouseDown={handleMouseDown}
              onMouseMove={handleMouseMove}
              onMouseUp={handleMouseUp}
              onMouseLeave={handleMouseUp}
              onDragOver={handleDragOver}
              onDrop={handleDrop}
            >
              <div
                style={{
                  transform: translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.scale}),
                  transformOrigin: "0 0",
                  width: "100%",
                  height: "100%",
                  position: "absolute",
                }}
              >
                {renderGrid()}
                {elements.map((element) => {
                  return (
                    <DraggableWithoutFindDOMNode
                      key={element.id}
                      position={{ x: element.x, y: element.y }}
                      onStop={(e, data) => handleDragStop(element.id, e, data)}
                      bounds="parent"
                      disabled={selectedTool !== "move"}
                      handle=".drag-handle"
                    >
                      <ResizableBox
                        width={element.width}
                        height={element.height}
                        onResizeStop={(e, data) => handleResizeStop(element.id, e, data)}
                        draggableOpts={{ disabled: selectedTool !== "resize" }}
                        handle={
                          <div className="absolute right-0 bottom-0 w-4 h-4 bg-blue-500 cursor-se-resize rounded-bl" />
                        }
                      >
                        <div
                          className={cn(
                            "relative border-2 drag-handle",
                            selectedElementIds.includes(element.id)
                              ? "border-blue-500"
                              : "border-gray-200"
                          )}
                          onClick={(e) => handleElementClick(e, element.id)}
                        >
                          {renderElement(element)}
                        </div>
                      </ResizableBox>
                    </DraggableWithoutFindDOMNode>
                  );
                })}
              </div>
            </div>
  
            {/* Properties panel */}
            {selectedElementIds.length === 1 && (
              <div className="w-80 border-l border-gray-200">
                <ElementProperties
                  selectedElement={elements.find((el) => el.id === selectedElementIds[0]) || null}
                  onUpdate={handleUpdateElement}
                  onDelete={handleDeleteElement}
                  lang={lang}
                />
              </div>
            )}
          </div>
        )}
      </div>
  
      {/* Language selector */}
      <div className="fixed bottom-0 right-0 p-4">
        <LanguageSelector lang={lang} />
      </div>
    </div>
  );
});

WarehouseGrid.displayName = "WarehouseGrid";

export default WarehouseGrid;

And to be complete, here is my page.tsx, from where I call warehouse-grid.


"use client";

export default function DashboardPage() {
  const router = useRouter();
  const { isLoading, isAuthenticated } = useAuth();

  const [selectedTool, setSelectedTool] = useState("select");
  const [activeCategory, setActiveCategory] = useState<ElementCategory>(null);
  const [isSearching, setIsSearching] = useState(false);
  const [searchQuery, setSearchQuery] = useState("");
  const [language, setLanguage] = useState("sk"); // Default to "en"
  const [showWarehouseDialog, setShowWarehouseDialog] = useState(true);
  // eslint-disable-next-line
  const [gridElements, setGridElements] = useState<any[]>([]);
  const [is3DMode, setIs3DMode] = useState(false);
  const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
  const [showSaveDialog, setShowSaveDialog] = useState(false);
  const [showSaveWarehouseDialog, setShowSaveWarehouseDialog] = useState(false);
  const [showLoadWarehouseDialog, setShowLoadWarehouseDialog] = useState(false);
  const [showUnsavedChangesDialog, setShowUnsavedChangesDialog] = useState(false);
  const [pendingAction, setPendingAction] = useState<'new' | 'load' | null>(null);

  // new
  const [feUrn, setFeUrn] = useState<string | null>(null);

  // Add a ref to access the fitAllElementsToView function
  const warehouseGridRef = useRef<{ fitAllElementsToView: () => void } | null>(null);

  const handleNewWarehouse = useCallback(() => {
    const createNew = () => {
      setGridElements([]);
      setHasUnsavedChanges(false);
      setShowWarehouseDialog(false);
    };
  
    if (hasUnsavedChanges) {
      setShowUnsavedChangesDialog(true);
      setPendingAction('new');
    } else {
      createNew();
    }
  }, [hasUnsavedChanges]);
  
  // Update the handleUnsavedChangesAction callback
  const handleUnsavedChangesAction = useCallback((action: 'save' | 'discard' | 'cancel') => {
    setShowUnsavedChangesDialog(false);
    
    if (action === 'save') {
      setShowSaveWarehouseDialog(true);
    } else if (action === 'discard') {
      if (pendingAction === 'new') {
        setGridElements([]);
        setHasUnsavedChanges(false);
        setShowWarehouseDialog(false);
      } else if (pendingAction === 'load') {
        setShowLoadWarehouseDialog(true);
      }
      setPendingAction(null);
    }
    // Always clear the pending action if cancelled
    if (action === 'cancel') {
      setPendingAction(null);
    }
  }, [pendingAction]);

  // Handle language cookie
  useEffect(() => {
    const langCookie = getCookie("lang");
    setLanguage(langCookie?.toString() || "en");
  }, []);

  useEffect(() => {
    const handleBeforeUnload = (e: BeforeUnloadEvent) => {
      if (hasUnsavedChanges) {
        const message = t(language, "unsavedChangesWarning");
        e.preventDefault();
        e.returnValue = message;
        return message;
      }
    };
  
    window.addEventListener('beforeunload', handleBeforeUnload);
  
    return () => {
      window.removeEventListener('beforeunload', handleBeforeUnload);
    };
  }, [hasUnsavedChanges, language]);

  useEffect(() => {
    return () => {
      // Prompt for unsaved changes when component unmounts
      if (hasUnsavedChanges) {
        const shouldSave = window.confirm(t(language, "unsavedChangesWarning"));
        if (shouldSave) {
          // You might want to handle auto-saving here
          console.log('Auto-saving on unmount...');
        }
      }
    };
  }, [hasUnsavedChanges, language]);

  if (isLoading) {
    return (
      <div className="flex h-screen items-center justify-center">
        <LoadingSpinner />
      </div>
    );
  }

  if (!isAuthenticated) {
    router.replace("/login");
    return null;
  }

  const handleLoadWarehouseClick = () => {
    setShowLoadWarehouseDialog(true);
  };

  const handleSaveWarehouse = () => {
    setShowSaveWarehouseDialog(true)
  }

  const handleLoadWarehouse = () => {
    if (hasUnsavedChanges) {
      setShowUnsavedChangesDialog(true)
      setPendingAction('load')
    } else {
      setShowLoadWarehouseDialog(true)
    }
  }

  // eslint-disable-next-line
  const handleLoadWarehouseData = (data: any) => {
    console.log("handleLoadWarehouseData called with data:", data);
    if (!data || !Array.isArray(data)) {
      console.error("Invalid warehouse data received:", data);
      toast.error(t(language, 'error_loading_warehouse'));
      return;
    }
    setGridElements(data)
    setHasUnsavedChanges(false)
    
    // Center the view on the loaded elements after a short delay
    setTimeout(() => {
      warehouseGridRef.current?.fitAllElementsToView();
    }, 100);
  }

  const handleFileImport = async (file: File) => {
    try {
      // Create FormData properly
      const formData = new FormData();
      formData.append('file', file);
      
      toast.loading(t(language, 'importing_file'));

      // Use native fetch
      const response = await fetch(${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3333'}/api/warehouses/import/dwg, {
        method: 'POST',
        body: formData,
        credentials: 'include',
      });
      
      if (!response.ok) {
        throw new Error(Upload failed: ${response.status} ${response.statusText});
      }
      
      const data = await response.json();
      toast.dismiss();

      if (data && data.elements && Array.isArray(data.elements)) {
        console.log('Received elements:', data.elements);
        
        // Add the imported elements to the grid
        setGridElements(prev => [...prev, ...data.elements]);
        
        toast.success(t(language, 'file_imported_successfully'));
      } else {
        throw new Error('Invalid response from server');
      }
    } catch (error: any) {
      console.error('Error importing file:', error);
      toast.dismiss();
      toast.error(error.message || t(language, 'error_importing_file'));
    }
  };

  const findElementCategory = (searchTerm: string): ElementCategory => {
    const searchTermLower = searchTerm.toLowerCase();

    const elementCategories = {
      wall: "building",
      door: "building",
      floor: "building",
      rack: "storage",
      shelf: "storage",
      forklift: "vehicles",
    } as const;

    const foundElement = Object.entries(elementCategories).find(([element]) =>
      element.includes(searchTermLower)
    );

    return foundElement ? (foundElement[1] as ElementCategory) : null;
  };

  const handleSearch = (query: string) => {
    setSearchQuery(query);
    const category = findElementCategory(query);
    if (category) {
      setActiveCategory(category);
    }
  };

  const handleExitWarehouse = () => {
    router.push("/");
  };

  const handleLogout = async () => {
    try {
      // Find the session cookie (it will be a long random string)
      const sessionId = Object.keys(
        document.cookie.split("; ").reduce((acc, cookie) => {
          const [key, value] = cookie.split("=");
          acc[key] = value;
          return acc;
        }, {} as Record<string, string>)
      ).find((key) => key.length > 20);

      await api.post("/auth/logout", {}, { withCredentials: true });

      // Clear any client-side storage
      localStorage.clear();

      // Clear the session cookie if found
      if (sessionId) {
        deleteCookie(sessionId, {
          path: "/",
          domain:
            process.env.NODE_ENV === "production"
              ? ".intralogisticgrid"
              : undefined,
          maxAge: 0,
          expires: new Date(0),
        });
      }

      // Force a router refresh and redirect
      router.refresh();
      router.replace("/");
    } catch (error) {
      console.error("Logout failed", error);
      toast.error(t(language, "logout_error"));
    }
  };

  return (
    <SidebarProvider>
      <WarehouseDialog 
        open={showWarehouseDialog} 
        onOpenChange={setShowWarehouseDialog}
        onFileImport={handleFileImport}
        onNewWarehouse={handleNewWarehouse}
        onLoadWarehouse={(data) => {
          console.log("LoadWarehouseDialog onLoad called with data:", data);
          if (data && Array.isArray(data)) {
            console.log("Valid data, calling onLoadWarehouse");
            handleLoadWarehouseData(data);
            setShowLoadWarehouseDialog(false);
            setShowWarehouseDialog(false);
          } else {
            console.error("Invalid data in WarehouseDialog onLoad:", data);
            toast.error("Invalid warehouse data format");
          }
        }}
      />

      <AppSidebar>
        <WarehouseSidebar
          selectedTool={selectedTool}
          setSelectedTool={setSelectedTool}
          activeCategory={activeCategory}
          lang={language}
        />
      </AppSidebar>
      <SidebarInset className="flex flex-col h-screen">
        <header className="fixed top-0 left-0 right-0 z-50 flex h-14 shrink-0 items-center justify-between bg-white border-b px-3">
          <div className="flex items-center gap-2">
            <ToolbarIcons onCategoryChange={setActiveCategory} />
          </div>
          <div className="flex items-center gap-4">
            {/* View mode switch */}
            <div className="flex items-center gap-2">
              <Square className={h-4 w-4 ${!is3DMode ? 'text-primary' : 'text-muted-foreground'}} />
              <Switch
                checked={is3DMode}
                onCheckedChange={setIs3DMode}
                aria-label="Toggle 3D mode"
              />
              <Cuboid className={h-4 w-4 ${is3DMode ? 'text-primary' : 'text-muted-foreground'}} />
            </div>

            {/* File operations */}
            <Button
              variant="ghost"
              size="icon"
              onClick={handleNewWarehouse}
              title={t(language, "newWarehouse")}
            >
              <FilePlus className="h-4 w-4" />
            </Button>
            <Button
              variant="ghost"
              size="icon"
              onClick={handleLoadWarehouseClick}
              title={t(language, "loadWarehouse")}
            >
              <FolderOpen className="h-4 w-4" />
            </Button>
            <Button
              variant="ghost"
              size="icon"
              onClick={() => setShowSaveWarehouseDialog(true)}
              title={t(language, "saveWarehouse")}
              className={hasUnsavedChanges ? 'text-yellow-500' : ''}
            >
              <Save className="h-4 w-4" />
            </Button>

            {isSearching ? (
              <div className="relative">
                <Input
                  placeholder={t(language, "search")}
                  value={searchQuery}
                  onChange={(e) => handleSearch(e.target.value)}
                  className="w-40 h-8"
                  autoFocus
                />
                <Button
                  variant="ghost"
                  size="icon"
                  className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6"
                  onClick={() => {
                    setSearchQuery("");
                    setIsSearching(false);
                  }}
                >
                  ×
                </Button>
              </div>
            ) : (
              <Button
                variant="ghost"
                size="icon"
                title={t(language, "searchElements")}
                onClick={() => setIsSearching(true)}
              >
                <Search className="h-4 w-4" />
              </Button>
            )}
            <DropdownMenu>
              <DropdownMenuTrigger asChild>
                <Button
                  variant="ghost"
                  size="icon"
                  title={t(language, "settings")}
                >
                  <Settings className="h-4 w-4" />
                </Button>
              </DropdownMenuTrigger>
              <DropdownMenuContent align="end">
                <DropdownMenuItem onClick={handleExitWarehouse}>
                  {t(language, "exitWarehouse")}
                </DropdownMenuItem>
                <DropdownMenuItem onClick={handleLogout}>
                  {t(language, "logout")}
                </DropdownMenuItem>
              </DropdownMenuContent>
            </DropdownMenu>
          </div>
        </header>

        <div className="flex-1 overflow-hidden mt-14">
          <WarehouseGrid
            ref={warehouseGridRef}
            selectedTool={selectedTool}
            setSelectedTool={setSelectedTool}
            lang={language}
            is3DMode={is3DMode}
            elements={gridElements}
            onElementsChange={(newElements) => {
              setGridElements(newElements);
              setHasUnsavedChanges(true);
            }}
          />
        </div>
      </SidebarInset>

      {/* Unsaved changes dialog */}
      <AlertDialog open={showSaveDialog} onOpenChange={setShowSaveDialog}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>
              {t(language, "unsavedChangesTitle")}
            </AlertDialogTitle>
            <AlertDialogDescription>
              {t(language, "unsavedChangesDescription")}
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
          <AlertDialogCancel onClick={() => setShowSaveDialog(false)}>
            {t(language, "cancel")}
          </AlertDialogCancel>
          <AlertDialogAction
        onClick={async () => {
          handleSaveWarehouse();
          setGridElements([]);
          setShowSaveDialog(false);
          setHasUnsavedChanges(false);
        }}
      >
        {t(language, "save")}
      </AlertDialogAction>
      <AlertDialogAction
        onClick={() => {
          setGridElements([]);
          setHasUnsavedChanges(false);
          setShowSaveDialog(false);
        }}
      >
        {t(language, "dontSave")}
      </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>

      {/* Save warehouse dialog */}
      <SaveWarehouseDialog
        open={showSaveWarehouseDialog}
        onOpenChange={setShowSaveWarehouseDialog}
        gridData={{
          elements: gridElements || [], // Ensure we never pass null/undefined
          scale: 1,
          width: GRID_SIZE_METERS,
          height: GRID_SIZE_METERS
        }}
        onSaved={() => setHasUnsavedChanges(false)}
      />

      {/* Load warehouse dialog */}
      <LoadWarehouseDialog
        open={showLoadWarehouseDialog}
        onOpenChange={setShowLoadWarehouseDialog}
        onLoad={(data) => {
          console.log("LoadWarehouseDialog onLoad called with data:", data);
          if (!data || !Array.isArray(data)) {
            toast.error(t(language, 'error_loading_warehouse'))
            return
          }
          setGridElements(data)
          setHasUnsavedChanges(false)
          
          // Center the view on the loaded elements after a short delay
          setTimeout(() => {
            warehouseGridRef.current?.fitAllElementsToView();
          }, 100);
        }}
      />

      {/* Unsaved changes dialog */}
      <UnsavedChangesDialog
        open={showUnsavedChangesDialog}
        onAction={handleUnsavedChangesAction}
      />

      {feUrn && (
        <div style={{ margin: 20, height: 500 }}>
          <FeViewer urn={feUrn} />
        </div>
      )}
      

    </SidebarProvider>
  );
}

I removed all imports. Thanks for any help

发布评论

评论列表(0)

  1. 暂无评论