diff --git a/exploded-diagrams/docs/plans/2026-01-21-webgl-part-viewer-design.md b/exploded-diagrams/docs/plans/2026-01-21-webgl-part-viewer-design.md new file mode 100644 index 000000000..21772a3c1 --- /dev/null +++ b/exploded-diagrams/docs/plans/2026-01-21-webgl-part-viewer-design.md @@ -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 | diff --git a/exploded-diagrams/package-lock.json b/exploded-diagrams/package-lock.json index 8822dfefa..f2bb1d4b1 100644 --- a/exploded-diagrams/package-lock.json +++ b/exploded-diagrams/package-lock.json @@ -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 + } + } } } } diff --git a/exploded-diagrams/package.json b/exploded-diagrams/package.json index cc8393286..860423ee7 100644 --- a/exploded-diagrams/package.json +++ b/exploded-diagrams/package.json @@ -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" } diff --git a/exploded-diagrams/public/packages/automotive/ford/fiesta/gearbox/parts/clutch-disc.json b/exploded-diagrams/public/packages/automotive/ford/fiesta/gearbox/parts/clutch-disc.json index f90d00de4..d209cd3be 100644 --- a/exploded-diagrams/public/packages/automotive/ford/fiesta/gearbox/parts/clutch-disc.json +++ b/exploded-diagrams/public/packages/automotive/ford/fiesta/gearbox/parts/clutch-disc.json @@ -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 } ] } diff --git a/exploded-diagrams/public/packages/automotive/ford/fiesta/gearbox/parts/flywheel.json b/exploded-diagrams/public/packages/automotive/ford/fiesta/gearbox/parts/flywheel.json index cb254e1c0..ef24318a9 100644 --- a/exploded-diagrams/public/packages/automotive/ford/fiesta/gearbox/parts/flywheel.json +++ b/exploded-diagrams/public/packages/automotive/ford/fiesta/gearbox/parts/flywheel.json @@ -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" } ] } diff --git a/exploded-diagrams/public/packages/automotive/ford/fiesta/gearbox/parts/pressure-plate.json b/exploded-diagrams/public/packages/automotive/ford/fiesta/gearbox/parts/pressure-plate.json index 38ab6499b..63a93ac25 100644 --- a/exploded-diagrams/public/packages/automotive/ford/fiesta/gearbox/parts/pressure-plate.json +++ b/exploded-diagrams/public/packages/automotive/ford/fiesta/gearbox/parts/pressure-plate.json @@ -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" } ] } diff --git a/exploded-diagrams/public/packages/automotive/ford/fiesta/gearbox/parts/release-bearing.json b/exploded-diagrams/public/packages/automotive/ford/fiesta/gearbox/parts/release-bearing.json index e76362c00..e627ad56e 100644 --- a/exploded-diagrams/public/packages/automotive/ford/fiesta/gearbox/parts/release-bearing.json +++ b/exploded-diagrams/public/packages/automotive/ford/fiesta/gearbox/parts/release-bearing.json @@ -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" } ] } diff --git a/exploded-diagrams/src/app/[category]/[manufacturer]/[product]/[assembly]/page.tsx b/exploded-diagrams/src/app/[category]/[manufacturer]/[product]/[assembly]/page.tsx index d8b24af9c..a7bd8fbb6 100644 --- a/exploded-diagrams/src/app/[category]/[manufacturer]/[product]/[assembly]/page.tsx +++ b/exploded-diagrams/src/app/[category]/[manufacturer]/[product]/[assembly]/page.tsx @@ -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(null) + const [activeTab, setActiveTab] = useState<'exploded' | '3d'>('exploded') const [explosion, setExplosion] = useState(50) const [rotation, setRotation] = useState(0) const [highlightedPart, setHighlightedPart] = useState(null) @@ -126,39 +129,46 @@ export default function AssemblyPage() { return ( <> + - - -
-
- + -
- -
+
+
+ +
- + +
+ + + + ) : ( + + )} ) } diff --git a/exploded-diagrams/src/app/globals.scss b/exploded-diagrams/src/app/globals.scss index ae2dded50..8d18346d3 100644 --- a/exploded-diagrams/src/app/globals.scss +++ b/exploded-diagrams/src/app/globals.scss @@ -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; +} diff --git a/exploded-diagrams/src/components/AssemblyTabs.tsx b/exploded-diagrams/src/components/AssemblyTabs.tsx new file mode 100644 index 000000000..72b207ff7 --- /dev/null +++ b/exploded-diagrams/src/components/AssemblyTabs.tsx @@ -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 ( +
+ + +
+ ) +} diff --git a/exploded-diagrams/src/components/PartSelector.tsx b/exploded-diagrams/src/components/PartSelector.tsx new file mode 100644 index 000000000..e327b3aaf --- /dev/null +++ b/exploded-diagrams/src/components/PartSelector.tsx @@ -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 ( +
+
+ {parts.map(part => ( +
onSelectPart(part.id)} + > +
{part.name}
+
{part.partNumber}
+
+ ))} +
+
+ ) +} diff --git a/exploded-diagrams/src/components/PartViewer3D.tsx b/exploded-diagrams/src/components/PartViewer3D.tsx new file mode 100644 index 000000000..d5d3de5d0 --- /dev/null +++ b/exploded-diagrams/src/components/PartViewer3D.tsx @@ -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 ( + + + + ) +} + +export default function PartViewer3D({ parts, materials }: PartViewer3DProps) { + const [selectedPartId, setSelectedPartId] = useState( + 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 ( +
+
+ + {selectedPart && ( + + )} + +
+ +
+ ) +} diff --git a/exploded-diagrams/src/components/ThreeCanvas.tsx b/exploded-diagrams/src/components/ThreeCanvas.tsx new file mode 100644 index 000000000..d0e40e6db --- /dev/null +++ b/exploded-diagrams/src/components/ThreeCanvas.tsx @@ -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 ( + + + + {/* Lighting */} + + + + {/* Ground plane grid */} + + + {/* Camera controls */} + + + {/* User content */} + {children} + + ) +} diff --git a/exploded-diagrams/src/lib/jscad-to-three.ts b/exploded-diagrams/src/lib/jscad-to-three.ts new file mode 100644 index 000000000..40cc2d23a --- /dev/null +++ b/exploded-diagrams/src/lib/jscad-to-three.ts @@ -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) +} diff --git a/exploded-diagrams/src/lib/types.ts b/exploded-diagrams/src/lib/types.ts index d7003d67a..6df192f8e 100644 --- a/exploded-diagrams/src/lib/types.ts +++ b/exploded-diagrams/src/lib/types.ts @@ -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 }