初始化卷积测试

This commit is contained in:
bql
2026-05-26 10:14:12 +08:00
commit 36c82f953e
16 changed files with 4519 additions and 0 deletions

12
index.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>深度图3D模型修复与刀路生成</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

2584
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "depth-map-3d-repair",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"three": "^0.160.0",
"@react-three/fiber": "^8.15.0",
"@react-three/drei": "^9.92.0",
"clipper2-wasm": "^0.1.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.2.0",
"vite": "^5.0.0"
}
}

24
src/App.css Normal file
View File

@@ -0,0 +1,24 @@
.app {
display: flex;
width: 100vw;
height: 100vh;
background: #1a1a2e;
}
.left-panel {
width: 50%;
display: flex;
flex-direction: column;
border-right: 2px solid #333;
}
.right-panel {
width: 50%;
display: flex;
flex-direction: column;
}
.viewer-container {
flex: 1;
position: relative;
}

80
src/App.jsx Normal file
View File

@@ -0,0 +1,80 @@
import React, { useState, useCallback } from 'react'
import ModelViewer from './components/ModelViewer'
import EnvelopeViewer from './components/EnvelopeViewer'
import ControlPanel from './components/ControlPanel'
import './App.css'
const MATERIAL_SIZE = 100 // mm
function App() {
const [model, setModel] = useState(null)
const [upperEnvelope, setUpperEnvelope] = useState(null)
const [convolutionEnvelope, setConvolutionEnvelope] = useState(null)
const [repairedEnvelope, setRepairedEnvelope] = useState(null)
const [toolpathEnvelope, setToolpathEnvelope] = useState(null)
const [showUpper, setShowUpper] = useState(false)
const [showConvolution, setShowConvolution] = useState(false)
const [showRepaired, setShowRepaired] = useState(false)
const [showToolpath, setShowToolpath] = useState(false)
const handleModelImport = useCallback((loadedModel) => {
setModel(loadedModel)
setUpperEnvelope(null)
setConvolutionEnvelope(null)
setRepairedEnvelope(null)
setToolpathEnvelope(null)
setShowUpper(false)
setShowConvolution(false)
setShowRepaired(false)
setShowToolpath(false)
}, [])
const handleGenerateUpperEnvelope = useCallback((envelope) => {
setUpperEnvelope(envelope)
setShowUpper(true)
}, [])
const handleRepairAndGenerate = useCallback((convolution, toolpath, repaired) => {
setConvolutionEnvelope(convolution)
setRepairedEnvelope(repaired)
setToolpathEnvelope(toolpath)
setShowConvolution(true)
setShowRepaired(true)
setShowToolpath(true)
}, [])
return (
<div className="app">
<div className="left-panel">
<ControlPanel
model={model}
upperEnvelope={upperEnvelope}
onModelImport={handleModelImport}
onGenerateUpper={handleGenerateUpperEnvelope}
onRepairAndGenerate={handleRepairAndGenerate}
/>
<div className="viewer-container">
<ModelViewer model={model} />
</div>
</div>
<div className="right-panel">
<EnvelopeViewer
upperEnvelope={upperEnvelope}
convolutionEnvelope={convolutionEnvelope}
repairedEnvelope={repairedEnvelope}
toolpathEnvelope={toolpathEnvelope}
showUpper={showUpper}
showConvolution={showConvolution}
showRepaired={showRepaired}
showToolpath={showToolpath}
onToggleUpper={() => setShowUpper(!showUpper)}
onToggleConvolution={() => setShowConvolution(!showConvolution)}
onToggleRepaired={() => setShowRepaired(!showRepaired)}
onToggleToolpath={() => setShowToolpath(!showToolpath)}
/>
</div>
</div>
)
}
export default App

View File

@@ -0,0 +1,150 @@
.control-panel {
padding: 16px;
background: #16213e;
border-bottom: 2px solid #333;
max-height: 40vh;
overflow-y: auto;
}
.panel-header h2 {
color: #e94560;
margin-bottom: 16px;
font-size: 18px;
}
.control-section {
margin-bottom: 16px;
padding: 12px;
background: #1a1a2e;
border-radius: 8px;
}
.control-section h3 {
color: #fff;
font-size: 14px;
margin-bottom: 8px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-right: 8px;
margin-bottom: 8px;
transition: opacity 0.2s;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
background: #0f3460;
color: #fff;
}
.btn-primary:hover:not(:disabled) {
background: #1a4a7a;
}
.btn-success {
background: #16c79a;
color: #fff;
}
.btn-success:hover:not(:disabled) {
background: #19a87f;
}
.btn-warning {
background: #f5a623;
color: #fff;
}
.btn-warning:hover:not(:disabled) {
background: #d4891a;
}
.info-text {
color: #aaa;
font-size: 12px;
margin-top: 8px;
}
.compute-mode-selector {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.compute-mode-selector .toggle-btn {
padding: 6px 14px;
border: 2px solid #444;
background: #1a1a2e;
color: #aaa;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.compute-mode-selector .toggle-btn.active {
border-color: #4a90d9;
background: #1a4a7a;
color: #fff;
}
.compute-mode-selector .toggle-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.tool-selector {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 12px;
}
.tool-selector label {
color: #fff;
font-size: 13px;
cursor: pointer;
}
.tool-selector input {
margin-right: 8px;
}
.progress-container {
margin-top: 12px;
display: flex;
align-items: center;
gap: 12px;
}
.progress-bar {
flex: 1;
height: 8px;
background: #333;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #f5a623, #16c79a);
border-radius: 4px;
transition: width 0.1s ease;
}
.progress-text {
color: #aaa;
font-size: 12px;
white-space: nowrap;
min-width: 100px;
text-align: right;
}

View File

@@ -0,0 +1,454 @@
import React, { useRef, useState, useEffect } from 'react'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
import * as THREE from 'three'
import { runWebGPUConvolution } from '../utils/webgpuConvolution'
import './ControlPanel.css'
const MATERIAL_SIZE = 100
function ControlPanel({ model, upperEnvelope, onModelImport, onGenerateUpper, onRepairAndGenerate }) {
const fileInputRef = useRef(null)
const workerRef = useRef(null)
const selectedToolRef = useRef('ball')
const [isGenerating, setIsGenerating] = useState(false)
const [isRepairing, setIsRepairing] = useState(false)
const [repairProgress, setRepairProgress] = useState(0)
const [repairIteration, setRepairIteration] = useState(0)
const [selectedTool, setSelectedTool] = useState('ball')
const [computeMode, setComputeMode] = useState('worker')
const [webgpuAvailable, setWebgpuAvailable] = useState(false)
useEffect(() => {
if (navigator.gpu) {
navigator.gpu.requestAdapter().then(adapter => {
setWebgpuAvailable(!!adapter)
}).catch(() => {
setWebgpuAvailable(false)
})
}
}, [])
useEffect(() => {
selectedToolRef.current = selectedTool
}, [selectedTool])
useEffect(() => {
workerRef.current = new Worker(
new URL('../workers/repairWorker.js', import.meta.url),
{ type: 'module' }
)
workerRef.current.onmessage = (e) => {
if (e.data.type === 'progress') {
setRepairProgress(e.data.progress)
setRepairIteration(e.data.iteration)
} else if (e.data.type === 'complete') {
const { convolutionMap, toolpathMap, repairedMap, iterations } = e.data
const convolutionPoints = []
const toolpathPoints = []
const repairedPoints = []
const halfSize = MATERIAL_SIZE / 2
const resolution = 0.1
const gridSize = Math.ceil(MATERIAL_SIZE / resolution)
const gridOffset = halfSize
for (let gy = 0; gy < gridSize; gy++) {
for (let gx = 0; gx < gridSize; gx++) {
const idx = gy * gridSize + gx
const cz = convolutionMap[idx]
const tz = toolpathMap[idx]
const rz = repairedMap[idx]
if (cz !== -Infinity) {
const x = gx * resolution - gridOffset
const y = gy * resolution - gridOffset
convolutionPoints.push({ x, y, z: cz })
}
if (tz !== -Infinity) {
const x = gx * resolution - gridOffset
const y = gy * resolution - gridOffset
toolpathPoints.push({ x, y, z: tz })
}
if (rz !== -Infinity) {
const x = gx * resolution - gridOffset
const y = gy * resolution - gridOffset
repairedPoints.push({ x, y, z: rz })
}
}
}
onRepairAndGenerate(
{
depthMap: new Float32Array(convolutionMap),
gridSize,
resolution,
points: convolutionPoints
},
{
depthMap: new Float32Array(toolpathMap),
gridSize,
resolution,
points: toolpathPoints
},
{
depthMap: new Float32Array(repairedMap),
gridSize,
resolution,
points: repairedPoints
}
)
setIsRepairing(false)
setRepairProgress(100)
console.log(`修复完成,迭代次数: ${iterations}`)
}
}
return () => {
if (workerRef.current) {
workerRef.current.terminate()
}
}
}, [onRepairAndGenerate])
const handleFileSelect = async (e) => {
const file = e.target.files[0]
if (!file) return
const extension = file.name.split('.').pop().toLowerCase()
const url = URL.createObjectURL(file)
try {
let geometry = null
if (extension === 'stl') {
const loader = new STLLoader()
geometry = await new Promise((resolve, reject) => {
loader.load(url, resolve, undefined, reject)
})
} else if (extension === 'obj') {
const loader = new OBJLoader()
const obj = await new Promise((resolve, reject) => {
loader.load(url, resolve, undefined, reject)
})
obj.traverse((child) => {
if (child.isMesh) {
geometry = child.geometry.clone()
}
})
} else if (extension === 'gltf' || extension === 'glb') {
const loader = new GLTFLoader()
const gltf = await new Promise((resolve, reject) => {
loader.load(url, resolve, undefined, reject)
})
gltf.scene.traverse((child) => {
if (child.isMesh) {
geometry = child.geometry.clone()
}
})
} else {
alert('不支持的文件格式,请导入 STL、OBJ 或 GLTF/GLB 文件')
return
}
if (!geometry) {
alert('无法加载模型几何体')
return
}
geometry.computeBoundingBox()
const bbox = geometry.boundingBox
const size = new THREE.Vector3()
bbox.getSize(size)
const center = new THREE.Vector3()
bbox.getCenter(center)
const maxDim = Math.max(size.x, size.y, size.z)
let scale = 1
if (maxDim > MATERIAL_SIZE) {
scale = MATERIAL_SIZE / maxDim
}
geometry.scale(scale, scale, scale)
geometry.translate(-center.x * scale, -center.y * scale, -center.z * scale)
geometry.computeBoundingBox()
onModelImport({
geometry,
name: file.name,
bbox: geometry.boundingBox.clone()
})
} catch (error) {
console.error('加载模型失败:', error)
alert('加载模型失败: ' + error.message)
} finally {
URL.revokeObjectURL(url)
}
e.target.value = ''
}
const generateUpperEnvelope = async () => {
if (!model) return
setIsGenerating(true)
try {
await new Promise(resolve => setTimeout(resolve, 50))
const geometry = model.geometry
const resolution = 0.1
const gridSize = Math.ceil(MATERIAL_SIZE / resolution)
const depthMap = new Float32Array(gridSize * gridSize).fill(-Infinity)
const vertices = geometry.attributes.position.array
const bbox = geometry.boundingBox
const halfSize = MATERIAL_SIZE / 2
const gridOffset = halfSize
for (let i = 0; i < vertices.length; i += 3) {
const x = vertices[i]
const y = vertices[i + 1]
const z = vertices[i + 2]
const gridX = Math.floor((x + gridOffset) / resolution)
const gridY = Math.floor((y + gridOffset) / resolution)
if (gridX >= 0 && gridX < gridSize && gridY >= 0 && gridY < gridSize) {
const idx = gridY * gridSize + gridX
if (z > depthMap[idx]) {
depthMap[idx] = z
}
}
}
console.log('depthMap 采样完成, gridSize:', gridSize, '有效像素:', depthMap.filter(z => z !== -Infinity).length)
const surfacePoints = []
for (let gy = 0; gy < gridSize; gy++) {
for (let gx = 0; gx < gridSize; gx++) {
const idx = gy * gridSize + gx
const z = depthMap[idx]
if (z !== -Infinity) {
const x = gx * resolution - gridOffset
const y = gy * resolution - gridOffset
surfacePoints.push({ x, y, z })
}
}
}
onGenerateUpper({
depthMap,
gridSize,
resolution,
points: surfacePoints,
bbox: bbox.clone()
})
} catch (error) {
console.error('生成上包络面失败:', error)
alert('生成上包络面失败: ' + error.message)
} finally {
setIsGenerating(false)
}
}
const repairAndGenerate = async () => {
if (!upperEnvelope) return
setIsRepairing(true)
setRepairProgress(0)
setRepairIteration(0)
let toolRadius = 0
let toolAngle = 0
let tipRadius = 0
if (selectedTool === 'ball') {
toolRadius = 3.175 / 2
toolAngle = 0
tipRadius = 0
} else if (selectedTool === 'taper15') {
toolRadius = 3.175 / 2
toolAngle = 15
tipRadius = 0.05
} else if (selectedTool === 'taper30') {
toolRadius = 3.175 / 2
toolAngle = 30
tipRadius = 0.05
}
if (computeMode === 'webgpu' && webgpuAvailable) {
try {
const result = await runWebGPUConvolution(
upperEnvelope.depthMap,
upperEnvelope.gridSize,
upperEnvelope.resolution,
selectedTool,
toolRadius,
toolAngle,
tipRadius,
(msg) => {
if (msg.type === 'progress') {
setRepairProgress(msg.progress)
setRepairIteration(msg.iteration)
}
}
)
const { convolutionMap, toolpathMap, repairedMap } = result
const { gridSize, resolution } = upperEnvelope
const surfacePoints = (map) => {
const points = []
for (let gy = 0; gy < gridSize; gy++) {
for (let gx = 0; gx < gridSize; gx++) {
const idx = gy * gridSize + gx
const z = map[idx]
if (z !== -Infinity) {
const x = gx * resolution - MATERIAL_SIZE / 2
const y = gy * resolution - MATERIAL_SIZE / 2
points.push({ x, y, z })
}
}
}
return points
}
onRepairAndGenerate(
{ depthMap: new Float32Array(convolutionMap), gridSize, resolution, points: surfacePoints(convolutionMap) },
{ depthMap: new Float32Array(toolpathMap), gridSize, resolution, points: surfacePoints(toolpathMap) },
{ depthMap: new Float32Array(repairedMap), gridSize, resolution, points: surfacePoints(repairedMap) }
)
} catch (error) {
console.error('WebGPU 计算失败:', error)
alert('WebGPU 计算失败: ' + error.message)
} finally {
setIsRepairing(false)
}
} else {
workerRef.current.postMessage({
depthMap: upperEnvelope.depthMap,
gridSize: upperEnvelope.gridSize,
resolution: upperEnvelope.resolution,
toolType: selectedTool,
toolRadius,
toolAngle,
tipRadius
})
}
}
return (
<div className="control-panel">
<div className="panel-header">
<h2>控制面板</h2>
</div>
<div className="control-section">
<h3>1. 导入模型</h3>
<button
className="btn btn-primary"
onClick={() => fileInputRef.current?.click()}
>
选择模型文件
</button>
<input
ref={fileInputRef}
type="file"
accept=".stl,.obj,.gltf,.glb"
onChange={handleFileSelect}
style={{ display: 'none' }}
/>
{model && <p className="info-text">已加载: {model.name}</p>}
</div>
<div className="control-section">
<h3>2. 生成上包络面</h3>
<button
className="btn btn-success"
onClick={generateUpperEnvelope}
disabled={!model || isGenerating}
>
{isGenerating ? '生成中...' : '生成上包络面'}
</button>
{upperEnvelope && <p className="info-text">上包络面已生成</p>}
</div>
<div className="control-section">
<h3>3. 修复并生成刀路</h3>
<div className="compute-mode-selector">
<button
className={`toggle-btn ${computeMode === 'worker' ? 'active' : ''}`}
onClick={() => setComputeMode('worker')}
>
Web Worker
</button>
<button
className={`toggle-btn ${computeMode === 'webgpu' ? 'active' : ''}`}
onClick={() => setComputeMode('webgpu')}
disabled={!webgpuAvailable}
>
WebGPU {webgpuAvailable ? '' : '(不可用)'}
</button>
</div>
<div className="tool-selector">
<label>
<input
type="radio"
name="tool"
value="ball"
checked={selectedTool === 'ball'}
onChange={() => setSelectedTool('ball')}
/>
1/8" 球刀
</label>
<label>
<input
type="radio"
name="tool"
value="taper15"
checked={selectedTool === 'taper15'}
onChange={() => setSelectedTool('taper15')}
/>
1/8" 15° 尖刀 (0.1mm)
</label>
<label>
<input
type="radio"
name="tool"
value="taper30"
checked={selectedTool === 'taper30'}
onChange={() => setSelectedTool('taper30')}
/>
1/8" 30° 尖刀 (0.1mm)
</label>
</div>
<button
className="btn btn-warning"
onClick={repairAndGenerate}
disabled={!upperEnvelope || isRepairing}
>
{isRepairing ? '修复中...' : '修复并生成刀路'}
</button>
{isRepairing && (
<div className="progress-container">
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${repairProgress}%` }}
/>
</div>
<span className="progress-text">
迭代 {repairIteration} | {repairProgress.toFixed(1)}%
</span>
</div>
)}
</div>
</div>
)
}
export default ControlPanel

View File

@@ -0,0 +1,119 @@
.envelope-viewer {
display: flex;
flex-direction: column;
height: 100%;
position: relative;
}
.viewer-controls {
display: flex;
gap: 8px;
padding: 12px;
background: #16213e;
border-bottom: 2px solid #333;
}
.toggle-btn {
padding: 8px 16px;
border: 2px solid #444;
background: #1a1a2e;
color: #aaa;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.toggle-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.toggle-btn:not(:disabled):hover {
border-color: #666;
color: #fff;
}
.toggle-btn.active {
border-color: #16c79a;
color: #16c79a;
background: rgba(22, 199, 154, 0.1);
}
.render-mode-switch {
display: flex;
align-items: center;
gap: 8px;
}
.switch-label {
color: #666;
font-size: 13px;
transition: color 0.2s;
}
.switch-label.active {
color: #16c79a;
}
.switch {
position: relative;
display: inline-block;
width: 44px;
height: 22px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #333;
transition: 0.3s;
border-radius: 22px;
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 3px;
bottom: 3px;
background-color: #aaa;
transition: 0.3s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #16c79a;
}
input:checked + .slider:before {
transform: translateX(22px);
background-color: #fff;
}
.viewer-container {
flex: 1;
position: relative;
}
.placeholder {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #666;
font-size: 16px;
text-align: center;
pointer-events: none;
}

View File

@@ -0,0 +1,428 @@
import React, { useRef, useEffect, useState } from 'react'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import './EnvelopeViewer.css'
const MATERIAL_SIZE = 100
function EnvelopeViewer({
upperEnvelope,
convolutionEnvelope,
repairedEnvelope,
toolpathEnvelope,
showUpper,
showConvolution,
showRepaired,
showToolpath,
onToggleUpper,
onToggleConvolution,
onToggleRepaired,
onToggleToolpath
}) {
const [renderMode, setRenderMode] = useState('points')
const containerRef = useRef(null)
const sceneRef = useRef(null)
const cameraRef = useRef(null)
const rendererRef = useRef(null)
const controlsRef = useRef(null)
const upperMeshRef = useRef(null)
const upperPointsRef = useRef(null)
const convolutionMeshRef = useRef(null)
const convolutionPointsRef = useRef(null)
const repairedMeshRef = useRef(null)
const repairedPointsRef = useRef(null)
const toolpathMeshRef = useRef(null)
const toolpathPointsRef = useRef(null)
useEffect(() => {
if (!containerRef.current) return
const scene = new THREE.Scene()
scene.background = new THREE.Color(0x1a1a2e)
sceneRef.current = scene
const camera = new THREE.PerspectiveCamera(
50,
containerRef.current.clientWidth / containerRef.current.clientHeight,
0.1,
1000
)
camera.position.set(80, -80, 80)
camera.lookAt(0, 0, 0)
cameraRef.current = camera
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(containerRef.current.clientWidth, containerRef.current.clientHeight)
renderer.setPixelRatio(window.devicePixelRatio)
containerRef.current.appendChild(renderer.domElement)
rendererRef.current = renderer
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.05
controls.target.set(0, 0, 0)
controlsRef.current = controls
const halfSize = MATERIAL_SIZE / 2
const gridHelper = new THREE.GridHelper(MATERIAL_SIZE, 10, 0x444444, 0x222222)
gridHelper.rotation.x = Math.PI / 2
scene.add(gridHelper)
const boxGeometry = new THREE.BoxGeometry(MATERIAL_SIZE, MATERIAL_SIZE, MATERIAL_SIZE)
const edges = new THREE.EdgesGeometry(boxGeometry)
const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0x444444 }))
scene.add(line)
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6)
scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8)
directionalLight.position.set(50, 100, 50)
scene.add(directionalLight)
const directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.4)
directionalLight2.position.set(-50, 50, -50)
scene.add(directionalLight2)
const axesHelper = new THREE.AxesHelper(60)
scene.add(axesHelper)
const animate = () => {
requestAnimationFrame(animate)
controls.update()
renderer.render(scene, camera)
}
animate()
const handleResize = () => {
if (!containerRef.current || !camera || !renderer) return
camera.aspect = containerRef.current.clientWidth / containerRef.current.clientHeight
camera.updateProjectionMatrix()
renderer.setSize(containerRef.current.clientWidth, containerRef.current.clientHeight)
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
if (containerRef.current && renderer.domElement) {
containerRef.current.removeChild(renderer.domElement)
}
renderer.dispose()
}
}, [])
const createSurfaceMesh = (envelope, color, zOffset = 0) => {
if (!envelope || !envelope.depthMap || envelope.depthMap.length === 0) return null
const { gridSize, resolution, depthMap } = envelope
const halfSize = MATERIAL_SIZE / 2
let validCount = 0
const totalPixels = gridSize * gridSize
for (let i = 0; i < totalPixels; i++) {
if (depthMap[i] !== -Infinity) validCount++
}
const density = validCount / totalPixels
console.log('createSurfaceMesh: density =', density.toFixed(4), 'validCount =', validCount)
const geometry = new THREE.BufferGeometry()
const vertices = []
const indices = []
for (let gy = 0; gy < gridSize - 1; gy++) {
for (let gx = 0; gx < gridSize - 1; gx++) {
const idx1 = gy * gridSize + gx
const idx2 = gy * gridSize + (gx + 1)
const idx3 = (gy + 1) * gridSize + gx
const idx4 = (gy + 1) * gridSize + (gx + 1)
const z1 = depthMap[idx1]
const z2 = depthMap[idx2]
const z3 = depthMap[idx3]
const z4 = depthMap[idx4]
if (z1 === -Infinity || z2 === -Infinity || z3 === -Infinity || z4 === -Infinity) continue
const x1 = gx * resolution - halfSize
const y1 = gy * resolution - halfSize
const x2 = (gx + 1) * resolution - halfSize
const y2 = (gy + 1) * resolution - halfSize
const idx = vertices.length / 3
vertices.push(x1, y1, z1 + zOffset)
vertices.push(x2, y1, z2 + zOffset)
vertices.push(x1, y2, z3 + zOffset)
vertices.push(x2, y2, z4 + zOffset)
indices.push(idx, idx + 2, idx + 1)
indices.push(idx + 1, idx + 2, idx + 3)
}
}
if (vertices.length === 0) return null
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3))
geometry.setIndex(indices)
geometry.computeVertexNormals()
const material = new THREE.MeshPhongMaterial({
color,
specular: 0x111111,
shininess: 30,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.75,
polygonOffset: true,
polygonOffsetFactor: 1,
polygonOffsetUnits: 1
})
return new THREE.Mesh(geometry, material)
}
const createPointCloud = (envelope, color, zOffset = 0) => {
if (!envelope || !envelope.depthMap || envelope.depthMap.length === 0) return null
const { gridSize, resolution, depthMap } = envelope
const halfSize = MATERIAL_SIZE / 2
const positions = []
const colors = []
const c = new THREE.Color(color)
for (let gy = 0; gy < gridSize; gy++) {
for (let gx = 0; gx < gridSize; gx++) {
const idx = gy * gridSize + gx
const z = depthMap[idx]
if (z === -Infinity) continue
const x = gx * resolution - halfSize
const y = gy * resolution - halfSize
positions.push(x, y, z + zOffset)
colors.push(c.r, c.g, c.b)
}
}
if (positions.length === 0) return null
console.log('PointCloud 生成完成, 点数:', positions.length / 3)
const geometry = new THREE.BufferGeometry()
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3))
const material = new THREE.PointsMaterial({
size: resolution * 0.8,
vertexColors: true,
transparent: true,
opacity: 0.8,
sizeAttenuation: true
})
return new THREE.Points(geometry, material)
}
useEffect(() => {
if (!sceneRef.current) return
if (upperMeshRef.current) {
sceneRef.current.remove(upperMeshRef.current)
upperMeshRef.current.geometry.dispose()
upperMeshRef.current.material.dispose()
upperMeshRef.current = null
}
if (upperPointsRef.current) {
sceneRef.current.remove(upperPointsRef.current)
upperPointsRef.current.geometry.dispose()
upperPointsRef.current.material.dispose()
upperPointsRef.current = null
}
if (showUpper && upperEnvelope) {
const mesh = createSurfaceMesh(upperEnvelope, 0x16c79a, 0)
if (mesh) {
mesh.visible = renderMode === 'mesh'
sceneRef.current.add(mesh)
upperMeshRef.current = mesh
}
const points = createPointCloud(upperEnvelope, 0x16c79a, 0)
if (points) {
points.visible = renderMode === 'points'
sceneRef.current.add(points)
upperPointsRef.current = points
}
}
}, [showUpper, upperEnvelope])
useEffect(() => {
if (upperMeshRef.current) upperMeshRef.current.visible = renderMode === 'mesh'
if (upperPointsRef.current) upperPointsRef.current.visible = renderMode === 'points'
}, [renderMode])
useEffect(() => {
if (!sceneRef.current) return
if (convolutionMeshRef.current) {
sceneRef.current.remove(convolutionMeshRef.current)
convolutionMeshRef.current.geometry.dispose()
convolutionMeshRef.current.material.dispose()
convolutionMeshRef.current = null
}
if (convolutionPointsRef.current) {
sceneRef.current.remove(convolutionPointsRef.current)
convolutionPointsRef.current.geometry.dispose()
convolutionPointsRef.current.material.dispose()
convolutionPointsRef.current = null
}
if (showConvolution && convolutionEnvelope) {
const mesh = createSurfaceMesh(convolutionEnvelope, 0x4a90d9, 0)
if (mesh) {
mesh.visible = renderMode === 'mesh'
sceneRef.current.add(mesh)
convolutionMeshRef.current = mesh
}
const points = createPointCloud(convolutionEnvelope, 0x4a90d9, 0)
if (points) {
points.visible = renderMode === 'points'
sceneRef.current.add(points)
convolutionPointsRef.current = points
}
}
}, [showConvolution, convolutionEnvelope])
useEffect(() => {
if (convolutionMeshRef.current) convolutionMeshRef.current.visible = renderMode === 'mesh'
if (convolutionPointsRef.current) convolutionPointsRef.current.visible = renderMode === 'points'
}, [renderMode])
useEffect(() => {
if (!sceneRef.current) return
if (repairedMeshRef.current) {
sceneRef.current.remove(repairedMeshRef.current)
repairedMeshRef.current.geometry.dispose()
repairedMeshRef.current.material.dispose()
repairedMeshRef.current = null
}
if (repairedPointsRef.current) {
sceneRef.current.remove(repairedPointsRef.current)
repairedPointsRef.current.geometry.dispose()
repairedPointsRef.current.material.dispose()
repairedPointsRef.current = null
}
if (showRepaired && repairedEnvelope) {
const mesh = createSurfaceMesh(repairedEnvelope, 0xf5a623, 0)
if (mesh) {
mesh.visible = renderMode === 'mesh'
sceneRef.current.add(mesh)
repairedMeshRef.current = mesh
}
const points = createPointCloud(repairedEnvelope, 0xf5a623, 0)
if (points) {
points.visible = renderMode === 'points'
sceneRef.current.add(points)
repairedPointsRef.current = points
}
}
}, [showRepaired, repairedEnvelope])
useEffect(() => {
if (repairedMeshRef.current) repairedMeshRef.current.visible = renderMode === 'mesh'
if (repairedPointsRef.current) repairedPointsRef.current.visible = renderMode === 'points'
}, [renderMode])
useEffect(() => {
if (!sceneRef.current) return
if (toolpathMeshRef.current) {
sceneRef.current.remove(toolpathMeshRef.current)
toolpathMeshRef.current.geometry.dispose()
toolpathMeshRef.current.material.dispose()
toolpathMeshRef.current = null
}
if (toolpathPointsRef.current) {
sceneRef.current.remove(toolpathPointsRef.current)
toolpathPointsRef.current.geometry.dispose()
toolpathPointsRef.current.material.dispose()
toolpathPointsRef.current = null
}
if (showToolpath && toolpathEnvelope) {
const mesh = createSurfaceMesh(toolpathEnvelope, 0xe94560, 0)
if (mesh) {
mesh.visible = renderMode === 'mesh'
sceneRef.current.add(mesh)
toolpathMeshRef.current = mesh
}
const points = createPointCloud(toolpathEnvelope, 0xe94560, 0)
if (points) {
points.visible = renderMode === 'points'
sceneRef.current.add(points)
toolpathPointsRef.current = points
}
}
}, [showToolpath, toolpathEnvelope])
useEffect(() => {
if (toolpathMeshRef.current) toolpathMeshRef.current.visible = renderMode === 'mesh'
if (toolpathPointsRef.current) toolpathPointsRef.current.visible = renderMode === 'points'
}, [renderMode])
return (
<div className="envelope-viewer">
<div className="viewer-controls">
<div className="render-mode-switch">
<span className={`switch-label ${renderMode === 'points' ? 'active' : ''}`}>点云</span>
<label className="switch">
<input
type="checkbox"
checked={renderMode === 'mesh'}
onChange={() => setRenderMode(renderMode === 'points' ? 'mesh' : 'points')}
/>
<span className="slider"></span>
</label>
<span className={`switch-label ${renderMode === 'mesh' ? 'active' : ''}`}>网格</span>
</div>
<button
className={`toggle-btn ${showUpper ? 'active' : ''}`}
onClick={onToggleUpper}
disabled={!upperEnvelope}
>
上包络面
</button>
<button
className={`toggle-btn ${showConvolution ? 'active' : ''}`}
onClick={onToggleConvolution}
disabled={!convolutionEnvelope}
>
卷积包络面
</button>
<button
className={`toggle-btn ${showRepaired ? 'active' : ''}`}
onClick={onToggleRepaired}
disabled={!repairedEnvelope}
>
修复包络面
</button>
<button
className={`toggle-btn ${showToolpath ? 'active' : ''}`}
onClick={onToggleToolpath}
disabled={!toolpathEnvelope}
>
刀路包络面
</button>
</div>
<div className="viewer-container" ref={containerRef}>
{!upperEnvelope && !convolutionEnvelope && !repairedEnvelope && !toolpathEnvelope && (
<div className="placeholder">请先导入模型并生成包络面</div>
)}
</div>
</div>
)
}
export default EnvelopeViewer

View File

@@ -0,0 +1,16 @@
.model-viewer {
width: 100%;
height: 100%;
position: relative;
}
.placeholder {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #666;
font-size: 16px;
text-align: center;
pointer-events: none;
}

View File

@@ -0,0 +1,128 @@
import React, { useRef, useEffect, useState } from 'react'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import './ModelViewer.css'
const MATERIAL_SIZE = 100
function ModelViewer({ model }) {
const containerRef = useRef(null)
const sceneRef = useRef(null)
const cameraRef = useRef(null)
const rendererRef = useRef(null)
const controlsRef = useRef(null)
const modelMeshRef = useRef(null)
useEffect(() => {
if (!containerRef.current) return
const scene = new THREE.Scene()
scene.background = new THREE.Color(0x1a1a2e)
sceneRef.current = scene
const camera = new THREE.PerspectiveCamera(
50,
containerRef.current.clientWidth / containerRef.current.clientHeight,
0.1,
1000
)
camera.position.set(80, -80, 80)
camera.lookAt(0, 0, 0)
cameraRef.current = camera
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(containerRef.current.clientWidth, containerRef.current.clientHeight)
renderer.setPixelRatio(window.devicePixelRatio)
containerRef.current.appendChild(renderer.domElement)
rendererRef.current = renderer
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.05
controls.target.set(0, 0, 0)
controlsRef.current = controls
const halfSize = MATERIAL_SIZE / 2
const gridHelper = new THREE.GridHelper(MATERIAL_SIZE, 10, 0x444444, 0x222222)
gridHelper.rotation.x = Math.PI / 2
scene.add(gridHelper)
const boxGeometry = new THREE.BoxGeometry(MATERIAL_SIZE, MATERIAL_SIZE, MATERIAL_SIZE)
const edges = new THREE.EdgesGeometry(boxGeometry)
const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0x444444 }))
line.position.set(0, 0, 0)
scene.add(line)
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6)
scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8)
directionalLight.position.set(50, 100, 50)
scene.add(directionalLight)
const directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.4)
directionalLight2.position.set(-50, 50, -50)
scene.add(directionalLight2)
const axesHelper = new THREE.AxesHelper(60)
scene.add(axesHelper)
const animate = () => {
requestAnimationFrame(animate)
controls.update()
renderer.render(scene, camera)
}
animate()
const handleResize = () => {
if (!containerRef.current || !camera || !renderer) return
camera.aspect = containerRef.current.clientWidth / containerRef.current.clientHeight
camera.updateProjectionMatrix()
renderer.setSize(containerRef.current.clientWidth, containerRef.current.clientHeight)
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
if (containerRef.current && renderer.domElement) {
containerRef.current.removeChild(renderer.domElement)
}
renderer.dispose()
}
}, [])
useEffect(() => {
if (!sceneRef.current) return
if (modelMeshRef.current) {
sceneRef.current.remove(modelMeshRef.current)
modelMeshRef.current.geometry.dispose()
if (modelMeshRef.current.material) {
modelMeshRef.current.material.dispose()
}
modelMeshRef.current = null
}
if (model) {
const material = new THREE.MeshPhongMaterial({
color: 0x4a90d9,
specular: 0x111111,
shininess: 30,
side: THREE.DoubleSide
})
const mesh = new THREE.Mesh(model.geometry, material)
sceneRef.current.add(mesh)
modelMeshRef.current = mesh
}
}, [model])
return (
<div className="model-viewer" ref={containerRef}>
{!model && <div className="placeholder">请导入3D模型</div>}
</div>
)
}
export default ModelViewer

19
src/index.css Normal file
View File

@@ -0,0 +1,19 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow: hidden;
}
#root {
width: 100vw;
height: 100vh;
}

10
src/main.jsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

View File

@@ -0,0 +1,309 @@
const EMPTY_VALUE = -50.0
const WEBGPU_COMPUTE_SHADER = `
struct Params {
gridSize: u32,
kernelRadius: i32,
kernelSize: u32,
centerProfileZ: f32,
}
struct KernelEntry {
kx: i32,
ky: i32,
profileZ: f32,
_pad: f32,
}
@group(0) @binding(0) var<storage, read> inputMap: array<f32>;
@group(0) @binding(1) var<storage, read_write> outputMap: array<f32>;
@group(0) @binding(2) var<uniform> params: Params;
@group(0) @binding(3) var<storage, read> kernel: array<KernelEntry>;
@compute @workgroup_size(16, 16)
fn convolutionMain(@builtin(global_invocation_id) global_id: vec3<u32>) {
let gx = global_id.x;
let gy = global_id.y;
let gs = params.gridSize;
if (gx >= gs || gy >= gs) {
return;
}
let centerIdx = gy * gs + gx;
let currentZ = inputMap[centerIdx];
if (currentZ < -49.9) {
outputMap[centerIdx] = -50.0;
return;
}
var convolutionZ = currentZ;
for (var i: u32 = 0u; i < params.kernelSize; i = i + 1u) {
let kx = kernel[i].kx;
let ky = kernel[i].ky;
let profileZ = kernel[i].profileZ;
let nx = i32(gx) + kx;
let ny = i32(gy) + ky;
if (nx < 0 || nx >= i32(gs) || ny < 0 || ny >= i32(gs)) {
continue;
}
let neighborIdx = u32(ny) * gs + u32(nx);
let neighborZ = inputMap[neighborIdx];
if (neighborZ < -49.9) {
continue;
}
let candidateZ = neighborZ + profileZ;
if (candidateZ > convolutionZ) {
convolutionZ = candidateZ;
}
}
outputMap[centerIdx] = convolutionZ;
}
@compute @workgroup_size(16, 16)
fn toolpathMain(@builtin(global_invocation_id) global_id: vec3<u32>) {
let gx = global_id.x;
let gy = global_id.y;
let gs = params.gridSize;
if (gx >= gs || gy >= gs) {
return;
}
let idx = gy * gs + gx;
let convZ = inputMap[idx];
if (convZ < -49.9) {
outputMap[idx] = -50.0;
} else {
outputMap[idx] = convZ - params.centerProfileZ;
}
}
@compute @workgroup_size(16, 16)
fn repairedMain(@builtin(global_invocation_id) global_id: vec3<u32>) {
let gx = global_id.x;
let gy = global_id.y;
let gs = params.gridSize;
if (gx >= gs || gy >= gs) {
return;
}
let centerIdx = gy * gs + gx;
let convolutionZ = inputMap[centerIdx];
if (convolutionZ < -49.9) {
outputMap[centerIdx] = -50.0;
return;
}
var repairedZ = convolutionZ;
for (var i: u32 = 0u; i < params.kernelSize; i = i + 1u) {
let kx = kernel[i].kx;
let ky = kernel[i].ky;
let profileZ = kernel[i].profileZ;
let nx = i32(gx) + kx;
let ny = i32(gy) + ky;
if (nx < 0 || nx >= i32(gs) || ny < 0 || ny >= i32(gs)) {
continue;
}
let neighborIdx = u32(ny) * gs + u32(nx);
let neighborConvolutionZ = inputMap[neighborIdx];
if (neighborConvolutionZ < -49.9) {
continue;
}
let surfaceZ = neighborConvolutionZ - profileZ;
if (surfaceZ < repairedZ) {
repairedZ = surfaceZ;
}
}
outputMap[centerIdx] = repairedZ;
}
`
export async function runWebGPUConvolution(depthMap, gridSize, resolution, toolType, toolRadius, toolAngle, tipRadius, onProgress) {
if (!navigator.gpu) {
throw new Error('WebGPU not supported')
}
const adapter = await navigator.gpu.requestAdapter()
if (!adapter) {
throw new Error('No WebGPU adapter found')
}
const device = await adapter.requestDevice()
const gpuInputMap = new Float32Array(depthMap.length)
for (let i = 0; i < depthMap.length; i++) {
gpuInputMap[i] = depthMap[i] === -Infinity ? EMPTY_VALUE : depthMap[i]
}
const kernelRadius = Math.ceil(toolRadius / resolution)
const computeToolProfile = (dist) => {
if (toolType === 'ball') {
if (dist <= toolRadius) {
return Math.sqrt(toolRadius * toolRadius - dist * dist)
}
return 0
} else {
const halfAngle = toolAngle * Math.PI / 180 / 2
const tanHalf = Math.tan(halfAngle)
if (dist <= tipRadius) {
return (toolRadius - tipRadius) / tanHalf
}
return (toolRadius - dist) / tanHalf
}
}
const kernel = []
const maxDist = toolRadius
for (let ky = -kernelRadius; ky <= kernelRadius; ky++) {
for (let kx = -kernelRadius; kx <= kernelRadius; kx++) {
const dx = kx * resolution
const dy = ky * resolution
const dist = Math.sqrt(dx * dx + dy * dy)
if (dist <= maxDist) {
const profileZ = computeToolProfile(dist)
kernel.push({ kx, ky, profileZ })
}
}
}
const centerProfileZ = kernel.find(k => k.kx === 0 && k.ky === 0)?.profileZ || 0
const kernelData = new ArrayBuffer(kernel.length * 16)
const kernelView = new DataView(kernelData)
kernel.forEach((k, i) => {
kernelView.setInt32(i * 16, k.kx, true)
kernelView.setInt32(i * 16 + 4, k.ky, true)
kernelView.setFloat32(i * 16 + 8, k.profileZ, true)
kernelView.setFloat32(i * 16 + 12, 0, true)
})
const paramsData = new ArrayBuffer(16)
const paramsView = new DataView(paramsData)
paramsView.setUint32(0, gridSize, true)
paramsView.setInt32(4, kernelRadius, true)
paramsView.setUint32(8, kernel.length, true)
paramsView.setFloat32(12, centerProfileZ, true)
const inputBuffer = device.createBuffer({
size: depthMap.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
})
const outputBuffer = device.createBuffer({
size: depthMap.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
})
const stagingBuffer = device.createBuffer({
size: depthMap.byteLength,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
})
const kernelBuffer = device.createBuffer({
size: kernelData.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
})
const paramsBuffer = device.createBuffer({
size: paramsData.byteLength,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
})
device.queue.writeBuffer(inputBuffer, 0, gpuInputMap)
device.queue.writeBuffer(kernelBuffer, 0, kernelData)
device.queue.writeBuffer(paramsBuffer, 0, paramsData)
const shaderModule = device.createShaderModule({ code: WEBGPU_COMPUTE_SHADER })
const runPass = async (entryPoint, input, output) => {
const bindGroupLayout = device.createBindGroupLayout({
entries: [
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } },
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } },
{ binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } },
]
})
const pipeline = device.createComputePipeline({
layout: device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] }),
compute: { module: shaderModule, entryPoint }
})
const bindGroup = device.createBindGroup({
layout: bindGroupLayout,
entries: [
{ binding: 0, resource: { buffer: input } },
{ binding: 1, resource: { buffer: output } },
{ binding: 2, resource: { buffer: paramsBuffer } },
{ binding: 3, resource: { buffer: kernelBuffer } },
]
})
const commandEncoder = device.createCommandEncoder()
const passEncoder = commandEncoder.beginComputePass()
passEncoder.setPipeline(pipeline)
passEncoder.setBindGroup(0, bindGroup)
passEncoder.dispatchWorkgroups(Math.ceil(gridSize / 16), Math.ceil(gridSize / 16))
passEncoder.end()
commandEncoder.copyBufferToBuffer(output, 0, stagingBuffer, 0, depthMap.byteLength)
device.queue.submit([commandEncoder.finish()])
await stagingBuffer.mapAsync(GPUMapMode.READ)
const result = new Float32Array(stagingBuffer.getMappedRange().slice(0))
stagingBuffer.unmap()
return result
}
if (onProgress) onProgress({ type: 'progress', progress: 5, iteration: 1 })
const convolutionMap = await runPass('convolutionMain', inputBuffer, outputBuffer)
if (onProgress) onProgress({ type: 'progress', progress: 35, iteration: 1 })
device.queue.writeBuffer(inputBuffer, 0, convolutionMap)
const toolpathMap = await runPass('toolpathMain', inputBuffer, outputBuffer)
if (onProgress) onProgress({ type: 'progress', progress: 50, iteration: 1 })
device.queue.writeBuffer(inputBuffer, 0, convolutionMap)
const repairedMap = await runPass('repairedMain', inputBuffer, outputBuffer)
if (onProgress) onProgress({ type: 'progress', progress: 95, iteration: 1 })
inputBuffer.destroy()
outputBuffer.destroy()
stagingBuffer.destroy()
kernelBuffer.destroy()
paramsBuffer.destroy()
device.destroy()
const convertBack = (arr) => {
const result = new Float32Array(arr.length)
for (let i = 0; i < arr.length; i++) {
result[i] = arr[i] < -49.9 ? -Infinity : arr[i]
}
return result
}
return {
convolutionMap: convertBack(convolutionMap),
toolpathMap: convertBack(toolpathMap),
repairedMap: convertBack(repairedMap)
}
}

155
src/workers/repairWorker.js Normal file
View File

@@ -0,0 +1,155 @@
self.onmessage = function(e) {
const { depthMap, gridSize, resolution, toolType, toolRadius, toolAngle, tipRadius } = e.data
const inputMap = new Float32Array(depthMap)
const convolutionMap = new Float32Array(depthMap.length)
convolutionMap.fill(-Infinity)
const repairedMap = new Float32Array(depthMap.length)
repairedMap.fill(-Infinity)
const kernelRadius = toolType === 'ball'
? Math.ceil(toolRadius / resolution)
: Math.ceil(toolRadius / resolution)
const computeToolProfile = (dist) => {
if (toolType === 'ball') {
if (dist <= toolRadius) {
return Math.sqrt(toolRadius * toolRadius - dist * dist)
}
return 0
} else {
const halfAngle = toolAngle * Math.PI / 180 / 2
const tanHalf = Math.tan(halfAngle)
if (dist <= tipRadius) {
return (toolRadius - tipRadius) / tanHalf
}
return (toolRadius - dist) / tanHalf
}
}
const precomputeKernel = () => {
const kernel = []
const maxDist = toolType === 'ball' ? toolRadius : toolRadius
for (let ky = -kernelRadius; ky <= kernelRadius; ky++) {
for (let kx = -kernelRadius; kx <= kernelRadius; kx++) {
const dx = kx * resolution
const dy = ky * resolution
const dist = Math.sqrt(dx * dx + dy * dy)
if (dist <= maxDist) {
const profileZ = computeToolProfile(dist)
kernel.push({ kx, ky, profileZ })
}
}
}
return kernel
}
const kernel = precomputeKernel()
let processedCount = 0
const totalPixels = gridSize * gridSize
for (let gy = 0; gy < gridSize; gy++) {
for (let gx = 0; gx < gridSize; gx++) {
const centerIdx = gy * gridSize + gx
const currentZ = inputMap[centerIdx]
if (currentZ === -Infinity) {
processedCount++
continue
}
let convolutionZ = currentZ
for (const { kx, ky, profileZ } of kernel) {
const nx = gx + kx
const ny = gy + ky
if (nx < 0 || nx >= gridSize || ny < 0 || ny >= gridSize) continue
const neighborIdx = ny * gridSize + nx
const neighborZ = inputMap[neighborIdx]
if (neighborZ === -Infinity) continue
const candidateZ = neighborZ + profileZ
if (candidateZ > convolutionZ) {
convolutionZ = candidateZ
}
}
convolutionMap[centerIdx] = convolutionZ
processedCount++
if (gy % 10 === 0) {
const progress = (processedCount / totalPixels) * 33
self.postMessage({
type: 'progress',
progress: Math.min(progress, 32.9),
iteration: 1
})
}
}
}
const centerProfileZ = kernel.find(k => k.kx === 0 && k.ky === 0)?.profileZ || 0
const toolpathMap = new Float32Array(depthMap.length)
for (let i = 0; i < depthMap.length; i++) {
if (convolutionMap[i] === -Infinity) {
toolpathMap[i] = -Infinity
} else {
toolpathMap[i] = convolutionMap[i] - centerProfileZ
}
}
processedCount = 0
for (let gy = 0; gy < gridSize; gy++) {
for (let gx = 0; gx < gridSize; gx++) {
const centerIdx = gy * gridSize + gx
const convolutionZ = convolutionMap[centerIdx]
if (convolutionZ === -Infinity) {
processedCount++
continue
}
let repairedZ = convolutionZ
for (const { kx, ky, profileZ } of kernel) {
const nx = gx + kx
const ny = gy + ky
if (nx < 0 || nx >= gridSize || ny < 0 || ny >= gridSize) continue
const neighborIdx = ny * gridSize + nx
const neighborConvolutionZ = convolutionMap[neighborIdx]
if (neighborConvolutionZ === -Infinity) continue
const surfaceZ = neighborConvolutionZ - profileZ
if (surfaceZ < repairedZ) {
repairedZ = surfaceZ
}
}
repairedMap[centerIdx] = repairedZ
processedCount++
if (gy % 10 === 0) {
const progress = 50 + (processedCount / totalPixels) * 50
self.postMessage({
type: 'progress',
progress: Math.min(progress, 99.9),
iteration: 1
})
}
}
}
self.postMessage({
type: 'complete',
convolutionMap,
toolpathMap,
repairedMap,
iterations: 1
})
}

6
vite.config.js Normal file
View File

@@ -0,0 +1,6 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
})