{"product_id":"svg-to-cam","title":"SVG to CAM","description":"\u003cdiv class=\"cam-container\" style=\"width: 100%; max-width: 100%; margin: 0 auto; background: #F5EFE0; padding: 20px; border-radius: 4px; box-shadow: 0 2px 8px rgba(44,26,14,0.08); color: #2C1A0E; font-family: system-ui, -apple-system, sans-serif; box-sizing: border-box;\"\u003e\n\n    \u003cdiv style=\"margin-bottom: 24px; padding-bottom: 20px; border-bottom: 1px solid #e0d4c0;\"\u003e\n        \u003cdiv style=\"font-size: 12px; font-weight: 700; letter-spacing: 3px; color: #C47A2B; margin-bottom: 8px; text-transform: uppercase;\"\u003e01 \/ TOOL\u003c\/div\u003e\n        \u003ch2 style=\"font-size: 24px; font-weight: 700; color: #2C1A0E; margin: 0 0 10px 0; line-height: 1.3;\"\u003eSVG to CAM 智慧雕刻轉換工具\u003c\/h2\u003e\n        \u003cp style=\"font-size: 14px; color: #5c5c5c; margin: 0; line-height: 1.7; text-align: justify;\"\u003e將向量圖形轉化為精準的 CNC 刀路，整合幾何取樣、工件尺寸設定、3D 空間刀路模擬與 ISO 後處理 G-code 生成，讓每一道工藝線條都有跡可循。\u003c\/p\u003e\n    \u003c\/div\u003e\n\n    \u003cdiv style=\"display: flex; flex-direction: column; gap: 12px; margin-bottom: 16px;\"\u003e\n\n        \u003cdiv style=\"background: #faf7f2; padding: 14px; border-radius: 4px; border: 1px solid #e0d4c0;\"\u003e\n            \u003cdiv style=\"font-size: 11px; font-weight: 700; color: #C47A2B; letter-spacing: 2px; text-transform: uppercase; margin-bottom: 4px;\"\u003e02 \/ FILE \u0026amp; WCS\u003c\/div\u003e\n            \u003cdiv style=\"font-size: 15px; font-weight: 700; color: #2C1A0E; margin-bottom: 12px; padding-bottom: 6px; border-bottom: 1px solid #e0d4c0;\"\u003e檔案與工件座標系\u003c\/div\u003e\n            \u003cdiv style=\"display: flex; flex-wrap: wrap; gap: 15px; align-items: center;\"\u003e\n                \u003cdiv style=\"flex: 1; min-width: 200px;\"\u003e\n                    \u003clabel style=\"font-size: 12px; font-weight: 600; display: block; margin-bottom: 4px; color: #5c5c5c;\"\u003e選擇 SVG 向量檔案：\u003c\/label\u003e\n                    \u003cinput type=\"file\" id=\"svgFileInput\" accept=\".svg\" style=\"width: 100%; font-size: 12px; padding: 5px; background: #fff; border: 1px solid #e0d4c0; border-radius: 3px; box-sizing: border-box; color: #2C1A0E;\"\u003e\n                \u003c\/div\u003e\n                \u003cdiv\u003e\n                    \u003clabel style=\"font-size: 12px; font-weight: 600; display: block; margin-bottom: 4px; color: #5c5c5c;\"\u003eWCS 原點位置：\u003c\/label\u003e\n                    \u003cselect id=\"wcsSelect\" style=\"padding: 6px 8px; font-size: 12px; border-radius: 3px; border: 1px solid #e0d4c0; background: #fff; width: 170px; color: #2C1A0E;\"\u003e\n                        \u003coption value=\"TOP_CENTER\"\u003e頂面中間 (Top Center)\u003c\/option\u003e\n                        \u003coption value=\"TOP_LEFT\"\u003e頂面左下 (Top Left)\u003c\/option\u003e\n                        \u003coption value=\"TOP_RIGHT\"\u003e頂面右上 (Top Right)\u003c\/option\u003e\n                        \u003coption value=\"BOTTOM_CENTER\"\u003e底面中間 (Bottom Center)\u003c\/option\u003e\n                        \u003coption value=\"BOTTOM_LEFT\"\u003e底面左下 (Bottom Left)\u003c\/option\u003e\n                    \u003c\/select\u003e\n                \u003c\/div\u003e\n                \u003cdiv style=\"padding-top: 18px;\"\u003e\n                    \u003cinput type=\"checkbox\" id=\"enableOptimize\" style=\"cursor: pointer; vertical-align: middle; accent-color: #C47A2B;\"\u003e\n                    \u003clabel for=\"enableOptimize\" style=\"font-size: 12px; font-weight: 600; cursor: pointer; margin-left: 5px; vertical-align: middle; color: #5c5c5c;\"\u003e開啟路徑優化 (0.8mm 公差)\u003c\/label\u003e\n                \u003c\/div\u003e\n            \u003c\/div\u003e\n        \u003c\/div\u003e\n\n        \u003cdiv style=\"background: #faf7f2; padding: 14px; border-radius: 4px; border: 1px solid #e0d4c0;\"\u003e\n            \u003cdiv style=\"font-size: 11px; font-weight: 700; color: #C47A2B; letter-spacing: 2px; text-transform: uppercase; margin-bottom: 4px;\"\u003e03 \/ DIMENSIONS\u003c\/div\u003e\n            \u003cdiv style=\"font-size: 15px; font-weight: 700; color: #2C1A0E; margin-bottom: 12px; padding-bottom: 6px; border-bottom: 1px solid #e0d4c0;\"\u003e加工尺寸與毛胚設定 (mm)\u003c\/div\u003e\n            \u003cdiv style=\"display: flex; flex-wrap: wrap; gap: 15px;\"\u003e\n                \u003cdiv style=\"flex: 1; min-width: 180px; background: #fff; padding: 10px; border-radius: 3px; border: 1px solid #e0d4c0;\"\u003e\n                    \u003cdiv style=\"font-size: 12px; font-weight: 700; color: #2C1A0E; margin-bottom: 8px; display: flex; justify-content: space-between;\"\u003e\n                        \u003cspan\u003e圖案尺寸\u003c\/span\u003e\n                        \u003clabel style=\"font-size: 11px; font-weight: normal; color: #5c5c5c;\"\u003e\u003cinput type=\"checkbox\" id=\"lockAspect\" checked style=\"accent-color: #C47A2B;\"\u003e 鎖定比例\u003c\/label\u003e\n                    \u003c\/div\u003e\n                    \u003cdiv style=\"display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;\"\u003e\n                        \u003cspan style=\"font-size: 12px; color: #5c5c5c;\"\u003e寬度 W:\u003c\/span\u003e\n                        \u003cinput type=\"number\" id=\"sizeW\" value=\"100\" style=\"width: 70px; padding: 3px 5px; font-size: 12px; border: 1px solid #e0d4c0; border-radius: 3px;\"\u003e\n                    \u003c\/div\u003e\n                    \u003cdiv style=\"display: flex; justify-content: space-between; align-items: center;\"\u003e\n                        \u003cspan style=\"font-size: 12px; color: #5c5c5c;\"\u003e高度 H:\u003c\/span\u003e\n                        \u003cinput type=\"number\" id=\"sizeH\" value=\"100\" style=\"width: 70px; padding: 3px 5px; font-size: 12px; border: 1px solid #e0d4c0; border-radius: 3px;\"\u003e\n                    \u003c\/div\u003e\n                \u003c\/div\u003e\n                \u003cdiv style=\"flex: 1; min-width: 180px; background: #fff; padding: 10px; border-radius: 3px; border: 1px solid #e0d4c0;\"\u003e\n                    \u003cdiv style=\"font-size: 12px; font-weight: 700; color: #2C1A0E; margin-bottom: 8px;\"\u003e實體毛胚 (Stock)\u003c\/div\u003e\n                    \u003cdiv style=\"display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;\"\u003e\n                        \u003cspan style=\"font-size: 12px; color: #5c5c5c;\"\u003e邊界 X:\u003c\/span\u003e\n                        \u003cinput type=\"number\" id=\"stockX\" value=\"140\" style=\"width: 70px; padding: 3px 5px; font-size: 12px; border: 1px solid #e0d4c0; border-radius: 3px;\"\u003e\n                    \u003c\/div\u003e\n                    \u003cdiv style=\"display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;\"\u003e\n                        \u003cspan style=\"font-size: 12px; color: #5c5c5c;\"\u003e邊界 Y:\u003c\/span\u003e\n                        \u003cinput type=\"number\" id=\"stockY\" value=\"140\" style=\"width: 70px; padding: 3px 5px; font-size: 12px; border: 1px solid #e0d4c0; border-radius: 3px;\"\u003e\n                    \u003c\/div\u003e\n                    \u003cdiv style=\"display: flex; justify-content: space-between; align-items: center;\"\u003e\n                        \u003cspan style=\"font-size: 12px; color: #5c5c5c;\"\u003e厚度 Z:\u003c\/span\u003e\n                        \u003cinput type=\"number\" id=\"cutDepth\" value=\"15.0\" step=\"0.5\" style=\"width: 70px; padding: 3px 5px; font-size: 12px; border: 1px solid #e0d4c0; border-radius: 3px;\"\u003e\n                    \u003c\/div\u003e\n                \u003c\/div\u003e\n            \u003c\/div\u003e\n        \u003c\/div\u003e\n    \u003c\/div\u003e\n\n    \u003cdiv style=\"font-size: 12px; color: #5c5c5c; background: #fdf6ec; padding: 10px 12px; border-radius: 4px; border-left: 4px solid #C47A2B; margin-bottom: 16px;\"\u003e\n        🔩 系統內定推薦刀具：\u003cspan style=\"color: #8B4513; font-weight: 700;\"\u003eΦ2.0mm 平底銑刀\u003c\/span\u003e（標準雙刃，不換刀模式）\n    \u003c\/div\u003e\n\n    \u003cdiv style=\"display: flex; flex-direction: column; margin-bottom: 16px;\"\u003e\n        \u003cdiv style=\"font-size: 11px; font-weight: 700; color: #C47A2B; letter-spacing: 2px; text-transform: uppercase; margin-bottom: 4px;\"\u003e04 \/ TOOLPATH PREVIEW\u003c\/div\u003e\n        \u003cdiv style=\"font-size: 14px; font-weight: 700; color: #2C1A0E; margin-bottom: 8px;\"\u003e3D 空間加工路徑預覽 \u003cspan style=\"font-size: 11px; font-weight: 400; color: #5c5c5c;\"\u003e（黃線框為毛胚）\u003c\/span\u003e\n\u003c\/div\u003e\n        \u003cdiv id=\"canvas3dContainer\" style=\"width: 100%; height: 320px; border: 1px solid #e0d4c0; background: #1a1a1a; border-radius: 4px; position: relative; overflow: hidden;\"\u003e\u003c\/div\u003e\n        \u003cdiv id=\"pathInfo\" style=\"font-size: 11px; margin-top: 6px; font-weight: 600; color: #8B4513;\"\u003e目前總加工節點數: 0 點\u003c\/div\u003e\n    \u003c\/div\u003e\n\n    \u003cdiv style=\"display: flex; flex-direction: column;\"\u003e\n        \u003cdiv style=\"font-size: 11px; font-weight: 700; color: #C47A2B; letter-spacing: 2px; text-transform: uppercase; margin-bottom: 4px;\"\u003e05 \/ G-CODE OUTPUT\u003c\/div\u003e\n        \u003cdiv style=\"font-size: 14px; font-weight: 700; color: #2C1A0E; margin-bottom: 8px;\"\u003e後處理器 ISO G-code 輸出 (.nc)\u003c\/div\u003e\n        \u003ctextarea id=\"gcodeOutput\" readonly style=\"width: 100%; height: 200px; background: #1e1e1e; color: #a6e22e; font-family: monospace; padding: 12px; border-radius: 4px; border: 1px solid #e0d4c0; resize: none; font-size: 12px; box-sizing: border-box;\" placeholder=\"上傳向量檔案後，此處將自動實時編譯標準工業 CNC 程式碼...\"\u003e\u003c\/textarea\u003e\n        \u003cbutton id=\"downloadGcodeBtn\" style=\"margin-top: 8px; padding: 10px 20px; background: #C47A2B; color: #F5EFE0; border: none; border-radius: 2px; font-weight: 700; cursor: pointer; font-size: 12px; letter-spacing: 1px; text-transform: uppercase; align-self: flex-end; transition: background 0.2s;\"\u003eDOWNLOAD G-CODE\u003c\/button\u003e\n    \u003c\/div\u003e\n\n    \u003csvg id=\"hiddenSvgContainer\" style=\"display:none;\"\u003e\u003c\/svg\u003e\n\u003c\/div\u003e\n\n\u003cscript src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/three.js\/r128\/three.min.js\"\u003e\u003c\/script\u003e\n\u003cscript src=\"https:\/\/cdn.jsdelivr.net\/npm\/three@0.128.0\/examples\/js\/controls\/OrbitControls.js\"\u003e\u003c\/script\u003e\n\n\u003cscript\u003e\nlet originalMmPaths = []; \nlet currentAspect = 1.0;\nlet isUpdatingSize = false;\n\nlet scene, camera, renderer, controls;\nlet stockMesh, toolpathLines = [], wcsAxes;\n\nfunction init3DEngine() {\n    const container = document.getElementById('canvas3dContainer');\n    \n    scene = new THREE.Scene();\n    scene.background = new THREE.Color(0x1e1e1e);\n\n    camera = new THREE.PerspectiveCamera(40, container.clientWidth \/ container.clientHeight, 1, 2000);\n    camera.position.set(160, 160, 200);\n\n    renderer = new THREE.WebGLRenderer({ antialias: true });\n    renderer.setSize(container.clientWidth, container.clientHeight);\n    container.appendChild(renderer.domElement);\n\n    controls = new THREE.OrbitControls(camera, renderer.domElement);\n    controls.enableDamping = true;\n    controls.dampingFactor = 0.05;\n\n    const ambientLight = new THREE.AmbientLight(0x777777);\n    scene.add(ambientLight);\n    const dirLight = new THREE.DirectionalLight(0xffffff, 0.7);\n    dirLight.position.set(150, 300, 200);\n    scene.add(dirLight);\n\n    const gridHelper = new THREE.GridHelper(320, 32, 0x444444, 0x2d2d2d);\n    gridHelper.position.z = -0.1; \n    gridHelper.rotation.x = Math.PI \/ 2;\n    scene.add(gridHelper);\n\n    window.addEventListener('resize', onWindowResize);\n\n    function animate() {\n        requestAnimationFrame(animate);\n        controls.update();\n        renderer.render(scene, camera);\n    }\n    animate();\n}\n\nfunction onWindowResize() {\n    const container = document.getElementById('canvas3dContainer');\n    if (!container || !camera || !renderer) return;\n    camera.aspect = container.clientWidth \/ container.clientHeight;\n    camera.updateProjectionMatrix();\n    renderer.setSize(container.clientWidth, container.clientHeight);\n}\n\ninit3DEngine();\n\nfunction optimizePath(points, tolerance) {\n    if (points.length \u003c= 2) return points;\n    let maxSqDist = 0, index = 0, end = points.length - 1;\n    for (let i = 1; i \u003c end; i++) {\n        let dx = points[end].x - points[0].x, dy = points[end].y - points[0].y;\n        let t = (dx !== 0 || dy !== 0) ? ((points[i].x - points[0].x) * dx + (points[i].y - points[0].y) * dy) \/ (dx*dx + dy*dy) : 0;\n        t = Math.max(0, Math.min(1, t));\n        let lx = points[0].x + t * dx, ly = points[0].y + t * dy;\n        let sqDist = (points[i].x - lx)**2 + (points[i].y - ly)**2;\n        if (sqDist \u003e maxSqDist) { index = i; maxSqDist = sqDist; }\n    }\n    if (maxSqDist \u003e tolerance * tolerance) {\n        let r1 = optimizePath(points.slice(0, index + 1), tolerance);\n        let r2 = optimizePath(points.slice(index), tolerance);\n        return r1.slice(0, r1.length - 1).concat(r2);\n    }\n    return [points[0], points[end]];\n}\n\ndocument.getElementById('svgFileInput').addEventListener('change', function(e) {\n    const file = e.target.files[0];\n    if (!file) return;\n    const reader = new FileReader();\n    reader.onload = function(event) {\n        document.getElementById('hiddenSvgContainer').innerHTML = event.target.result;\n        const paths = document.getElementById('hiddenSvgContainer').querySelectorAll('path');\n        originalMmPaths = [];\n\n        paths.forEach(path =\u003e {\n            let totalLength = 0;\n            try { totalLength = path.getTotalLength(); } catch(err) { return; }\n            if (totalLength === 0) return;\n            let pts = [];\n            let step = Math.max(0.2, totalLength \/ 400);\n            for (let len = 0; len \u003c= totalLength; len += step) {\n                let p = path.getPointAtLength(len);\n                pts.push({x: p.x, y: p.y});\n            }\n            originalMmPaths.push(pts);\n        });\n\n        if (originalMmPaths.length === 0) return;\n\n        let allPts = originalMmPaths.flat();\n        let minX = Math.min(...allPts.map(p =\u003e p.x)), maxX = Math.max(...allPts.map(p =\u003e p.x));\n        let minY = Math.min(...allPts.map(p =\u003e p.y)), maxY = Math.max(...allPts.map(p =\u003e p.y));\n        let cx = (minX + maxX) \/ 2, cy = (minY + maxY) \/ 2;\n        \n        originalMmPaths = originalMmPaths.map(sub =\u003e sub.map(p =\u003e ({ x: p.x - cx, y: -(p.y - cy) })));\n\n        let finalW = maxX - minX, finalH = maxY - minY;\n        currentAspect = finalW \/ finalH;\n        \n        isUpdatingSize = true;\n        document.getElementById('sizeW').value = Math.round(finalW);\n        document.getElementById('sizeH').value = Math.round(finalH);\n        document.getElementById('stockX').value = Math.round(finalW * 1.3);\n        document.getElementById('stockY').value = Math.round(finalH * 1.3);\n        isUpdatingSize = false;\n\n        processAndRender3DCAM();\n    };\n    reader.readAsText(file);\n});\n\ndocument.getElementById('sizeW').addEventListener('input', function() {\n    if (isUpdatingSize || originalMmPaths.length === 0) return;\n    if (document.getElementById('lockAspect').checked) {\n        isUpdatingSize = true;\n        document.getElementById('sizeH').value = (this.value \/ currentAspect).toFixed(1);\n        isUpdatingSize = false;\n    }\n    processAndRender3DCAM();\n});\ndocument.getElementById('sizeH').addEventListener('input', function() {\n    if (isUpdatingSize || originalMmPaths.length === 0) return;\n    if (document.getElementById('lockAspect').checked) {\n        isUpdatingSize = true;\n        document.getElementById('sizeW').value = (this.value * currentAspect).toFixed(1);\n        isUpdatingSize = false;\n    }\n    processAndRender3DCAM();\n});\n\nfunction processAndRender3DCAM() {\n    if (originalMmPaths.length === 0) return;\n\n    if(stockMesh) scene.remove(stockMesh);\n    toolpathLines.forEach(l =\u003e scene.remove(l));\n    toolpathLines = [];\n    if(wcsAxes) scene.remove(wcsAxes);\n\n    const targetW = parseFloat(document.getElementById('sizeW').value) || 100;\n    const targetH = parseFloat(document.getElementById('sizeH').value) || 100;\n    const stockX = parseFloat(document.getElementById('stockX').value) || 140;\n    const stockY = parseFloat(document.getElementById('stockY').value) || 140;\n    const stockZ = parseFloat(document.getElementById('cutDepth').value) || 15.0;\n    const wcsType = document.getElementById('wcsSelect').value;\n    const optEnabled = document.getElementById('enableOptimize').checked;\n\n    const activeCutDepth = 2.0; \n\n    let allPts = originalMmPaths.flat();\n    let minX = Math.min(...allPts.map(p =\u003e p.x)), maxX = Math.max(...allPts.map(p =\u003e p.x));\n    let minY = Math.min(...allPts.map(p =\u003e p.y)), maxY = Math.max(...allPts.map(p =\u003e p.y));\n    let currentW = maxX - minX, currentH = maxY - minY;\n    \n    let scaledPaths = originalMmPaths.map(sub =\u003e sub.map(p =\u003e ({\n        x: p.x * (targetW \/ currentW),\n        y: p.y * (targetH \/ currentH)\n    })));\n\n    if (optEnabled) { scaledPaths = scaledPaths.map(sub =\u003e optimizePath(sub, 0.8)); }\n\n    let offsetX = 0, offsetY = 0, offsetZ = 0;\n    let stockMeshX = 0, stockMeshY = 0, stockMeshZ = -stockZ \/ 2;\n\n    if (wcsType === 'TOP_CENTER') {\n        offsetX = 0; offsetY = 0; offsetZ = 0;\n        stockMeshX = 0; stockMeshY = 0; stockMeshZ = -stockZ \/ 2;\n    } else if (wcsType === 'TOP_LEFT') {\n        offsetX = targetW \/ 2; offsetY = targetH \/ 2; offsetZ = 0;\n        stockMeshX = stockX \/ 2; stockMeshY = stockY \/ 2; stockMeshZ = -stockZ \/ 2;\n    } else if (wcsType === 'TOP_RIGHT') {\n        offsetX = -targetW \/ 2; offsetY = -targetH \/ 2; offsetZ = 0;\n        stockMeshX = -stockX \/ 2; stockMeshY = -stockY \/ 2; stockMeshZ = -stockZ \/ 2;\n    } else if (wcsType === 'BOTTOM CENTER') {\n        offsetX = 0; offsetY = 0; offsetZ = stockZ;\n        stockMeshX = 0; stockMeshY = 0; stockMeshZ = stockZ \/ 2;\n    } else if (wcsType === 'BOTTOM_LEFT') {\n        offsetX = targetW \/ 2; offsetY = targetH \/ 2; offsetZ = stockZ;\n        stockMeshX = stockX \/ 2; stockMeshY = stockY \/ 2; stockMeshZ = stockZ \/ 2;\n    }\n\n    const geometry = new THREE.BoxGeometry(stockX, stockY, stockZ);\n    const material = new THREE.MeshPhongMaterial({\n        color: 0x8B6914,\n        transparent: true,\n        opacity: 0.25\n    });\n    stockMesh = new THREE.Mesh(geometry, material);\n    stockMesh.position.set(stockMeshX, stockMeshY, stockMeshZ);\n    scene.add(stockMesh);\n\n    const edges = new THREE.EdgesGeometry(geometry);\n    const lineMat = new THREE.LineBasicMaterial({ color: 0xf1c40f });\n    const wireframe = new THREE.LineSegments(edges, lineMat);\n    stockMesh.add(wireframe);\n\n    let totalPoints = 0;\n    let zCutLevel = -activeCutDepth + offsetZ;\n\n    scaledPaths.forEach(sub =\u003e {\n        if (sub.length === 0) return;\n        totalPoints += sub.length;\n\n        const points3D = [];\n        points3D.push(new THREE.Vector3(sub[0].x + offsetX, sub[0].y + offsetY, 5.0 + offsetZ));\n        sub.forEach(p =\u003e { points3D.push(new THREE.Vector3(p.x + offsetX, p.y + offsetY, zCutLevel)); });\n        points3D.push(new THREE.Vector3(sub[sub.length-1].x + offsetX, sub[sub.length-1].y + offsetY, 5.0 + offsetZ));\n\n        const pathGeom = new THREE.BufferGeometry().setFromPoints(points3D);\n        const pathMat = new THREE.LineBasicMaterial({ color: 0x00ffcc, linewidth: 2 });\n        const pathLine = new THREE.Line(pathGeom, pathMat);\n        scene.add(pathLine);\n        toolpathLines.push(pathLine);\n    });\n\n    wcsAxes = new THREE.AxesHelper(35);\n    wcsAxes.material.linewidth = 3;\n    scene.add(wcsAxes);\n\n    document.getElementById('pathInfo').innerText = `目前總加工節點數: ${totalPoints} 點`;\n\n    let gcode = [];\n    gcode.push(\"%\");\n    gcode.push(\"O1002 (WKIDEA SVG-TO-CAM PROCESSOR)\");\n    gcode.push(\"(TOOL: FLAT END MILL D2.0)\");\n    gcode.push(\"G90 G21 G17 G40 G80\");\n    gcode.push(\"G94\");\n    gcode.push(\"M03 S12000\");\n    gcode.push(`G00 Z ${(5.0 + offsetZ).toFixed(3)}`);\n\n    scaledPaths.forEach(sub =\u003e {\n        if (sub.length \u003c 2) return;\n        gcode.push(`G00 X ${(sub[0].x + offsetX).toFixed(3)} Y ${(sub[0].y + offsetY).toFixed(3)}`);\n        gcode.push(`G01 Z ${zCutLevel.toFixed(3)} F300`);\n        for(let i=1; i\u003csub.length; i++) {\n            gcode.push(`G01 X ${(sub[i].x + offsetX).toFixed(3)} Y ${(sub[i].y + offsetY).toFixed(3)} F1200`);\n        }\n        gcode.push(`G00 Z ${(5.0 + offsetZ).toFixed(3)}`);\n    });\n\n    gcode.push(\"M05\");\n    gcode.push(\"G00 X0 Y0\");\n    gcode.push(\"M30\");\n    gcode.push(\"%\");\n\n    document.getElementById('gcodeOutput').value = gcode.join(\"\\n\");\n}\n\n['stockX', 'stockY', 'cutDepth', 'wcsSelect', 'enableOptimize'].forEach(id =\u003e {\n    document.getElementById(id).addEventListener('change', processAndRender3DCAM);\n});\n\ndocument.getElementById('downloadGcodeBtn').addEventListener('click', function() {\n    const text = document.getElementById('gcodeOutput').value;\n    if(!text) return;\n    const blob = new Blob([text], { type: 'text\/plain' });\n    const url = URL.createObjectURL(blob);\n    const a = document.createElement('a');\n    a.href = url;\n    a.download = 'wkidea_cam_output.nc';\n    a.click();\n    URL.revokeObjectURL(url);\n});\n\u003c\/script\u003e","brand":"WKidea","offers":[{"title":"Default Title","offer_id":49993168519445,"sku":null,"price":30.0,"currency_code":"TWD","in_stock":true}],"url":"https:\/\/wkidea.com\/products\/svg-to-cam","provider":"WKidea","version":"1.0","type":"link"}