Files
zeekay b836586a64 fix: Include all component types in registry build
- Updated build-registry.mts to include all component types (ui, ai, 3d, animation, code, etc.)
- Fixed missing components issue by processing all components that start with 'components:'
- Now properly handles components that only exist in one theme variant
- Registry now contains 363 components (up from 117)
- AI, 3D, animation, and code components are now accessible via the registry
2025-10-16 22:33:38 -07:00

15 lines
19 KiB
JSON

{
"name": "3d-model-viewer",
"description": "3D model viewer with controls",
"dependencies": [
"three",
"@react-three/fiber"
],
"files": [
{
"name": "3d-model-viewer.tsx",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n Camera,\n Download,\n Eye,\n Move3d,\n Palette,\n RotateCcw,\n Sun,\n ZoomIn,\n ZoomOut,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/registry/default/ui/button\"\n\ninterface ModelViewerProps extends React.HTMLAttributes<HTMLDivElement> {\n modelUrl?: string\n width?: number\n height?: number\n autoRotate?: boolean\n autoRotateSpeed?: number\n enableZoom?: boolean\n enablePan?: boolean\n enableRotate?: boolean\n backgroundColor?: string\n showControls?: boolean\n showStats?: boolean\n annotations?: Annotation[]\n onModelLoad?: (model: any) => void\n onModelError?: (error: string) => void\n onAnnotationClick?: (annotation: Annotation) => void\n className?: string\n}\n\ninterface Annotation {\n id: string\n position: { x: number; y: number; z: number }\n title: string\n description?: string\n visible?: boolean\n}\n\ninterface ModelStats {\n vertices: number\n faces: number\n materials: number\n textures: number\n size: string\n}\n\n// Mock 3D model renderer since we don't have Three.js available\nclass ModelRenderer {\n private canvas: HTMLCanvasElement\n private ctx: CanvasRenderingContext2D\n private model: any = null\n private camera = {\n rotation: { x: 0, y: 0 },\n zoom: 1,\n position: { x: 0, y: 0 },\n }\n private autoRotate = false\n private autoRotateSpeed = 1\n private animationFrame: number | null = null\n private isDragging = false\n private lastMousePosition = { x: 0, y: 0 }\n private annotations: Annotation[] = []\n\n constructor(canvas: HTMLCanvasElement) {\n this.canvas = canvas\n const ctx = canvas.getContext(\"2d\")\n if (!ctx) throw new Error(\"Could not get 2D context\")\n this.ctx = ctx\n\n this.setupEventListeners()\n }\n\n private setupEventListeners() {\n this.canvas.addEventListener(\"mousedown\", this.handleMouseDown.bind(this))\n this.canvas.addEventListener(\"mousemove\", this.handleMouseMove.bind(this))\n this.canvas.addEventListener(\"mouseup\", this.handleMouseUp.bind(this))\n this.canvas.addEventListener(\"wheel\", this.handleWheel.bind(this))\n this.canvas.addEventListener(\"click\", this.handleClick.bind(this))\n }\n\n private handleMouseDown(e: MouseEvent) {\n this.isDragging = true\n this.lastMousePosition = { x: e.clientX, y: e.clientY }\n }\n\n private handleMouseMove(e: MouseEvent) {\n if (this.isDragging) {\n const deltaX = e.clientX - this.lastMousePosition.x\n const deltaY = e.clientY - this.lastMousePosition.y\n\n this.camera.rotation.y += deltaX * 0.01\n this.camera.rotation.x += deltaY * 0.01\n\n this.lastMousePosition = { x: e.clientX, y: e.clientY }\n }\n }\n\n private handleMouseUp() {\n this.isDragging = false\n }\n\n private handleWheel(e: WheelEvent) {\n e.preventDefault()\n this.camera.zoom = Math.max(\n 0.1,\n Math.min(5, this.camera.zoom + e.deltaY * -0.001)\n )\n }\n\n private handleClick(e: MouseEvent) {\n // Check for annotation clicks\n const rect = this.canvas.getBoundingClientRect()\n const x = e.clientX - rect.left\n const y = e.clientY - rect.top\n\n // Simple hit detection for annotations\n this.annotations.forEach((annotation) => {\n if (annotation.visible) {\n const screenPos = this.worldToScreen(annotation.position)\n const distance = Math.sqrt(\n (x - screenPos.x) ** 2 + (y - screenPos.y) ** 2\n )\n if (distance < 20) {\n // Annotation clicked\n const event = new CustomEvent(\"annotationClick\", {\n detail: annotation,\n })\n this.canvas.dispatchEvent(event)\n }\n }\n })\n }\n\n private worldToScreen(worldPos: { x: number; y: number; z: number }) {\n // Simple 3D to 2D projection\n const { width, height } = this.canvas\n return {\n x: width / 2 + worldPos.x * 100 * this.camera.zoom,\n y: height / 2 - worldPos.y * 100 * this.camera.zoom,\n }\n }\n\n private drawModel() {\n if (!this.model) return\n\n const { width, height } = this.canvas\n const centerX = width / 2\n const centerY = height / 2\n\n this.ctx.save()\n this.ctx.translate(\n centerX + this.camera.position.x,\n centerY + this.camera.position.y\n )\n this.ctx.scale(this.camera.zoom, this.camera.zoom)\n\n // Simulate 3D rotation by skewing\n const rotX = this.camera.rotation.x\n const rotY = this.camera.rotation.y\n\n this.ctx.transform(\n Math.cos(rotY),\n Math.sin(rotY) * Math.sin(rotX),\n 0,\n Math.cos(rotX),\n 0,\n 0\n )\n\n // Draw a simple 3D-ish model representation\n this.drawCube()\n\n this.ctx.restore()\n }\n\n private drawCube() {\n const size = 80\n\n // Front face\n this.ctx.fillStyle = \"#60a5fa\"\n this.ctx.fillRect(-size / 2, -size / 2, size, size)\n this.ctx.strokeStyle = \"#1e40af\"\n this.ctx.lineWidth = 2\n this.ctx.strokeRect(-size / 2, -size / 2, size, size)\n\n // Top face (isometric)\n this.ctx.fillStyle = \"#93c5fd\"\n this.ctx.beginPath()\n this.ctx.moveTo(-size / 2, -size / 2)\n this.ctx.lineTo(-size / 4, -size / 2 - size / 4)\n this.ctx.lineTo(size / 4, -size / 2 - size / 4)\n this.ctx.lineTo(size / 2, -size / 2)\n this.ctx.closePath()\n this.ctx.fill()\n this.ctx.stroke()\n\n // Right face (isometric)\n this.ctx.fillStyle = \"#3b82f6\"\n this.ctx.beginPath()\n this.ctx.moveTo(size / 2, -size / 2)\n this.ctx.lineTo(size / 4, -size / 2 - size / 4)\n this.ctx.lineTo(size / 4, size / 2 - size / 4)\n this.ctx.lineTo(size / 2, size / 2)\n this.ctx.closePath()\n this.ctx.fill()\n this.ctx.stroke()\n }\n\n private drawAnnotations() {\n this.annotations.forEach((annotation) => {\n if (!annotation.visible) return\n\n const screenPos = this.worldToScreen(annotation.position)\n\n // Draw annotation marker\n this.ctx.fillStyle = \"#ef4444\"\n this.ctx.beginPath()\n this.ctx.arc(screenPos.x, screenPos.y, 8, 0, Math.PI * 2)\n this.ctx.fill()\n\n // Draw annotation line\n this.ctx.strokeStyle = \"#ef4444\"\n this.ctx.lineWidth = 2\n this.ctx.beginPath()\n this.ctx.moveTo(screenPos.x, screenPos.y)\n this.ctx.lineTo(screenPos.x + 20, screenPos.y - 20)\n this.ctx.stroke()\n\n // Draw annotation label\n this.ctx.fillStyle = \"rgba(0, 0, 0, 0.8)\"\n this.ctx.fillRect(screenPos.x + 22, screenPos.y - 35, 100, 20)\n this.ctx.fillStyle = \"white\"\n this.ctx.font = \"12px Arial\"\n this.ctx.fillText(annotation.title, screenPos.x + 25, screenPos.y - 22)\n })\n }\n\n private render() {\n const { width, height } = this.canvas\n\n // Clear canvas\n this.ctx.fillStyle = \"#f8fafc\"\n this.ctx.fillRect(0, 0, width, height)\n\n if (this.autoRotate) {\n this.camera.rotation.y += this.autoRotateSpeed * 0.01\n }\n\n this.drawModel()\n this.drawAnnotations()\n }\n\n loadModel(url: string): Promise<ModelStats> {\n return new Promise((resolve, reject) => {\n // Simulate model loading\n setTimeout(() => {\n if (url) {\n this.model = { loaded: true, url }\n resolve({\n vertices: 8,\n faces: 12,\n materials: 1,\n textures: 0,\n size: \"2.4 KB\",\n })\n } else {\n reject(\"Invalid model URL\")\n }\n }, 1000)\n })\n }\n\n setAutoRotate(enabled: boolean, speed = 1) {\n this.autoRotate = enabled\n this.autoRotateSpeed = speed\n }\n\n setAnnotations(annotations: Annotation[]) {\n this.annotations = annotations\n }\n\n resetCamera() {\n this.camera = {\n rotation: { x: 0, y: 0 },\n zoom: 1,\n position: { x: 0, y: 0 },\n }\n }\n\n zoomIn() {\n this.camera.zoom = Math.min(5, this.camera.zoom * 1.2)\n }\n\n zoomOut() {\n this.camera.zoom = Math.max(0.1, this.camera.zoom / 1.2)\n }\n\n takeScreenshot(): string {\n return this.canvas.toDataURL(\"image/png\")\n }\n\n startAnimation() {\n const animate = () => {\n this.render()\n this.animationFrame = requestAnimationFrame(animate)\n }\n animate()\n }\n\n stopAnimation() {\n if (this.animationFrame) {\n cancelAnimationFrame(this.animationFrame)\n this.animationFrame = null\n }\n }\n\n destroy() {\n this.stopAnimation()\n }\n}\n\nconst ModelViewer3D = React.forwardRef<HTMLDivElement, ModelViewerProps>(\n (\n {\n modelUrl,\n width = 800,\n height = 600,\n autoRotate = false,\n autoRotateSpeed = 1,\n enableZoom = true,\n enablePan = true,\n enableRotate = true,\n backgroundColor = \"#f8fafc\",\n showControls = true,\n showStats = false,\n annotations = [],\n onModelLoad,\n onModelError,\n onAnnotationClick,\n className,\n ...props\n },\n ref\n ) => {\n const containerRef = React.useRef<HTMLDivElement>(null)\n const canvasRef = React.useRef<HTMLCanvasElement>(null)\n const rendererRef = React.useRef<ModelRenderer | null>(null)\n const [isLoading, setIsLoading] = React.useState(false)\n const [modelStats, setModelStats] = React.useState<ModelStats | null>(null)\n const [error, setError] = React.useState<string | null>(null)\n const [currentAutoRotate, setCurrentAutoRotate] = React.useState(autoRotate)\n\n // Initialize renderer\n React.useEffect(() => {\n if (!canvasRef.current) return\n\n const renderer = new ModelRenderer(canvasRef.current)\n rendererRef.current = renderer\n\n renderer.startAnimation()\n\n // Listen for annotation clicks\n canvasRef.current.addEventListener(\"annotationClick\", (e: any) => {\n onAnnotationClick?.(e.detail)\n })\n\n return () => {\n renderer.destroy()\n }\n }, [onAnnotationClick])\n\n // Load model\n React.useEffect(() => {\n if (!rendererRef.current || !modelUrl) return\n\n setIsLoading(true)\n setError(null)\n\n rendererRef.current\n .loadModel(modelUrl)\n .then((stats) => {\n setModelStats(stats)\n setIsLoading(false)\n onModelLoad?.(stats)\n })\n .catch((err) => {\n setError(err)\n setIsLoading(false)\n onModelError?.(err)\n })\n }, [modelUrl, onModelLoad, onModelError])\n\n // Update auto rotate\n React.useEffect(() => {\n if (rendererRef.current) {\n rendererRef.current.setAutoRotate(currentAutoRotate, autoRotateSpeed)\n }\n }, [currentAutoRotate, autoRotateSpeed])\n\n // Update annotations\n React.useEffect(() => {\n if (rendererRef.current) {\n rendererRef.current.setAnnotations(annotations)\n }\n }, [annotations])\n\n const handleResetCamera = () => {\n rendererRef.current?.resetCamera()\n }\n\n const handleZoomIn = () => {\n rendererRef.current?.zoomIn()\n }\n\n const handleZoomOut = () => {\n rendererRef.current?.zoomOut()\n }\n\n const handleToggleAutoRotate = () => {\n setCurrentAutoRotate((prev) => !prev)\n }\n\n const handleScreenshot = () => {\n if (!rendererRef.current) return\n\n const dataURL = rendererRef.current.takeScreenshot()\n const link = document.createElement(\"a\")\n link.download = \"model-screenshot.png\"\n link.href = dataURL\n link.click()\n }\n\n React.useImperativeHandle(ref, () => containerRef.current!)\n\n return (\n <div\n ref={containerRef}\n className={cn(\"relative border rounded-lg overflow-hidden\", className)}\n style={{ width, height }}\n {...props}\n >\n {/* Canvas */}\n <canvas\n ref={canvasRef}\n width={width}\n height={height}\n className=\"w-full h-full cursor-grab active:cursor-grabbing\"\n style={{ backgroundColor }}\n />\n\n {/* Loading overlay */}\n {isLoading && (\n <div className=\"absolute inset-0 bg-background/80 backdrop-blur-sm flex items-center justify-center\">\n <div className=\"flex flex-col items-center space-y-2\">\n <div className=\"animate-spin rounded-full h-8 w-8 border-2 border-primary border-t-transparent\" />\n <p className=\"text-sm text-muted-foreground\">Loading model...</p>\n </div>\n </div>\n )}\n\n {/* Error overlay */}\n {error && (\n <div className=\"absolute inset-0 bg-background/80 backdrop-blur-sm flex items-center justify-center\">\n <div className=\"text-center space-y-2\">\n <p className=\"text-sm text-destructive\">{error}</p>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setError(null)}\n >\n Dismiss\n </Button>\n </div>\n </div>\n )}\n\n {/* Controls */}\n {showControls && !isLoading && !error && (\n <div className=\"absolute top-4 right-4 flex flex-col space-y-2\">\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={handleResetCamera}\n title=\"Reset camera\"\n >\n <RotateCcw className=\"w-4 h-4\" />\n </Button>\n\n {enableZoom && (\n <>\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={handleZoomIn}\n title=\"Zoom in\"\n >\n <ZoomIn className=\"w-4 h-4\" />\n </Button>\n\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={handleZoomOut}\n title=\"Zoom out\"\n >\n <ZoomOut className=\"w-4 h-4\" />\n </Button>\n </>\n )}\n\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={handleToggleAutoRotate}\n title=\"Toggle auto rotate\"\n className={\n currentAutoRotate ? \"bg-primary text-primary-foreground\" : \"\"\n }\n >\n <Move3d className=\"w-4 h-4\" />\n </Button>\n\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={handleScreenshot}\n title=\"Take screenshot\"\n >\n <Camera className=\"w-4 h-4\" />\n </Button>\n </div>\n )}\n\n {/* Stats panel */}\n {showStats && modelStats && !isLoading && !error && (\n <div className=\"absolute bottom-4 left-4 p-3 rounded-lg bg-background/80 backdrop-blur-sm border\">\n <h4 className=\"font-semibold text-sm mb-2\">Model Stats</h4>\n <div className=\"space-y-1 text-xs text-muted-foreground\">\n <div>Vertices: {modelStats.vertices.toLocaleString()}</div>\n <div>Faces: {modelStats.faces.toLocaleString()}</div>\n <div>Materials: {modelStats.materials}</div>\n <div>Textures: {modelStats.textures}</div>\n <div>Size: {modelStats.size}</div>\n </div>\n </div>\n )}\n\n {/* Annotations panel */}\n {annotations.length > 0 && (\n <div className=\"absolute bottom-4 right-4 max-w-xs\">\n <div className=\"p-3 rounded-lg bg-background/80 backdrop-blur-sm border\">\n <h4 className=\"font-semibold text-sm mb-2\">Annotations</h4>\n <div className=\"space-y-2 max-h-32 overflow-y-auto\">\n {annotations.map((annotation) => (\n <button\n key={annotation.id}\n className=\"w-full text-left p-2 rounded text-xs border hover:bg-accent\"\n onClick={() => onAnnotationClick?.(annotation)}\n >\n <div className=\"font-medium\">{annotation.title}</div>\n {annotation.description && (\n <div className=\"text-muted-foreground line-clamp-2\">\n {annotation.description}\n </div>\n )}\n </button>\n ))}\n </div>\n </div>\n </div>\n )}\n\n {/* Instructions */}\n {!isLoading && !error && !modelUrl && (\n <div className=\"absolute inset-0 flex items-center justify-center\">\n <div className=\"text-center space-y-2\">\n <div className=\"w-16 h-16 mx-auto rounded-full bg-muted flex items-center justify-center\">\n <Eye className=\"w-8 h-8 text-muted-foreground\" />\n </div>\n <p className=\"text-sm text-muted-foreground\">No model loaded</p>\n <p className=\"text-xs text-muted-foreground\">\n Provide a modelUrl to view a 3D model\n </p>\n </div>\n </div>\n )}\n </div>\n )\n }\n)\nModelViewer3D.displayName = \"ModelViewer3D\"\n\n// Preset configurations\nconst ModelViewer3DPreset = {\n Basic: React.forwardRef<\n HTMLDivElement,\n Omit<ModelViewerProps, \"showControls\" | \"showStats\">\n >((props, ref) => (\n <ModelViewer3D\n ref={ref}\n showControls\n showStats={false}\n autoRotate\n autoRotateSpeed={0.5}\n {...props}\n />\n )),\n\n Advanced: React.forwardRef<\n HTMLDivElement,\n Omit<ModelViewerProps, \"showControls\" | \"showStats\">\n >((props, ref) => (\n <ModelViewer3D\n ref={ref}\n showControls\n showStats\n enableZoom\n enablePan\n enableRotate\n {...props}\n />\n )),\n\n Presentation: React.forwardRef<\n HTMLDivElement,\n Omit<ModelViewerProps, \"showControls\" | \"autoRotate\">\n >((props, ref) => (\n <ModelViewer3D\n ref={ref}\n showControls={false}\n autoRotate\n autoRotateSpeed={1}\n backgroundColor=\"#000000\"\n {...props}\n />\n )),\n}\n\n// Assign display names\nModelViewer3DPreset.Basic.displayName = \"ModelViewer3DPreset.Basic\"\nModelViewer3DPreset.Advanced.displayName = \"ModelViewer3DPreset.Advanced\"\nModelViewer3DPreset.Presentation.displayName =\n \"ModelViewer3DPreset.Presentation\"\n\nexport {\n ModelViewer3D,\n ModelViewer3DPreset,\n type ModelViewerProps,\n type Annotation,\n type ModelStats,\n}\n"
}
],
"type": "components:3d"
}