diff --git a/src/kiri/app/paint.js b/src/kiri/app/paint.js index 872df5b8..7b76ac5d 100644 --- a/src/kiri/app/paint.js +++ b/src/kiri/app/paint.js @@ -17,6 +17,11 @@ * support regions. For multi-material coloring or other uses, set * options.onlyDownwardFacing = false to paint all surfaces. * + * BELT PRINTER MODE: + * For belt printers, surfaces need support when facing "down the belt slope" rather + * than straight down. Set options.beltMode = true and provide options.beltAngle (in radians) + * to paint surfaces that face down the angled belt direction. + * * PAINT POINT DATA STRUCTURE: * --------------------------- * Paint points must be an array of objects with this structure: @@ -79,6 +84,9 @@ import { THREE } from '../../ext/three.js'; * @param {THREE.Color} [paintColor] - Color for painted regions (default: light blue) * @param {Object} [options] - Optional configuration * @param {boolean} [options.onlyDownwardFacing=true] - Only paint on downward-facing surfaces (z-normal < 0) + * @param {boolean} [options.beltMode=false] - Enable belt printer mode (paints surfaces facing down belt slope) + * @param {number} [options.beltAngle=0] - Belt angle in radians (only used if beltMode is true) + * @param {number} [options.supportAngle=45] - Support angle in degrees (cone around down direction) * @returns {THREE.Material} The modified material */ function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Color(0x4488ff), options = {}) { @@ -89,6 +97,10 @@ function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Col // Default: only paint downward-facing surfaces (for support painting) const onlyDownwardFacing = options.onlyDownwardFacing !== undefined ? options.onlyDownwardFacing : true; + const beltMode = options.beltMode || false; + const beltAngle = options.beltAngle || 0; + const supportAngle = options.supportAngle || 45; + const supportThreshold = Math.cos((90 - supportAngle) * Math.PI / 180); // Store initial paint points reference material.userData.paintPoints = paintPoints; @@ -119,6 +131,9 @@ function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Col shader.uniforms.paintCount = { value: validPoints.length }; shader.uniforms.paintColor = { value: paintColor }; shader.uniforms.onlyDownwardFacing = { value: onlyDownwardFacing }; + shader.uniforms.beltMode = { value: beltMode }; + shader.uniforms.beltAngle = { value: beltAngle }; + shader.uniforms.supportThreshold = { value: supportThreshold }; // Pass model position from vertex shader to fragment shader // Paint points are in widget/model space, so we compare against transformed (model space) @@ -143,6 +158,9 @@ function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Col uniform int paintCount; uniform vec3 paintColor; uniform bool onlyDownwardFacing; + uniform bool beltMode; + uniform float beltAngle; + uniform float supportThreshold; ` + shader.fragmentShader; shader.fragmentShader = shader.fragmentShader.replace( @@ -156,7 +174,20 @@ function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Col vec3 worldNormal = normalize(cross(posDx, posDy)); // Only paint downward-facing surfaces if option is enabled - bool shouldPaint = !onlyDownwardFacing || worldNormal.z < -0.001; + bool shouldPaint; + if (beltMode) { + // Belt mode: mesh is rotated, check if normal points toward world down + // World down (0,0,-1) appears as (0, -sin(θ), -cos(θ)) in rotated mesh space + // Dot product: normal · (0, -sin(θ), -cos(θ)) = -sin(θ)*ny - cos(θ)*nz + float sinAngle = sin(beltAngle); + float cosAngle = cos(beltAngle); + float rotatedZ = worldNormal.y * sinAngle + worldNormal.z * cosAngle; + // Check if within support angle cone from down direction (rotatedZ < -threshold) + shouldPaint = !onlyDownwardFacing || rotatedZ < -supportThreshold; + } else { + // Normal mode: check if normal points downward within support angle cone + shouldPaint = !onlyDownwardFacing || worldNormal.z < -supportThreshold; + } if (shouldPaint) { // Check if this fragment is within any paint sphere bool painted = false; @@ -206,6 +237,9 @@ function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Col * @param {THREE.Color} [paintColor] - Color for painted regions (default: light blue) * @param {Object} [options] - Optional configuration * @param {boolean} [options.onlyDownwardFacing=true] - Only paint on downward-facing surfaces (z-normal < 0) + * @param {boolean} [options.beltMode=false] - Enable belt printer mode (paints surfaces facing down belt slope) + * @param {number} [options.beltAngle=0] - Belt angle in radians (only used if beltMode is true) + * @param {number} [options.supportAngle=45] - Support angle in degrees (cone around down direction) * @returns {THREE.Material} The modified material */ function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Color(0x4488ff), options = {}) { @@ -216,6 +250,10 @@ function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Co // Default: only paint downward-facing surfaces (for support painting) const onlyDownwardFacing = options.onlyDownwardFacing !== undefined ? options.onlyDownwardFacing : true; + const beltMode = options.beltMode || false; + const beltAngle = options.beltAngle || 0; + const supportAngle = options.supportAngle || 45; + const supportThreshold = Math.cos((90 - supportAngle) * Math.PI / 180); // Store initial paint points reference material.userData.paintPoints = paintPoints; @@ -264,6 +302,9 @@ function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Co shader.uniforms.paintTexSize = { value: new THREE.Vector2(texWidth, texHeight) }; shader.uniforms.paintColor = { value: paintColor }; shader.uniforms.onlyDownwardFacing = { value: onlyDownwardFacing }; + shader.uniforms.beltMode = { value: beltMode }; + shader.uniforms.beltAngle = { value: beltAngle }; + shader.uniforms.supportThreshold = { value: supportThreshold }; // Pass model position from vertex shader to fragment shader // Paint points are in widget/model space, so we compare against transformed (model space) @@ -289,6 +330,9 @@ function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Co uniform vec2 paintTexSize; uniform vec3 paintColor; uniform bool onlyDownwardFacing; + uniform bool beltMode; + uniform float beltAngle; + uniform float supportThreshold; ` + shader.fragmentShader; shader.fragmentShader = shader.fragmentShader.replace( @@ -302,7 +346,20 @@ function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Co vec3 worldNormal = normalize(cross(posDx, posDy)); // Only paint downward-facing surfaces if option is enabled - bool shouldPaint = !onlyDownwardFacing || worldNormal.z < -0.001; + bool shouldPaint; + if (beltMode) { + // Belt mode: mesh is rotated, check if normal points toward world down + // World down (0,0,-1) appears as (0, -sin(θ), -cos(θ)) in rotated mesh space + // Dot product: normal · (0, -sin(θ), -cos(θ)) = -sin(θ)*ny - cos(θ)*nz + float sinAngle = sin(beltAngle); + float cosAngle = cos(beltAngle); + float rotatedZ = worldNormal.y * sinAngle + worldNormal.z * cosAngle; + // Check if within support angle cone from down direction (rotatedZ < -threshold) + shouldPaint = !onlyDownwardFacing || rotatedZ < -supportThreshold; + } else { + // Normal mode: check if normal points downward within support angle cone + shouldPaint = !onlyDownwardFacing || worldNormal.z < -supportThreshold; + } if (shouldPaint) { bool painted = false; diff --git a/src/kiri/mode/fdm/app/init-ui.js b/src/kiri/mode/fdm/app/init-ui.js index 2045f0ac..d74c7c74 100644 --- a/src/kiri/mode/fdm/app/init-ui.js +++ b/src/kiri/mode/fdm/app/init-ui.js @@ -267,7 +267,30 @@ function supportStart({ remove } = { remove: false }) { return api.widgets.meshes(); }; api.feature.on_mouse_drag = ({ int }) => { - if (int?.length && int[0].face.normal.z < 0) { + // Calculate if surface needs support based on belt mode or normal mode + let needsSupport = false; + if (int?.length) { + const settings = api.conf.get(); + const normal = int[0].face.normal; + + // Get support angle threshold (how steep before needing support) + const supportAngle = settings.process.sliceSupportAngle || 45; + const supportThreshold = Math.cos((90 - supportAngle) * Math.PI / 180); + + if (settings.device.bedBelt) { + // Belt mode: check if surface will face downward after belt rotation + // After rotating by angle θ around X: nz' = ny*sin(θ) + nz*cos(θ) + // We want nz' < -threshold (within cone of support angle from down) + const angle = (settings.process.sliceAngle || 45) * Math.PI / 180; + const rotatedZ = normal.y * Math.sin(angle) + normal.z * Math.cos(angle); + needsSupport = rotatedZ < -supportThreshold; + } else { + // Normal mode: check if normal points downward within support angle cone + needsSupport = normal.z < -supportThreshold; + } + } + + if (needsSupport) { supportUpdate(int[0].point, down.widget, remove); } return [ down.widget.mesh ]; @@ -279,6 +302,12 @@ function supportStart({ remove } = { remove: false }) { const scheme = colorSchemeRegistry.getScheme(mode, theme); const paintColor = scheme.operations?.paint?.overlay ?? 0x4488ff; + // Get belt configuration for paint constraint + const settings = api.conf.get(); + const isBelt = settings.device.bedBelt; + const beltAngle = isBelt ? (settings.process.sliceAngle || 45) : 0; + const supportAngle = settings.process.sliceSupportAngle || 45; + api.widgets.each(w => { // Use pushVisualState to track the paint operation w.pushVisualState('paint', { @@ -295,7 +324,11 @@ function supportStart({ remove } = { remove: false }) { if (!w.anno.paint) { w.anno.paint = []; } - addPaintOverlayTexture(mat, w.anno.paint, new THREE.Color(paintColor)); + addPaintOverlayTexture(mat, w.anno.paint, new THREE.Color(paintColor), { + beltMode: isBelt, + beltAngle: beltAngle * Math.PI / 180, // Convert to radians + supportAngle: settings.process.sliceSupportAngle || 45 + }); }) api.space.update(); }