⚠ Designet til desktop-skærme

Dette værktøj blev bygget til PC-skærme (1920×1080 og derover) med et tastatur og en mus. Det bliver nok Virker ikke behageligt på en mobilskærm?.

Tip: bookmærk cncfoam.com og åbn det på en bærbar computer.

Gratis hot-wire foam CAM, der kører i din browser

Designér eller importer, forhåndsvis i 3D, og send G-code direkte til din ESP32/FluidNC eller GRBL maskinen via USB eller Wi-Fi? 2–5 akser. Ingen installation, ingen tilmelding, ingen eksportbegrænsninger.

Bygget af Pete ScheepensHvem står bag FoamCube – maskiner sendt til producenter over hele verden siden 2015? Hvorfor CNCFOAM?

Importér SVG, DXF eller G-kode fil (eller en STL/OBJ 3D-model), indstil din maskinstørrelse, skumblok, fødehastighed og tråd-temperatur, og værktøjet simulerer skæringen og beregner skæretiden samt en solid 3D-forhåndsvisning, inden du smelter noget som helst skum. Det skærer regulerede overflader: lige ekstruderinger (2-akset), koniske og snoede vinger og vingeprofiler via en to-profil morf (4-akse X/Y + U/V), loftede former, kanaler, spiralformede søjler og reglerede overfladearkitektur, plus indekserede rotationsdele på 3/5-akset udstyr.

Virkelige snit, ikke renders — skum skåret på en rigtig maskine:

En batch af hvide EPS-skumdele skåret med varmetråd på en FoamCube — en to-søjlet balustrade, en top, en græsk-nøgle-frise, tandhjul og cylindre — på et arbejdsbord
En batch af skumarkitektoniske dele, varmtråds skåret
Flere identiske rulleprofiler skærer lige igennem en hel pakke EPS-polystyren-isolationsplade, med et udskåret skumblomst ved siden af.
Identiske profiler skæres gennem en hel pakke EPS-plade

CNCFOAM.COM

Formbibliotek Wiki
XYUVA ▾
SKALA
%
%
%
%
ROTERE
°
OFFSET FRA 0,0
mm
mm
mm
mm
Drop .gcode / .nc / .txt / .cnc / .tap / .ngc / .svg
Hot wire
Tråd (hurtig/af)
Skæringsbane
Oprindelseslinje (X)0 Y0)

Status

4-akset
0

Rotér del

X-akse Y-aksen Z-akse
Importér STL/OBJ (+ Indlæs dele) for at genorientere det.

Skæreindstillinger

Materialblok

Fra 0-linjen. Gemt på denne enhed.
G-KODE ⋮⋮ XYUVA ▾ Kopiér ×
X/Y-plan ⋮⋮ planblok ×
hjul / +− zoom · træk for at panorere · 0 nulstil
U/V-plan ⋮⋮ planblok ×
hjul / +− zoom · træk for at panorere · 0 nulstil

Kontroller

Banetræk / pile
Pandeshift-drag / højre-drag
Zoomhjul / + −
Afspil / Pauserum
TilbageR
Nulstil visningHjem / F
Indstillinger træk ⋮⋮
Maskinindstillinger

Maskintype

2-akset spejling U/V til X/Y automatisk. To-delt morf er deaktiveret. 3/5-akset tilføjer en rotationsakse til indekserede flersidige snit.

Rotationsakse (A)

Vertikal anvender Offset X + Z. Horisontal anvender Offset Y + Z. Frit positionering understøttet — indstil offsets efter eget valg.
Bemærk: G92 erklærer blot den aktuelle position som nul — det flytter IKKE fysisk på maskinen. At sætte flueben her springer denne linje over; nyttigt hvis din controller-workflow allerede fastlægger sit eget arbejds-nul (f.eks. via $H eller en brugerdefineret knap).

Skærestørrelse (maskinens arbejdsområde)

Akseorientering

X vendt som standard, så 0-linjen er forrest?
Materialblok befinder sig i venstre sidepanel.
Controller / G-kodeformat
controlleren og cncfoam.com udfører altid identisk bevægelse — denne indstilling ændrer kun hvilken akse bogstaver bliver skrevet ind i den eksporterede / streamede G-kode. Live G-kode-vinduet og mærker afspejler altid dit valg
Foam-native (X Y U V A) — LinuxCNC, Mach3/4 og de fleste dedikerede foam controllere. GRBL / FluidNC (X Y Z B A) — ESP32 + FluidNC, grbl-Mega-5X og lignende boards, der kun understøtter X Y Z A B C, så U/V-tårnet omdøbes til Z/B. Brugerdefineret X-akse Y-akse Z-akse U-akse V-akse A-akse
Formerlige former i biblioteket er altid gemt kanonisk (X Y U V A) og omdøbt til dit format ved download — så den samme form fungerer til alle controllere. Akse-navne & din controller
Oprindelsesmarkør (X=0, Y=0)
Den synlige 0,0-markør i 3D-visningsvinduet — en lille prik + tynd stiplet linje, der viser, hvor maskinens nulpunkt er. Positionen er i maskinkoordinater; farve + diameter er kun visuelle og påvirker aldrig skæringen.
Værktøjsgrænseflade

Synlig

Udseende

Streaming via USB

Linje 0 / 0 · ledig

Wi-Fi · FluidNC-maskine

Projekter

Material

GEN's — formgeneratorer

Generer skum RC / EDF jet vingepanel fra NACA 4-cifret aerofoiler — eller vælg en navngivet profil (MH45, Clark Y, …) til flyvinger og svævefly. De 4 NACA-cifre er max kamber %, camber position og tykkelse % — e.g. 2412 = 2% camber at 40% chord, 12% thick; 0009 = symmetric, 9% thick. Set a smaller tip chord Til konisk form, en anden vingprofil for at blende, plus svep og skylSkær som en 4-tråds morf fra rod til spids.

Hurtig start: 20 færdige forudindstillinger

Lige vinge: sæt tip-korde = rod-korde, sveep 0 Skyl vipper spidsen nedad for bløde stalling (drejer om ¼-kordeVinger skæres som én panel — skær et spejlpar Flip G-koden eller skummet til den anden sideprojekteret til din blokposition Vinger & NACA forklaret · Spar tunnel?

 

Del dit design

Udgiv til bibliotek

Du skal have en gratis konto for at offentliggøre i den offentlige Shape-bibliotek. Nye former bliver gennemgået, før de bliver live.

Der er endnu intet privat bibliotek — alt hvad du udgiver er offentligt.

Log ind Opret konto
flerakset CNC-skæring af skum gjort simpelt og gratis at bruge )
⚠ Beta-test — hjælp mig venligst | Tak PIBOT til din donation
oves(r.moves, { lengthZ }); document.getElementById('morphStatus').textContent = `Generated ${r.moves.length} moves over ${lengthZ} mm.`; closeModal('parts'); }; // More dropdown const moreMenu = document.getElementById('moreMenu'); document.getElementById('moreBtn').onclick = e => { e.stopPropagation(); if (typeof renderRecentFiles === 'function') renderRecentFiles(); moreMenu.classList.toggle('open'); }; document.addEventListener('click', e => { if (!document.getElementById('moreWrap').contains(e.target)) moreMenu.classList.remove('open'); }); // ---- Download / stream fold-out menu (combines G-code / 3D / USB / Wi-Fi) ---- { const dlWrap = document.getElementById('dlWrap'); const dlBtn = document.getElementById('dlMenuBtn'); if (dlWrap && dlBtn){ dlBtn.onclick = e => { e.stopPropagation(); dlWrap.classList.toggle('open'); }; document.addEventListener('click', e => { if (!dlWrap.contains(e.target)) dlWrap.classList.remove('open'); }); dlWrap.querySelectorAll('#dlMenu button').forEach(b => b.addEventListener('click', () => dlWrap.classList.remove('open'))); } } // ---- Recent files (last 5 loaded shapes / demos / G-code drops) ---- // Each entry: { name, kind: 'gcode'|'svg'|'dxf'|'demo'|'library', payload, ts } // payload is the file text for local loads, or a shape-library id for // library loads. Stored in localStorage so the list survives page reloads. const RECENT_KEY = 'cncfoam-recent'; const RECENT_MAX = 5; function recentList(){ try { return JSON.parse(localStorage.getItem(RECENT_KEY) || '[]'); } catch(e){ return []; } } function recentRecord(entry){ let list = recentList().filter(e => !(e.name === entry.name && e.kind === entry.kind)); list.unshift({ ...entry, ts: Date.now() }); list = list.slice(0, RECENT_MAX); // localStorage has a ~5MB cap; drop the largest payload if the JSON grows // beyond 1.5MB (a couple of huge G-code files would otherwise fill the bucket). while (list.length){ try { const j = JSON.stringify(list); if (j.length < 1500000){ localStorage.setItem(RECENT_KEY, j); return; } } catch(e){} // Strip the payload of the oldest entry first. for (let i = list.length - 1; i >= 0; i--){ if (list[i].payload){ list[i] = { ...list[i], payload: null }; break; } if (i === 0) list.pop(); } } } function renderRecentFiles(){ const list = recentList(); const section = document.getElementById('mmRecentSection'); const container = document.getElementById('mmRecentList'); if (!section || !container) return; if (!list.length){ section.style.display = 'none'; return; } section.style.display = 'block'; container.innerHTML = ''; for (const e of list){ const row = document.createElement('div'); row.className = 'mm-recent'; const ageMs = Date.now() - (e.ts || 0); const age < 60000 ? 'just now' : ageMs < 3600000 ? Math.round(ageMs/60000) + 'm ago' : ageMs < 86400000 ? Math.round(ageMs/3600000) + 'h ago' : Math.round(ageMs/86400000) + 'd ago'; const icon = e.kind === 'library' ? '🗂' : e.kind === 'demo' ? '🎬' : e.kind === 'gcode' ? '⚙️' : e.kind === 'dxf' ? '◇' : e.kind === 'svg' ? '✎' : '·'; row.innerHTML = 'G-kode<>Send<':'<','>Der er ingen tekst at oversætte.alder'; row.onclick = async () => { moreMenu.classList.remove('open'); try { if (e.kind === 'library' && e.payload){ window.location.href = '/?shape=' + encodeURIComponent(e.payload); return; } if (!e.payload){ document.getElementById('status').textContent = '"' + e.name + '" is too old to reload — pick the file again.'; return; } if (e.kind === 'gcode' || e.kind === 'demo'){ loadMoves(parseGcode(e.payload)); } else { const blob = new Blob([e.payload], { type: 'text/plain' }); const file = new File([blob], e.name, { type: 'text/plain' }); await pickProfile(file, 'A'); setTimeout(() => { const b = document.getElementById('morphGo'); if (b && !b.disabled) b.click(); }, 250); } } catch(err){ document.getElementById('status').textContent = 'Recent reload failed: ' + err.message; } }; container.appendChild(row); } } // ---------- OBJECT toolbar ---------- function readScale(src){ const linkEl = document.getElementById('scaleAll'); // Direct edits to Y/U/V were being clobbered by X on every keystroke when // link-all was on. Auto-uncheck the link so manual Y/U/V edits stick. if (linkEl.checked && src && (src === 'scaleY' || src === 'scaleU' || src === 'scaleV')) { linkEl.checked = false; } const link = linkEl.checked; const rot = parseFloat(document.getElementById('rotUV').value) || 0; const mx = parseFloat(document.getElementById('movX').value) || 0; const my = parseFloat(document.getElementById('movY').value) || 0; const mu = parseFloat(document.getElementById('movU').value) || 0; const mv = parseFloat(document.getElementById('movV').value) || 0; if (link){ const v = (parseFloat(document.getElementById('scaleX').value) || 100) / 100; SCALE = { x:v, y:v, u:v, v:v, rotUV:rot, movX:mx, movY:my, movU:mu, movV:mv }; ['scaleY','scaleU','scaleV'].forEach(id => document.getElementById(id).value = Math.round(v*100)); } else { SCALE = { x: (parseFloat(document.getElementById('scaleX').value) || 100) / 100, y: (parseFloat(document.getElementById('scaleY').value) || 100) / 100, u: (parseFloat(document.getElementById('scaleU').value) || 100) / 100, v: (parseFloat(document.getElementById('scaleV').value) || 100) / 100, rotUV: rot, movX: mx, movY: my, movU: mu, movV: mv, }; } applyScaleToMoves(); } ['scaleX','scaleY','scaleU','scaleV','scaleAll','rotUV','movX','movY','movU','movV'].forEach(id => { document.getElementById(id).addEventListener('input', () => readScale(id)); document.getElementById(id).addEventListener('change', () => readScale(id)); }); document.getElementById('scaleReset').onclick = () => { ['scaleX','scaleY','scaleU','scaleV'].forEach(id => document.getElementById(id).value = 100); document.getElementById('scaleAll').checked = true; document.getElementById('rotUV').value = 0; document.getElementById('movX').value = 0; document.getElementById('movY').value = 0; document.getElementById('movU').value = 0; document.getElementById('movV').value = 0; readScale(); }; // ---------- Settings draggable + accordion ---------- (function() { const card = document.getElementById('settingsCard'); const handle = document.getElementById('settingsDrag'); let dx=0, dy=0, dragging=false; handle.addEventListener('mousedown', e => { dragging=true; const r=card.getBoundingClientRect(); dx=e.clientX-r.left; dy=e.clientY-r.top; e.preventDefault(); }); window.addEventListener('mousemove', e => { if (!dragging) return; card.style.right='auto'; card.style.left=Math.max(0,Math.min(window.innerWidth-card.offsetWidth,e.clientX-dx))+'px'; card.style.top=Math.max(0,Math.min(window.innerHeight-60,e.clientY-dy))+'px'; }); window.addEventListener('mouseup', () => dragging=false); document.querySelectorAll('#settings details.acc').forEach(d => { d.addEventListener('toggle', () => { if (d.open) document.querySelectorAll('#settings details.acc').forEach(o => { if (o!==d) o.open=false; }); }); }); })(); // ---------- UI visibility toggles ---------- function applyUiToggles(){ document.getElementById('objectBar').classList.toggle('hidden', !CFG.showObjBar); document.getElementById('controls') .classList.toggle('hidden', !CFG.showControls); // Status section now lives inside #leftPanel — toggle its visibility there. const lp = document.getElementById('leftPanel'); if (lp) lp.classList.toggle('hud-hidden', !CFG.showHud); document.getElementById('legend') .classList.toggle('hidden', !CFG.showLegend); } // Top-centre reminder when the user has disabled the auto-zero line. Dismissable // for 6 hours via the × (reappears after that, or on a fresh trigger/reload). function applyNoHomingBanner(){ const b = document.getElementById('noHomingBanner'); if (!b) return; let until = 0; try { until = parseInt(localStorage.getItem('cncfoam-nohoming-dismiss') || '0', 10) || 0; } catch(e){} b.style.display = (CFG && CFG.noHoming && Date.now() > until) ? 'block' : 'none'; } { const nx = document.getElementById('noHomingX'); if (nx) nx.onclick = () => { try { localStorage.setItem('cncfoam-nohoming-dismiss', String(Date.now() + 6 * 3600 * 1000)); } catch(e){} applyNoHomingBanner(); }; } bindCheckbox('cfgShowObjBar', 'showObjBar', applyUiToggles); bindCheckbox('cfgShowControls', 'showControls', applyUiToggles); bindCheckbox('cfgShowHud', 'showHud', applyUiToggles); bindCheckbox('cfgShowLegend', 'showLegend', applyUiToggles); // ---------- Mini preview window ---------- const previewBox = document.getElementById('preview'); const previewBody = document.getElementById('previewBody'); const previewRenderer = new THREE.WebGLRenderer({ antialias:true, alpha:true }); previewRenderer.setPixelRatio(Math.min(devicePixelRatio||1, 2)); previewRenderer.setSize(240, 220); previewRenderer.setClearColor(0x0e1116, 0); previewBody.appendChild(previewRenderer.domElement); const previewScene = new THREE.Scene(); const previewCamera = new THREE.PerspectiveCamera(35, 240/220, 0.1, 8000); previewScene.add(new THREE.AmbientLight(0xffffff, 0.55)); const pvLight = new THREE.DirectionalLight(0xffffff, 0.9); pvLight.position.set(300, 500, 400); previewScene.add(pvLight); const pvLight2 = new THREE.DirectionalLight(0xffffff, 0.25); pvLight2.position.set(-200, -100, 200); previewScene.add(pvLight2); // Share the cut geometry + material so the preview tracks the main scene live. const previewCut = new THREE.Mesh(cutGeo, cutMat); previewScene.add(previewCut); // Faint wireframe block in the preview for spatial context. const previewBlockGroup = new THREE.Group(); previewScene.add(previewBlockGroup); function rebuildPreviewBlock(){ while (previewBlockGroup.children.length){ const c=previewBlockGroup.children.pop(); if (c.geometry) c.geometry.dispose(); if (c.material) c.material.dispose(); } const g = new THREE.BoxGeometry(BLOCK.x, BLOCK.y, BLOCK.z); const edges = new THREE.LineSegments(new THREE.EdgesGeometry(g), new THREE.LineBasicMaterial({ color:0x444c56 })); edges.position.set(BLOCK.x/2, BLOCK.y/2, BLOCK.z/2); previewBlockGroup.add(edges); } rebuildPreviewBlock(); let pvYaw = 0; function renderPreview(dt){ if (!previewBox.classList.contains('show')) return; pvYaw += dt * 0.6; const dist = Math.max(BLOCK.x, BLOCK.y, BLOCK.z) * 1.5 || 1000; previewCamera.position.set( BLOCK.x/2 + dist * Math.sin(pvYaw), BLOCK.y/2 + dist * 0.35, BLOCK.z/2 + dist * Math.cos(pvYaw) ); previewCamera.lookAt(BLOCK.x/2, BLOCK.y/2, BLOCK.z/2); previewRenderer.render(previewScene, previewCamera); } // Hook into the main animation loop const _origFrame = frame; let lastPv = null; window._cncPv = () => { /* placeholder so closures see it */ }; function pvLoop(now){ requestAnimationFrame(pvLoop); if (lastPv === null){ lastPv = now; return; } const dt = Math.min(0.1, (now - lastPv) / 1000); lastPv = now; renderPreview(dt); } requestAnimationFrame(pvLoop); // Show / hide preview, drag, close const _origLoadMoves2 = loadMoves; loadMoves = function(parsed, opts){ _origLoadMoves2(parsed, opts); previewBox.classList.add('show'); rebuildPreviewBlock(); }; document.getElementById('previewClose').onclick = () => previewBox.classList.remove('show'); (function() { const handle = previewBox.querySelector('.ph'); let dx=0, dy=0, dragging=false; handle.addEventListener('mousedown', e => { if (e.target.id === 'previewClose') return; dragging=true; const r=previewBox.getBoundingClientRect(); dx=e.clientX-r.left; dy=e.clientY-r.top; e.preventDefault(); }); window.addEventListener('mousemove', e => { if (!dragging) return; previewBox.style.right='auto'; previewBox.style.left=Math.max(0,Math.min(window.innerWidth-previewBox.offsetWidth,e.clientX-dx))+'px'; previewBox.style.top=Math.max(0,Math.min(window.innerHeight-50,e.clientY-dy))+'px'; }); window.addEventListener('mouseup', () => dragging=false); })(); // ---------- Init (no auto-load demo) ---------- syncCfgInputs(); document.getElementById('cfgShowObjBar').checked = !!CFG.showObjBar; document.getElementById('cfgShowControls').checked = !!CFG.showControls; document.getElementById('cfgShowHud').checked = !!CFG.showHud; document.getElementById('cfgShowLegend').checked = !!CFG.showLegend; { const g = document.getElementById('cfgShowGcode'); if (g) g.checked = !!CFG.showGcode; } { const xy = document.getElementById('cfgShowXyView'); if (xy) xy.checked = !!CFG.showXyView; } { const uv = document.getElementById('cfgShowUvView'); if (uv) uv.checked = !!CFG.showUvView; } { const h = document.getElementById('cfgNoHoming'); if (h) h.checked = !!CFG.noHoming; } if (typeof gcViewApplyToggle === 'function') gcViewApplyToggle(); if (window.fvXY) fvXY.applyToggle(); if (window.fvUV) fvUV.applyToggle(); if (typeof applyNoHomingBanner === 'function') applyNoHomingBanner(); applyAppearance(); applyUiToggles(); buildWire(); // Hydrate BLOCK from saved CFG (cutter size persists across reloads). This // calls buildBlock() internally + re-frames the camera + fires the checkFit // warning. Direct buildBlock() here would use stale default 500×500×500. resizeBlockFromCfg(); resize(); resetView(); requestAnimationFrame(frame); updatePartsUI(); document.getElementById('status').textContent = 'No part loaded — click + Load parts to start.';