mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-26 23:04:57 +00:00
feat: Add WebGL 3D part viewer with JSCAD geometry support
- Add tabbed interface to switch between Exploded View and 3D Part View - Create JSCAD-to-Three.js converter for CSG operations (union, subtract, intersect) - Support cylinder, box, sphere, torus, cone geometry types with transforms - Add ThreeCanvas component with OrbitControls for camera manipulation - Add PartSelector sidebar for selecting parts to view in 3D - Extend Part type with optional geometry3d array for 3D definitions - Add geometry3d to clutch parts: flywheel, clutch-disc, pressure-plate, release-bearing - Parts without geometry3d fall back to placeholder box Dependencies: three, @react-three/fiber, @react-three/drei, @jscad/modeling Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
# WebGL 3D Part Viewer Design
|
||||
|
||||
**Date**: 2026-01-21
|
||||
**Status**: Ready for Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
Add a tabbed interface to the assembly page with two views:
|
||||
1. **Exploded View** (existing) - 2D SVG exploded diagram
|
||||
2. **3D Part View** (new) - WebGL viewer for individual parts with part selector
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ [Exploded View] [3D Part View] │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────────────┐ ┌─────────────────────────┐ │
|
||||
│ │ │ │ Part List │ │
|
||||
│ │ Three.js Canvas │ │ ───────────────────── │ │
|
||||
│ │ │ │ ○ Flywheel │ │
|
||||
│ │ (OrbitControls) │ │ ● Clutch Disc ←active │ │
|
||||
│ │ │ │ ○ Pressure Plate │ │
|
||||
│ │ │ │ ○ Release Bearing │ │
|
||||
│ └──────────────────────┘ │ ... │ │
|
||||
│ └─────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Three.js** - WebGL rendering, camera, lighting
|
||||
- **@jscad/modeling** - CSG operations (union, subtract, intersect)
|
||||
- **@jscad/regl-renderer** or custom Three.js conversion - Render JSCAD geometry
|
||||
|
||||
## JSON Schema Extension
|
||||
|
||||
### New 3D Geometry Types
|
||||
|
||||
```typescript
|
||||
interface Geometry3D {
|
||||
// Identity
|
||||
type: string
|
||||
id?: string // Reference for boolean ops
|
||||
|
||||
// Positioning
|
||||
offsetX?: number
|
||||
offsetY?: number
|
||||
offsetZ?: number
|
||||
rotateX?: number // Degrees
|
||||
rotateY?: number
|
||||
rotateZ?: number
|
||||
|
||||
// Boolean operation
|
||||
subtract?: boolean // Subtract from union of previous shapes
|
||||
intersect?: boolean // Intersect with union of previous shapes
|
||||
|
||||
// Appearance
|
||||
fill?: string
|
||||
material?: string
|
||||
opacity?: number
|
||||
}
|
||||
|
||||
// Cylinder: r, height
|
||||
interface CylinderGeometry extends Geometry3D {
|
||||
type: 'cylinder'
|
||||
r: number
|
||||
height: number
|
||||
segments?: number // Default 32
|
||||
}
|
||||
|
||||
// Box: width, height, depth
|
||||
interface BoxGeometry extends Geometry3D {
|
||||
type: 'box'
|
||||
width: number
|
||||
height: number
|
||||
depth: number
|
||||
}
|
||||
|
||||
// Sphere: r
|
||||
interface SphereGeometry extends Geometry3D {
|
||||
type: 'sphere'
|
||||
r: number
|
||||
segments?: number
|
||||
}
|
||||
|
||||
// Torus: r (major), tubeR (minor)
|
||||
interface TorusGeometry extends Geometry3D {
|
||||
type: 'torus'
|
||||
r: number
|
||||
tubeR: number
|
||||
}
|
||||
|
||||
// Cone/Frustum: r1 (bottom), r2 (top), height
|
||||
interface ConeGeometry extends Geometry3D {
|
||||
type: 'cone'
|
||||
r1: number
|
||||
r2: number
|
||||
height: number
|
||||
}
|
||||
|
||||
// Extruded polygon: points[], height
|
||||
interface ExtrudeGeometry extends Geometry3D {
|
||||
type: 'extrude'
|
||||
points: number[] // [x1,y1, x2,y2, ...]
|
||||
height: number
|
||||
}
|
||||
|
||||
// Revolved profile: points[], angle
|
||||
interface RevolveGeometry extends Geometry3D {
|
||||
type: 'revolve'
|
||||
points: number[] // [x1,y1, x2,y2, ...] profile
|
||||
angle?: number // Default 360
|
||||
}
|
||||
```
|
||||
|
||||
### Backward Compatibility
|
||||
|
||||
Existing 2D geometry types (`circle`, `rect`, `ellipse`, `polygon`) continue to work for the SVG renderer. The 3D viewer will:
|
||||
|
||||
1. Use `geometry3d` array if present
|
||||
2. Fall back to auto-extruding `geometry` if no `geometry3d`
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "flywheel",
|
||||
"name": "Flywheel",
|
||||
"geometry": [
|
||||
{ "type": "circle", "r": 55, "fill": "#555" }
|
||||
],
|
||||
"geometry3d": [
|
||||
{ "type": "cylinder", "r": 55, "height": 25, "fill": "#555" },
|
||||
{ "type": "cylinder", "r": 12, "height": 25, "subtract": true }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Component Structure
|
||||
|
||||
```
|
||||
src/components/
|
||||
├── AssemblyTabs.tsx # Tab switcher component
|
||||
├── DiagramRenderer.tsx # Existing 2D SVG (unchanged)
|
||||
├── PartViewer3D.tsx # New 3D viewer container
|
||||
├── ThreeCanvas.tsx # Three.js canvas + OrbitControls
|
||||
├── PartSelector.tsx # Part list for 3D view
|
||||
└── lib/
|
||||
└── jscad-to-three.ts # Convert JSCAD geometry to Three.js
|
||||
```
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Infrastructure
|
||||
1. Install dependencies (`three`, `@react-three/fiber`, `@react-three/drei`, `@jscad/modeling`)
|
||||
2. Create tab component and wire into assembly page
|
||||
3. Create basic Three.js canvas with OrbitControls
|
||||
|
||||
### Phase 2: JSCAD Integration
|
||||
4. Build geometry parser (JSON → JSCAD primitives)
|
||||
5. Build JSCAD-to-Three.js converter
|
||||
6. Implement boolean operations (subtract, intersect)
|
||||
|
||||
### Phase 3: Part Viewer
|
||||
7. Create part selector sidebar
|
||||
8. Wire up part selection to render geometry
|
||||
9. Apply materials/colors from JSON
|
||||
|
||||
### Phase 4: Sample Parts
|
||||
10. Add `geometry3d` to a few parts (gearbox clutch components)
|
||||
11. Test and refine
|
||||
|
||||
## File Changes
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `package.json` | Add three, @react-three/fiber, @react-three/drei, @jscad/modeling |
|
||||
| `src/lib/types.ts` | Add Geometry3D types |
|
||||
| `src/app/[...]/page.tsx` | Add tab state, render tabs |
|
||||
| `src/components/AssemblyTabs.tsx` | New - tab UI |
|
||||
| `src/components/PartViewer3D.tsx` | New - 3D viewer |
|
||||
| `src/components/ThreeCanvas.tsx` | New - Three.js setup |
|
||||
| `src/components/PartSelector.tsx` | New - part list |
|
||||
| `src/lib/jscad-to-three.ts` | New - geometry conversion |
|
||||
| `public/.../gearbox/parts/*.json` | Add geometry3d to clutch parts |
|
||||
|
||||
## Risks & Mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| JSCAD bundle size | Tree-shake to only import used ops |
|
||||
| CSG performance | Cache converted Three.js geometry |
|
||||
| Mobile WebGL | Graceful fallback to 2D only |
|
||||
712
exploded-diagrams/package-lock.json
generated
712
exploded-diagrams/package-lock.json
generated
@@ -8,18 +8,38 @@
|
||||
"name": "exploded-diagrams",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@jscad/modeling": "^2.12.6",
|
||||
"@react-three/drei": "^10.7.7",
|
||||
"@react-three/fiber": "^9.5.0",
|
||||
"next": "15.3.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
"react-dom": "^19.0.0",
|
||||
"three": "^0.182.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@types/three": "^0.182.0",
|
||||
"sass": "^1.97.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dimforge/rapier3d-compat": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
|
||||
"integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
|
||||
@@ -496,6 +516,30 @@
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@jscad/modeling": {
|
||||
"version": "2.12.6",
|
||||
"resolved": "https://registry.npmjs.org/@jscad/modeling/-/modeling-2.12.6.tgz",
|
||||
"integrity": "sha512-RyT7hrxuq9UHuBmsC8o+KzimQCY6En2IFiSYPDbSlNFKLFdOc3Kyq9NXpt5+RNQhrhLz6uo15XDtnE43EFF3YQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@mediapipe/tasks-vision": {
|
||||
"version": "0.10.17",
|
||||
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz",
|
||||
"integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@monogrid/gainmap-js": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz",
|
||||
"integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"promise-worker-transferable": "^1.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">= 0.159.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "15.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.4.tgz",
|
||||
@@ -926,6 +970,94 @@
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-three/drei": {
|
||||
"version": "10.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.7.tgz",
|
||||
"integrity": "sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@mediapipe/tasks-vision": "0.10.17",
|
||||
"@monogrid/gainmap-js": "^3.0.6",
|
||||
"@use-gesture/react": "^10.3.1",
|
||||
"camera-controls": "^3.1.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"detect-gpu": "^5.0.56",
|
||||
"glsl-noise": "^0.0.0",
|
||||
"hls.js": "^1.5.17",
|
||||
"maath": "^0.10.8",
|
||||
"meshline": "^3.3.1",
|
||||
"stats-gl": "^2.2.8",
|
||||
"stats.js": "^0.17.0",
|
||||
"suspend-react": "^0.1.3",
|
||||
"three-mesh-bvh": "^0.8.3",
|
||||
"three-stdlib": "^2.35.6",
|
||||
"troika-three-text": "^0.52.4",
|
||||
"tunnel-rat": "^0.1.2",
|
||||
"use-sync-external-store": "^1.4.0",
|
||||
"utility-types": "^3.11.0",
|
||||
"zustand": "^5.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-three/fiber": "^9.0.0",
|
||||
"react": "^19",
|
||||
"react-dom": "^19",
|
||||
"three": ">=0.159"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@react-three/fiber": {
|
||||
"version": "9.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.5.0.tgz",
|
||||
"integrity": "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.17.8",
|
||||
"@types/webxr": "*",
|
||||
"base64-js": "^1.5.1",
|
||||
"buffer": "^6.0.3",
|
||||
"its-fine": "^2.0.0",
|
||||
"react-use-measure": "^2.1.7",
|
||||
"scheduler": "^0.27.0",
|
||||
"suspend-react": "^0.1.3",
|
||||
"use-sync-external-store": "^1.4.0",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": ">=43.0",
|
||||
"expo-asset": ">=8.4",
|
||||
"expo-file-system": ">=11.0",
|
||||
"expo-gl": ">=11.0",
|
||||
"react": ">=19 <19.3",
|
||||
"react-dom": ">=19 <19.3",
|
||||
"react-native": ">=0.78",
|
||||
"three": ">=0.156"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"expo": {
|
||||
"optional": true
|
||||
},
|
||||
"expo-asset": {
|
||||
"optional": true
|
||||
},
|
||||
"expo-file-system": {
|
||||
"optional": true
|
||||
},
|
||||
"expo-gl": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/counter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||
@@ -941,6 +1073,18 @@
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tweenjs/tween.js": {
|
||||
"version": "23.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
|
||||
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/draco3d": {
|
||||
"version": "1.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz",
|
||||
"integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz",
|
||||
@@ -951,11 +1095,16 @@
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/offscreencanvas": {
|
||||
"version": "2019.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
|
||||
"integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz",
|
||||
"integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
@@ -971,6 +1120,119 @@
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-reconciler": {
|
||||
"version": "0.28.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz",
|
||||
"integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/stats.js": {
|
||||
"version": "0.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
|
||||
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/three": {
|
||||
"version": "0.182.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.182.0.tgz",
|
||||
"integrity": "sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@dimforge/rapier3d-compat": "~0.12.0",
|
||||
"@tweenjs/tween.js": "~23.1.3",
|
||||
"@types/stats.js": "*",
|
||||
"@types/webxr": ">=0.5.17",
|
||||
"@webgpu/types": "*",
|
||||
"fflate": "~0.8.2",
|
||||
"meshoptimizer": "~0.22.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/webxr": {
|
||||
"version": "0.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
|
||||
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@use-gesture/core": {
|
||||
"version": "10.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz",
|
||||
"integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@use-gesture/react": {
|
||||
"version": "10.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz",
|
||||
"integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@use-gesture/core": "10.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@webgpu/types": {
|
||||
"version": "0.1.69",
|
||||
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz",
|
||||
"integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bidi-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
||||
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"require-from-string": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
@@ -982,6 +1244,19 @@
|
||||
"node": ">=10.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/camera-controls": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.2.tgz",
|
||||
"integrity": "sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22.0.0",
|
||||
"npm": ">=10.5.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">=0.126.1"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001765",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz",
|
||||
@@ -1024,13 +1299,53 @@
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-env": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
|
||||
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"cross-env": "src/bin/cross-env.js",
|
||||
"cross-env-shell": "src/bin/cross-env-shell.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.14",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/detect-gpu": {
|
||||
"version": "5.0.70",
|
||||
"resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz",
|
||||
"integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"webgl-constants": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
@@ -1041,6 +1356,56 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/draco3d": {
|
||||
"version": "1.5.7",
|
||||
"resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz",
|
||||
"integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glsl-noise": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz",
|
||||
"integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/hls.js": {
|
||||
"version": "1.6.15",
|
||||
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz",
|
||||
"integrity": "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
|
||||
@@ -1071,6 +1436,64 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
|
||||
"integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/its-fine": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz",
|
||||
"integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react-reconciler": "^0.28.9"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/maath": {
|
||||
"version": "0.10.8",
|
||||
"resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz",
|
||||
"integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/three": ">=0.134.0",
|
||||
"three": ">=0.134.0"
|
||||
}
|
||||
},
|
||||
"node_modules/meshline": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz",
|
||||
"integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"three": ">=0.137"
|
||||
}
|
||||
},
|
||||
"node_modules/meshoptimizer": {
|
||||
"version": "0.22.0",
|
||||
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.22.0.tgz",
|
||||
"integrity": "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
@@ -1151,6 +1574,15 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -1198,6 +1630,22 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/potpack": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz",
|
||||
"integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/promise-worker-transferable": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz",
|
||||
"integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"is-promise": "^2.1.0",
|
||||
"lie": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||
@@ -1219,6 +1667,21 @@
|
||||
"react": "^19.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/react-use-measure": {
|
||||
"version": "2.1.7",
|
||||
"resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz",
|
||||
"integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.13",
|
||||
"react-dom": ">=16.13"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
@@ -1233,6 +1696,15 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.97.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.97.2.tgz",
|
||||
@@ -1318,6 +1790,27 @@
|
||||
"@img/sharp-win32-x64": "0.34.5"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -1327,6 +1820,32 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stats-gl": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz",
|
||||
"integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/three": "*",
|
||||
"three": "^0.170.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/three": "*",
|
||||
"three": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/stats-gl/node_modules/three": {
|
||||
"version": "0.170.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz",
|
||||
"integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stats.js": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz",
|
||||
"integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/streamsearch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
@@ -1358,12 +1877,126 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/suspend-react": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz",
|
||||
"integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/three": {
|
||||
"version": "0.182.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.182.0.tgz",
|
||||
"integrity": "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/three-mesh-bvh": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz",
|
||||
"integrity": "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"three": ">= 0.159.0"
|
||||
}
|
||||
},
|
||||
"node_modules/three-stdlib": {
|
||||
"version": "2.36.1",
|
||||
"resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz",
|
||||
"integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/draco3d": "^1.4.0",
|
||||
"@types/offscreencanvas": "^2019.6.4",
|
||||
"@types/webxr": "^0.5.2",
|
||||
"draco3d": "^1.4.1",
|
||||
"fflate": "^0.6.9",
|
||||
"potpack": "^1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">=0.128.0"
|
||||
}
|
||||
},
|
||||
"node_modules/three-stdlib/node_modules/fflate": {
|
||||
"version": "0.6.10",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz",
|
||||
"integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/troika-three-text": {
|
||||
"version": "0.52.4",
|
||||
"resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz",
|
||||
"integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bidi-js": "^1.0.2",
|
||||
"troika-three-utils": "^0.52.4",
|
||||
"troika-worker-utils": "^0.52.0",
|
||||
"webgl-sdf-generator": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"three": ">=0.125.0"
|
||||
}
|
||||
},
|
||||
"node_modules/troika-three-utils": {
|
||||
"version": "0.52.4",
|
||||
"resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz",
|
||||
"integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"three": ">=0.125.0"
|
||||
}
|
||||
},
|
||||
"node_modules/troika-worker-utils": {
|
||||
"version": "0.52.0",
|
||||
"resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz",
|
||||
"integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tunnel-rat": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz",
|
||||
"integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"zustand": "^4.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/tunnel-rat/node_modules/zustand": {
|
||||
"version": "4.5.7",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
|
||||
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "^1.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
@@ -1384,6 +2017,79 @@
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/utility-types": {
|
||||
"version": "3.11.0",
|
||||
"resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz",
|
||||
"integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/webgl-constants": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz",
|
||||
"integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg=="
|
||||
},
|
||||
"node_modules/webgl-sdf-generator": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz",
|
||||
"integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "5.0.10",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.10.tgz",
|
||||
"integrity": "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=18.0.0",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=18.0.0",
|
||||
"use-sync-external-store": ">=1.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"use-sync-external-store": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,14 +12,19 @@
|
||||
"clean": "rm -rf .next node_modules/.cache"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jscad/modeling": "^2.12.6",
|
||||
"@react-three/drei": "^10.7.7",
|
||||
"@react-three/fiber": "^9.5.0",
|
||||
"next": "15.3.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
"react-dom": "^19.0.0",
|
||||
"three": "^0.182.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@types/three": "^0.182.0",
|
||||
"sass": "^1.97.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
|
||||
@@ -31,5 +31,11 @@
|
||||
{ "type": "circle", "r": 3, "offsetX": 40, "offsetY": 0, "fill": "#333" },
|
||||
{ "type": "circle", "r": 3, "offsetX": 0, "offsetY": -40, "fill": "#333" },
|
||||
{ "type": "circle", "r": 3, "offsetX": 0, "offsetY": 40, "fill": "#333" }
|
||||
],
|
||||
"geometry3d": [
|
||||
{ "type": "cylinder", "r": 50, "height": 8, "fill": "#8b4513" },
|
||||
{ "type": "cylinder", "r": 35, "height": 8, "subtract": true },
|
||||
{ "type": "cylinder", "r": 18, "height": 20, "fill": "#666" },
|
||||
{ "type": "cylinder", "r": 10, "height": 20, "subtract": true }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -39,5 +39,11 @@
|
||||
{ "type": "circle", "r": 8, "offsetX": -35, "offsetY": -35, "fill": "#333" },
|
||||
{ "type": "circle", "r": 8, "offsetX": 35, "offsetY": 35, "fill": "#333" },
|
||||
{ "type": "text", "content": "DMF", "offsetY": 3, "fill": "#888", "fontSize": 10 }
|
||||
],
|
||||
"geometry3d": [
|
||||
{ "type": "cylinder", "r": 55, "height": 25, "fill": "#555" },
|
||||
{ "type": "cylinder", "r": 50, "height": 20, "offsetZ": 2.5, "fill": "#666" },
|
||||
{ "type": "cylinder", "r": 12, "height": 25, "subtract": true },
|
||||
{ "type": "torus", "r": 53, "tubeR": 3, "offsetZ": 10, "fill": "#444" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -38,5 +38,11 @@
|
||||
{ "type": "circle", "r": 6, "offsetX": 55, "offsetY": 0, "fill": "#333" },
|
||||
{ "type": "circle", "r": 6, "offsetX": 0, "offsetY": -55, "fill": "#333" },
|
||||
{ "type": "circle", "r": 6, "offsetX": 0, "offsetY": 55, "fill": "#333" }
|
||||
],
|
||||
"geometry3d": [
|
||||
{ "type": "cylinder", "r": 52, "height": 15, "fill": "#555" },
|
||||
{ "type": "cylinder", "r": 45, "height": 10, "offsetZ": -2.5, "fill": "#666" },
|
||||
{ "type": "cylinder", "r": 20, "height": 15, "subtract": true },
|
||||
{ "type": "cylinder", "r": 48, "height": 3, "offsetZ": 6, "fill": "#777" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -29,5 +29,11 @@
|
||||
{ "type": "circle", "r": 22, "fill": "#4a4a4a" },
|
||||
{ "type": "circle", "r": 15, "fill": "#3a3a3a" },
|
||||
{ "type": "circle", "r": 10, "fill": "#2a2a2a" }
|
||||
],
|
||||
"geometry3d": [
|
||||
{ "type": "cylinder", "r": 22, "height": 18, "fill": "#444" },
|
||||
{ "type": "cylinder", "r": 18, "height": 12, "offsetZ": 3, "fill": "#555" },
|
||||
{ "type": "cylinder", "r": 12, "height": 18, "subtract": true },
|
||||
{ "type": "torus", "r": 15, "tubeR": 4, "fill": "#666" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import DiagramRenderer from '@/components/DiagramRenderer'
|
||||
import Controls from '@/components/Controls'
|
||||
import Sidebar from '@/components/Sidebar'
|
||||
import Tooltip from '@/components/Tooltip'
|
||||
import AssemblyTabs from '@/components/AssemblyTabs'
|
||||
import PartViewer3D from '@/components/PartViewer3D'
|
||||
import { loadAssembly, loadMaterials } from '@/lib/loader'
|
||||
import type { Assembly, Materials, Part } from '@/lib/types'
|
||||
|
||||
@@ -22,6 +24,7 @@ export default function AssemblyPage() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'exploded' | '3d'>('exploded')
|
||||
const [explosion, setExplosion] = useState(50)
|
||||
const [rotation, setRotation] = useState(0)
|
||||
const [highlightedPart, setHighlightedPart] = useState<string | null>(null)
|
||||
@@ -126,39 +129,46 @@ export default function AssemblyPage() {
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb path={[category, manufacturer, product, assembly]} />
|
||||
<AssemblyTabs activeTab={activeTab} onTabChange={setActiveTab} />
|
||||
|
||||
<Controls
|
||||
explosion={explosion}
|
||||
rotation={rotation}
|
||||
onExplosionChange={setExplosion}
|
||||
onRotationChange={setRotation}
|
||||
onAnimate={handleAnimate}
|
||||
onExport={handleExport}
|
||||
/>
|
||||
|
||||
<div className="main-layout">
|
||||
<div className="diagram-container">
|
||||
<DiagramRenderer
|
||||
assembly={data}
|
||||
materials={materials}
|
||||
{activeTab === 'exploded' ? (
|
||||
<>
|
||||
<Controls
|
||||
explosion={explosion}
|
||||
rotation={rotation}
|
||||
highlightedPart={highlightedPart}
|
||||
onPartHover={handlePartHover}
|
||||
onExplosionChange={setExplosion}
|
||||
onRotationChange={setRotation}
|
||||
onAnimate={handleAnimate}
|
||||
onExport={handleExport}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Sidebar
|
||||
assembly={data}
|
||||
materials={materials}
|
||||
highlightedPart={highlightedPart}
|
||||
selectedPart={selectedPart}
|
||||
onPartHover={handlePartHover}
|
||||
onPartSelect={handlePartSelect}
|
||||
/>
|
||||
</div>
|
||||
<div className="main-layout">
|
||||
<div className="diagram-container">
|
||||
<DiagramRenderer
|
||||
assembly={data}
|
||||
materials={materials}
|
||||
explosion={explosion}
|
||||
rotation={rotation}
|
||||
highlightedPart={highlightedPart}
|
||||
onPartHover={handlePartHover}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Tooltip tooltip={tooltip} materials={materials} />
|
||||
<Sidebar
|
||||
assembly={data}
|
||||
materials={materials}
|
||||
highlightedPart={highlightedPart}
|
||||
selectedPart={selectedPart}
|
||||
onPartHover={handlePartHover}
|
||||
onPartSelect={handlePartSelect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Tooltip tooltip={tooltip} materials={materials} />
|
||||
</>
|
||||
) : (
|
||||
<PartViewer3D parts={data.parts} materials={materials} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -545,3 +545,110 @@ button {
|
||||
margin-bottom: 10px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Assembly Tabs */
|
||||
.assembly-tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
margin: 0 20px 20px;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.assembly-tab {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -2px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: #333;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #1a1a1a;
|
||||
border-bottom-color: #1a1a1a;
|
||||
}
|
||||
}
|
||||
|
||||
/* 3D Part Viewer */
|
||||
.part-viewer-3d {
|
||||
display: flex;
|
||||
height: calc(100vh - 200px);
|
||||
min-height: 500px;
|
||||
margin: 0 20px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.three-canvas-container {
|
||||
flex: 1;
|
||||
background: #f0f0f0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.part-selector {
|
||||
width: 250px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.part-selector-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.part-selector-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.part-selector-item {
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: #e8f4fc;
|
||||
border-left: 3px solid #0066cc;
|
||||
}
|
||||
}
|
||||
|
||||
.part-selector-name {
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.part-selector-number {
|
||||
font-family: monospace;
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.no-part-selected {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
27
exploded-diagrams/src/components/AssemblyTabs.tsx
Normal file
27
exploded-diagrams/src/components/AssemblyTabs.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
'use client'
|
||||
|
||||
type TabValue = 'exploded' | '3d'
|
||||
|
||||
interface AssemblyTabsProps {
|
||||
activeTab: TabValue
|
||||
onTabChange: (tab: TabValue) => void
|
||||
}
|
||||
|
||||
export default function AssemblyTabs({ activeTab, onTabChange }: AssemblyTabsProps) {
|
||||
return (
|
||||
<div className="assembly-tabs">
|
||||
<button
|
||||
className={`assembly-tab ${activeTab === 'exploded' ? 'active' : ''}`}
|
||||
onClick={() => onTabChange('exploded')}
|
||||
>
|
||||
Exploded View
|
||||
</button>
|
||||
<button
|
||||
className={`assembly-tab ${activeTab === '3d' ? 'active' : ''}`}
|
||||
onClick={() => onTabChange('3d')}
|
||||
>
|
||||
3D Part View
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
28
exploded-diagrams/src/components/PartSelector.tsx
Normal file
28
exploded-diagrams/src/components/PartSelector.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
|
||||
import type { Part } from '@/lib/types'
|
||||
|
||||
interface PartSelectorProps {
|
||||
parts: Part[]
|
||||
selectedPartId: string | null
|
||||
onSelectPart: (partId: string) => void
|
||||
}
|
||||
|
||||
export default function PartSelector({ parts, selectedPartId, onSelectPart }: PartSelectorProps) {
|
||||
return (
|
||||
<div className="part-selector">
|
||||
<div className="part-selector-list">
|
||||
{parts.map(part => (
|
||||
<div
|
||||
key={part.id}
|
||||
className={`part-selector-item ${selectedPartId === part.id ? 'selected' : ''}`}
|
||||
onClick={() => onSelectPart(part.id)}
|
||||
>
|
||||
<div className="part-selector-name">{part.name}</div>
|
||||
<div className="part-selector-number">{part.partNumber}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
92
exploded-diagrams/src/components/PartViewer3D.tsx
Normal file
92
exploded-diagrams/src/components/PartViewer3D.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useMemo } from 'react'
|
||||
import * as THREE from 'three'
|
||||
import ThreeCanvas from './ThreeCanvas'
|
||||
import PartSelector from './PartSelector'
|
||||
import { geometryToThree } from '@/lib/jscad-to-three'
|
||||
import type { Part, Materials } from '@/lib/types'
|
||||
|
||||
interface PartViewer3DProps {
|
||||
parts: Part[]
|
||||
materials: Materials
|
||||
}
|
||||
|
||||
interface PartMeshProps {
|
||||
part: Part
|
||||
materials: Materials
|
||||
}
|
||||
|
||||
function PartMesh({ part, materials }: PartMeshProps) {
|
||||
const geometry = useMemo(() => {
|
||||
if (part.geometry3d && part.geometry3d.length > 0) {
|
||||
// Convert geometry3d to Three.js BufferGeometry
|
||||
return geometryToThree(part.geometry3d)
|
||||
}
|
||||
// Fallback to a simple box if no geometry3d present
|
||||
return new THREE.BoxGeometry(2, 2, 2)
|
||||
}, [part.geometry3d])
|
||||
|
||||
const materialColor = useMemo(() => {
|
||||
// Try to get color from first geometry3d fill, or material reference, or part material
|
||||
if (part.geometry3d && part.geometry3d.length > 0) {
|
||||
const firstGeom = part.geometry3d[0]
|
||||
if (firstGeom.fill) {
|
||||
return firstGeom.fill
|
||||
}
|
||||
if (firstGeom.material && materials[firstGeom.material]) {
|
||||
// Use first gradient stop color
|
||||
const mat = materials[firstGeom.material]
|
||||
if (mat.gradient.stops.length > 0) {
|
||||
return mat.gradient.stops[0].color
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fall back to part material reference
|
||||
if (part.material && materials[part.material]) {
|
||||
const mat = materials[part.material]
|
||||
if (mat.gradient.stops.length > 0) {
|
||||
return mat.gradient.stops[0].color
|
||||
}
|
||||
}
|
||||
// Default gray color
|
||||
return '#888888'
|
||||
}, [part, materials])
|
||||
|
||||
return (
|
||||
<mesh geometry={geometry}>
|
||||
<meshStandardMaterial color={materialColor} />
|
||||
</mesh>
|
||||
)
|
||||
}
|
||||
|
||||
export default function PartViewer3D({ parts, materials }: PartViewer3DProps) {
|
||||
const [selectedPartId, setSelectedPartId] = useState<string | null>(
|
||||
parts.length > 0 ? parts[0].id : null
|
||||
)
|
||||
|
||||
const selectedPart = useMemo(() => {
|
||||
return parts.find(p => p.id === selectedPartId) ?? null
|
||||
}, [parts, selectedPartId])
|
||||
|
||||
const handleSelectPart = (partId: string) => {
|
||||
setSelectedPartId(partId)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="part-viewer-3d">
|
||||
<div className="three-canvas-container">
|
||||
<ThreeCanvas>
|
||||
{selectedPart && (
|
||||
<PartMesh part={selectedPart} materials={materials} />
|
||||
)}
|
||||
</ThreeCanvas>
|
||||
</div>
|
||||
<PartSelector
|
||||
parts={parts}
|
||||
selectedPartId={selectedPartId}
|
||||
onSelectPart={handleSelectPart}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
32
exploded-diagrams/src/components/ThreeCanvas.tsx
Normal file
32
exploded-diagrams/src/components/ThreeCanvas.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
'use client'
|
||||
|
||||
import { Canvas } from '@react-three/fiber'
|
||||
import { OrbitControls } from '@react-three/drei'
|
||||
|
||||
interface ThreeCanvasProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export default function ThreeCanvas({ children }: ThreeCanvasProps) {
|
||||
return (
|
||||
<Canvas
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
camera={{ position: [5, 5, 5], fov: 50 }}
|
||||
>
|
||||
<color attach="background" args={['#f0f0f0']} />
|
||||
|
||||
{/* Lighting */}
|
||||
<ambientLight intensity={0.5} />
|
||||
<directionalLight position={[10, 10, 5]} intensity={1} />
|
||||
|
||||
{/* Ground plane grid */}
|
||||
<gridHelper args={[20, 20, '#888888', '#cccccc']} />
|
||||
|
||||
{/* Camera controls */}
|
||||
<OrbitControls makeDefault />
|
||||
|
||||
{/* User content */}
|
||||
{children}
|
||||
</Canvas>
|
||||
)
|
||||
}
|
||||
175
exploded-diagrams/src/lib/jscad-to-three.ts
Normal file
175
exploded-diagrams/src/lib/jscad-to-three.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import * as THREE from 'three'
|
||||
import { primitives, booleans, transforms, geometries } from '@jscad/modeling'
|
||||
import type { Geom3 } from '@jscad/modeling/src/geometries/types'
|
||||
import type { Geometry3D } from './types'
|
||||
|
||||
const { cylinder, cuboid, sphere, torus, cylinderElliptic } = primitives
|
||||
const { subtract, union, intersect } = booleans
|
||||
const { translate, rotateX, rotateY, rotateZ } = transforms
|
||||
const { geom3 } = geometries
|
||||
|
||||
/**
|
||||
* Creates a JSCAD primitive from a Geometry3D definition
|
||||
*/
|
||||
function createPrimitive(geom: Geometry3D): Geom3 {
|
||||
switch (geom.type) {
|
||||
case 'cylinder': {
|
||||
const radius = geom.r ?? 1
|
||||
const height = geom.height ?? 1
|
||||
const segments = geom.segments ?? 32
|
||||
return cylinder({ radius, height, segments })
|
||||
}
|
||||
|
||||
case 'box': {
|
||||
const width = geom.width ?? 1
|
||||
const height = geom.height ?? 1
|
||||
const depth = geom.depth ?? 1
|
||||
// JSCAD cuboid uses [width, depth, height] order
|
||||
return cuboid({ size: [width, depth, height] })
|
||||
}
|
||||
|
||||
case 'sphere': {
|
||||
const radius = geom.r ?? 1
|
||||
const segments = geom.segments ?? 32
|
||||
return sphere({ radius, segments })
|
||||
}
|
||||
|
||||
case 'torus': {
|
||||
const outerRadius = geom.r ?? 1
|
||||
const tubeRadius = geom.tubeR ?? 0.25
|
||||
const innerRadius = outerRadius - tubeRadius
|
||||
return torus({ innerRadius, outerRadius })
|
||||
}
|
||||
|
||||
case 'cone': {
|
||||
const r1 = geom.r1 ?? 1 // bottom radius
|
||||
const r2 = geom.r2 ?? 0 // top radius
|
||||
const height = geom.height ?? 1
|
||||
const segments = geom.segments ?? 32
|
||||
return cylinderElliptic({
|
||||
startRadius: [r1, r1],
|
||||
endRadius: [r2, r2],
|
||||
height,
|
||||
segments
|
||||
})
|
||||
}
|
||||
|
||||
default:
|
||||
// Return a small default cube for unsupported types
|
||||
console.warn(`Unsupported geometry type: ${geom.type}`)
|
||||
return cuboid({ size: [1, 1, 1] })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies transforms to a JSCAD geometry based on Geometry3D properties
|
||||
*/
|
||||
function applyTransforms(geom: Geom3, def: Geometry3D): Geom3 {
|
||||
let result = geom
|
||||
|
||||
// Apply rotations (convert degrees to radians)
|
||||
if (def.rotateX !== undefined && def.rotateX !== 0) {
|
||||
result = rotateX((def.rotateX * Math.PI) / 180, result)
|
||||
}
|
||||
if (def.rotateY !== undefined && def.rotateY !== 0) {
|
||||
result = rotateY((def.rotateY * Math.PI) / 180, result)
|
||||
}
|
||||
if (def.rotateZ !== undefined && def.rotateZ !== 0) {
|
||||
result = rotateZ((def.rotateZ * Math.PI) / 180, result)
|
||||
}
|
||||
|
||||
// Apply translation
|
||||
const offsetX = def.offsetX ?? 0
|
||||
const offsetY = def.offsetY ?? 0
|
||||
const offsetZ = def.offsetZ ?? 0
|
||||
if (offsetX !== 0 || offsetY !== 0 || offsetZ !== 0) {
|
||||
result = translate([offsetX, offsetY, offsetZ], result)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a JSCAD Geom3 from an array of Geometry3D definitions.
|
||||
* Handles boolean operations (subtract, intersect) and transforms.
|
||||
*/
|
||||
export function buildJscadGeometry(geometry3d: Geometry3D[]): Geom3 {
|
||||
if (geometry3d.length === 0) {
|
||||
return cuboid({ size: [0.001, 0.001, 0.001] })
|
||||
}
|
||||
|
||||
let accumulated: Geom3 | null = null
|
||||
|
||||
for (const def of geometry3d) {
|
||||
// Create the primitive
|
||||
let primitive = createPrimitive(def)
|
||||
|
||||
// Apply transforms
|
||||
primitive = applyTransforms(primitive, def)
|
||||
|
||||
// Handle boolean operations
|
||||
if (accumulated === null) {
|
||||
accumulated = primitive
|
||||
} else if (def.subtract) {
|
||||
accumulated = subtract(accumulated, primitive)
|
||||
} else if (def.intersect) {
|
||||
accumulated = intersect(accumulated, primitive)
|
||||
} else {
|
||||
accumulated = union(accumulated, primitive)
|
||||
}
|
||||
}
|
||||
|
||||
return accumulated!
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a JSCAD Geom3 to a Three.js BufferGeometry.
|
||||
* Extracts polygons and converts them to vertices/normals.
|
||||
*/
|
||||
export function jscadToThree(geom: Geom3): THREE.BufferGeometry {
|
||||
const polygons = geom3.toPolygons(geom)
|
||||
|
||||
const positions: number[] = []
|
||||
const normals: number[] = []
|
||||
|
||||
for (const polygon of polygons) {
|
||||
const vertices = polygon.vertices
|
||||
|
||||
if (vertices.length < 3) continue
|
||||
|
||||
// Calculate face normal using first three vertices
|
||||
const v0 = new THREE.Vector3(vertices[0][0], vertices[0][1], vertices[0][2])
|
||||
const v1 = new THREE.Vector3(vertices[1][0], vertices[1][1], vertices[1][2])
|
||||
const v2 = new THREE.Vector3(vertices[2][0], vertices[2][1], vertices[2][2])
|
||||
|
||||
const edge1 = new THREE.Vector3().subVectors(v1, v0)
|
||||
const edge2 = new THREE.Vector3().subVectors(v2, v0)
|
||||
const normal = new THREE.Vector3().crossVectors(edge1, edge2).normalize()
|
||||
|
||||
// Triangulate the polygon (fan triangulation)
|
||||
for (let i = 1; i < vertices.length - 1; i++) {
|
||||
// Triangle: vertex[0], vertex[i], vertex[i+1]
|
||||
const tri = [vertices[0], vertices[i], vertices[i + 1]]
|
||||
|
||||
for (const vert of tri) {
|
||||
positions.push(vert[0], vert[1], vert[2])
|
||||
normals.push(normal.x, normal.y, normal.z)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const geometry = new THREE.BufferGeometry()
|
||||
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
|
||||
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3))
|
||||
|
||||
return geometry
|
||||
}
|
||||
|
||||
/**
|
||||
* Main export: Converts an array of Geometry3D definitions to Three.js BufferGeometry.
|
||||
* Builds JSCAD geometry with boolean operations, then converts to Three.js format.
|
||||
*/
|
||||
export function geometryToThree(geometry3d: Geometry3D[]): THREE.BufferGeometry {
|
||||
const jscadGeom = buildJscadGeometry(geometry3d)
|
||||
return jscadToThree(jscadGeom)
|
||||
}
|
||||
@@ -44,6 +44,54 @@ export interface Geometry {
|
||||
fontFamily?: string
|
||||
}
|
||||
|
||||
// 3D Geometry types for WebGL viewer
|
||||
export interface Geometry3D {
|
||||
type: 'cylinder' | 'box' | 'sphere' | 'torus' | 'cone' | 'extrude' | 'revolve'
|
||||
|
||||
// Positioning
|
||||
offsetX?: number
|
||||
offsetY?: number
|
||||
offsetZ?: number
|
||||
rotateX?: number // degrees
|
||||
rotateY?: number
|
||||
rotateZ?: number
|
||||
|
||||
// Boolean operations
|
||||
subtract?: boolean
|
||||
intersect?: boolean
|
||||
|
||||
// Appearance
|
||||
fill?: string
|
||||
material?: string
|
||||
opacity?: number
|
||||
|
||||
// Cylinder
|
||||
r?: number
|
||||
height?: number
|
||||
segments?: number
|
||||
|
||||
// Box
|
||||
width?: number
|
||||
depth?: number
|
||||
// height shared with cylinder
|
||||
|
||||
// Sphere
|
||||
// r shared with cylinder
|
||||
|
||||
// Torus
|
||||
tubeR?: number
|
||||
|
||||
// Cone/Frustum
|
||||
r1?: number // bottom radius
|
||||
r2?: number // top radius
|
||||
|
||||
// Extrude
|
||||
points?: number[] // [x1,y1, x2,y2, ...]
|
||||
|
||||
// Revolve
|
||||
angle?: number // default 360
|
||||
}
|
||||
|
||||
export interface Tool {
|
||||
name: string
|
||||
size: string
|
||||
@@ -82,6 +130,7 @@ export interface Part {
|
||||
quantity: number
|
||||
baseY: number
|
||||
geometry: Geometry[]
|
||||
geometry3d?: Geometry3D[]
|
||||
installation?: Installation
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user