checkpoint material painting

This commit is contained in:
Stewart Allen 2026-01-05 20:09:00 -05:00
commit dff3cf9b1e
14 changed files with 267 additions and 712 deletions

View file

@ -177,7 +177,7 @@ export function init_input() {
space.sky.showGrid(false); space.sky.showGrid(false);
space.sky.setColor(controller.dark ? 0 : 0xffffff); space.sky.setColor(controller.dark ? 0 : 0xffffff);
space.setAntiAlias(controller.antiAlias); space.setAntiAlias(controller.antiAlias);
space.init(container, function (delta) { space.init(container, (delta) => {
const { lo, hi, max } = slider.getRange(); const { lo, hi, max } = slider.getRange();
if (max === 0 || !delta) return; if (max === 0 || !delta) return;
if (controller.reverseZoom) delta = -delta; if (controller.reverseZoom) delta = -delta;
@ -471,7 +471,7 @@ export function init_input() {
DOC.body.requestFullscreen(); DOC.body.requestFullscreen();
} catch (e) { } catch (e) {
event.emit('resize'); event.emit('resize');
moto.space.event.onResize(); space.event.onResize();
} }
}; };

View file

@ -113,10 +113,7 @@ class InteractionControl {
this.#space.mouse.downSelect((int, event) => { this.#space.mouse.downSelect((int, event) => {
// Feature hook for custom mouse down handling // Feature hook for custom mouse down handling
if (this.#api.feature.on_mouse_down) { if (this.#api.feature.on_mouse_down) {
if (int) { return this.#api.feature.on_mouse_down(int, event);
this.#api.feature.on_mouse_down(int, event);
return;
}
} }
// Hover mode handling // Hover mode handling
@ -178,9 +175,10 @@ class InteractionControl {
}); });
// Drag handler // Drag handler
this.#space.mouse.onDrag((delta, offset, up = false) => { this.#space.mouse.onDrag((delta, offset, up = false, int) => {
if (this.#api.feature.hover) { if (this.#api.feature.hover) {
return; return this.#api.feature?.on_mouse_drag({ delta, offset, up, int });
// return;
} }
if (up) { if (up) {

View file

@ -71,21 +71,45 @@ import { THREE } from '../../ext/three.js';
* @returns {THREE.Material} The modified material * @returns {THREE.Material} The modified material
*/ */
function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Color(0x4488ff)) { function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Color(0x4488ff)) {
// Initialize userData if it doesn't exist
if (!material.userData) {
material.userData = {};
}
// Store initial paint points reference
material.userData.paintPoints = paintPoints;
material.userData.paintColor = paintColor;
material.onBeforeCompile = (shader) => { material.onBeforeCompile = (shader) => {
// Store shader reference for later updates // Store shader reference for later updates
material.userData.shader = shader; material.userData.shader = shader;
// Use updated paint points if they were modified before first compile
const currentPoints = material.userData.paintPoints || paintPoints;
// Convert paint points to vec4 array (x, y, z, radius) // Convert paint points to vec4 array (x, y, z, radius)
const points = paintPoints.map(p => // Filter out invalid entries
new THREE.Vector4(p.point.x, p.point.y, p.point.z, p.radius) const validPoints = currentPoints
); .filter(p => p && p.point && typeof p.radius === 'number')
.map(p => new THREE.Vector4(p.point.x, p.point.y, p.point.z, p.radius));
// Pad array to exactly 256 elements (shader uniform array size)
// THREE.js requires the array length to match the shader declaration
const points = new Array(256);
for (let i = 0; i < 256; i++) {
points[i] = i < validPoints.length ? validPoints[i] : new THREE.Vector4(0, 0, 0, 0);
}
console.log('[Paint Shader] onBeforeCompile called with', validPoints.length, 'valid points');
console.log('[Paint Shader] First point:', currentPoints[0]);
// Add uniforms for paint data // Add uniforms for paint data
shader.uniforms.paintPoints = { value: points }; shader.uniforms.paintPoints = { value: points };
shader.uniforms.paintCount = { value: points.length }; shader.uniforms.paintCount = { value: validPoints.length };
shader.uniforms.paintColor = { value: paintColor }; shader.uniforms.paintColor = { value: paintColor };
// Pass world position from vertex shader to fragment shader // Pass model position from vertex shader to fragment shader
// Paint points are in widget/model space, so we compare against transformed (model space)
shader.vertexShader = shader.vertexShader.replace( shader.vertexShader = shader.vertexShader.replace(
`#include <worldpos_vertex>`, `#include <worldpos_vertex>`,
` `
@ -128,9 +152,14 @@ function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Col
} }
if (painted) { if (painted) {
// Blend paint color with existing fragment color // Blend paint color with existing fragment color (increased to 0.7 for visibility)
gl_FragColor.rgb = mix(gl_FragColor.rgb, paintColor, 0.5); gl_FragColor.rgb = mix(gl_FragColor.rgb, paintColor, 0.7);
} }
// DEBUG: Uncomment to tint entire mesh when shader is active (proves shader is running)
// if (paintCount > 0) {
// gl_FragColor.rgb += vec3(0.3, 0.0, 0.0); // Add red tint to entire mesh
// }
` `
); );
}; };
@ -154,21 +183,36 @@ function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Col
* @returns {THREE.Material} The modified material * @returns {THREE.Material} The modified material
*/ */
function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Color(0x4488ff)) { function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Color(0x4488ff)) {
// Initialize userData if it doesn't exist
if (!material.userData) {
material.userData = {};
}
// Store initial paint points reference
material.userData.paintPoints = paintPoints;
material.userData.paintColor = paintColor;
material.onBeforeCompile = (shader) => { material.onBeforeCompile = (shader) => {
// Store shader reference for later updates // Store shader reference for later updates
material.userData.shader = shader; material.userData.shader = shader;
// Use updated paint points if they were modified before first compile
const currentPoints = material.userData.paintPoints || paintPoints;
// Filter out invalid entries
const validPoints = currentPoints.filter(p => p && p.point && typeof p.radius === 'number');
// Create data texture from paint points // Create data texture from paint points
// Texture is laid out as square grid: width x height >= point count // Texture is laid out as square grid: width x height >= point count
const pointCount = paintPoints.length; const pointCount = validPoints.length;
const texWidth = Math.ceil(Math.sqrt(pointCount)); const texWidth = Math.ceil(Math.sqrt(pointCount));
const texHeight = Math.ceil(pointCount / texWidth); const texHeight = Math.ceil(pointCount / texWidth);
const data = new Float32Array(texWidth * texHeight * 4); const data = new Float32Array(texWidth * texHeight * 4);
// Pack paint points into texture data // Pack paint points into texture data
// Each point = 4 floats (x, y, z, radius) // Each point = 4 floats (x, y, z, radius)
for (let i = 0; i < paintPoints.length; i++) { for (let i = 0; i < validPoints.length; i++) {
const p = paintPoints[i]; const p = validPoints[i];
data[i * 4 + 0] = p.point.x; data[i * 4 + 0] = p.point.x;
data[i * 4 + 1] = p.point.y; data[i * 4 + 1] = p.point.y;
data[i * 4 + 2] = p.point.z; data[i * 4 + 2] = p.point.z;
@ -191,7 +235,8 @@ function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Co
shader.uniforms.paintTexSize = { value: new THREE.Vector2(texWidth, texHeight) }; shader.uniforms.paintTexSize = { value: new THREE.Vector2(texWidth, texHeight) };
shader.uniforms.paintColor = { value: paintColor }; shader.uniforms.paintColor = { value: paintColor };
// Pass world position from vertex shader to fragment shader // Pass model position from vertex shader to fragment shader
// Paint points are in widget/model space, so we compare against transformed (model space)
shader.vertexShader = shader.vertexShader.replace( shader.vertexShader = shader.vertexShader.replace(
`#include <worldpos_vertex>`, `#include <worldpos_vertex>`,
` `
@ -245,9 +290,14 @@ function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Co
} }
if (painted) { if (painted) {
// Blend paint color with existing fragment color // Blend paint color with existing fragment color (increased to 0.7 for visibility)
gl_FragColor.rgb = mix(gl_FragColor.rgb, paintColor, 0.5); gl_FragColor.rgb = mix(gl_FragColor.rgb, paintColor, 0.7);
} }
// DEBUG: Uncomment to tint entire mesh when shader is active (proves shader is running)
// if (paintCount > 0) {
// gl_FragColor.rgb += vec3(0.3, 0.0, 0.0); // Add red tint to entire mesh
// }
` `
); );
}; };
@ -282,16 +332,32 @@ function addPaintOverlayAuto(material, paintPoints, paintColor = new THREE.Color
* IMPORTANT: The material must have already been initialized with one of the * IMPORTANT: The material must have already been initialized with one of the
* addPaintOverlay functions before calling this update function. * addPaintOverlay functions before calling this update function.
* *
* NOTE: If the shader hasn't been compiled yet (first render not complete),
* this will trigger a material update to force recompilation with new points.
*
* @param {THREE.Material} material - Material with paint shader already applied * @param {THREE.Material} material - Material with paint shader already applied
* @param {Array<{point: {x,y,z}, radius: number}>} paintPoints - Updated array of paint spheres * @param {Array<{point: {x,y,z}, radius: number}>} paintPoints - Updated array of paint spheres
*/ */
function updatePaintOverlay(material, paintPoints) { function updatePaintOverlay(material, paintPoints) {
// Initialize userData if it doesn't exist
if (!material.userData) {
material.userData = {};
}
// Store paint points reference in userData for access during shader compilation
if (!material.userData.paintPoints) {
material.userData.paintPoints = paintPoints;
}
// Access the compiled shader uniforms // Access the compiled shader uniforms
// After onBeforeCompile runs once, userData.shader contains the compiled program // After onBeforeCompile runs once, userData.shader contains the compiled program
const shader = material.userData?.shader; const shader = material.userData?.shader;
if (!shader || !shader.uniforms) { if (!shader || !shader.uniforms) {
console.warn('updatePaintOverlay: material does not have compiled paint shader'); // Shader hasn't compiled yet (first render not complete)
// Store the new points and trigger recompile
material.userData.paintPoints = paintPoints;
material.needsUpdate = true;
return; return;
} }
@ -315,13 +381,20 @@ function updatePaintOverlaySimple(shader, paintPoints) {
const uniforms = shader.uniforms; const uniforms = shader.uniforms;
// Convert paint points to vec4 array (x, y, z, radius) // Convert paint points to vec4 array (x, y, z, radius)
const points = paintPoints.map(p => // Filter out invalid entries
new THREE.Vector4(p.point.x, p.point.y, p.point.z, p.radius) const validPoints = paintPoints
); .filter(p => p && p.point && typeof p.radius === 'number')
.map(p => new THREE.Vector4(p.point.x, p.point.y, p.point.z, p.radius));
// Pad array to exactly 256 elements (shader uniform array size)
const points = new Array(256);
for (let i = 0; i < 256; i++) {
points[i] = i < validPoints.length ? validPoints[i] : new THREE.Vector4(0, 0, 0, 0);
}
// Update uniform values // Update uniform values
uniforms.paintPoints.value = points; uniforms.paintPoints.value = points;
uniforms.paintCount.value = points.length; uniforms.paintCount.value = validPoints.length;
// Note: No need to set needsUpdate on the material // Note: No need to set needsUpdate on the material
// Uniform updates are automatically detected by THREE.js // Uniform updates are automatically detected by THREE.js
@ -334,15 +407,18 @@ function updatePaintOverlaySimple(shader, paintPoints) {
function updatePaintOverlayTexture(shader, paintPoints) { function updatePaintOverlayTexture(shader, paintPoints) {
const uniforms = shader.uniforms; const uniforms = shader.uniforms;
// Filter out invalid entries
const validPoints = paintPoints.filter(p => p && p.point && typeof p.radius === 'number');
// Recreate texture with new paint points // Recreate texture with new paint points
const pointCount = paintPoints.length; const pointCount = validPoints.length;
const texWidth = Math.ceil(Math.sqrt(pointCount)); const texWidth = Math.ceil(Math.sqrt(pointCount));
const texHeight = Math.ceil(pointCount / texWidth); const texHeight = Math.ceil(pointCount / texWidth);
const data = new Float32Array(texWidth * texHeight * 4); const data = new Float32Array(texWidth * texHeight * 4);
// Pack paint points into texture data // Pack paint points into texture data
for (let i = 0; i < paintPoints.length; i++) { for (let i = 0; i < validPoints.length; i++) {
const p = paintPoints[i]; const p = validPoints[i];
data[i * 4 + 0] = p.point.x; data[i * 4 + 0] = p.point.x;
data[i * 4 + 1] = p.point.y; data[i * 4 + 1] = p.point.y;
data[i * 4 + 2] = p.point.z; data[i * 4 + 2] = p.point.z;

View file

@ -49,11 +49,11 @@ class Widget extends WidgetCore {
} }
const widget = this; const widget = this;
index().put('ws-save-'+this.id, { index().put('ws-save-'+this.id, {
anno: this.annotations(),
geo: widget.getGeoVertices({ unroll: false }).slice(), geo: widget.getGeoVertices({ unroll: false }).slice(),
track: widget.track,
group: this.group.id, group: this.group.id,
meta: this.meta, meta: this.meta,
anno: this.annotations() track: widget.track
}, result => { }, result => {
widget.meta.saved = Date.now(); widget.meta.saved = Date.now();
if (ondone) ondone(); if (ondone) ondone();

View file

@ -19,7 +19,7 @@ let debug = self.debug === true,
poolpath; poolpath;
/** /**
* @param {Function} fn name of function in kiri.worker * @param {Function} fn name of function in worker
* @param {Object} data to send to server * @param {Object} data to send to server
* @param {Function} onreply function to call on reply messages * @param {Function} onreply function to call on reply messages
* @param {Object[]} zerocopy array of objects to pass using zerocopy * @param {Object[]} zerocopy array of objects to pass using zerocopy
@ -31,14 +31,17 @@ function send(fn, data, onreply, zerocopy) {
// track it only when we expect and can handle a reply // track it only when we expect and can handle a reply
running[seq] = { fn:onreply }; running[seq] = { fn:onreply };
} }
let msg = { let msg = {
seq: seq, seq: seq,
task: fn, task: fn,
time: time(), time: time(),
data: data data: data
}; };
// console.log('client send', msg); // console.log('client send', msg);
// if (!fn) { console.trace({empty_fn: data}) }; // if (!fn) { console.trace({empty_fn: data}) };
try { try {
worker.postMessage(msg, zerocopy); worker.postMessage(msg, zerocopy);
} catch (error) { } catch (error) {
@ -46,10 +49,19 @@ function send(fn, data, onreply, zerocopy) {
} }
} }
// Promise wrapper around send()
function sendp(fn, data, zerocopy) {
return new Promise(resolve => {
send(fn, data, resolve, zerocopy);
});
}
// code is running in the browser / client context // code is running in the browser / client context
export const client = { export const client = {
send, send,
sendp,
setWorkPath(path) { setWorkPath(path) {
workpath = path; workpath = path;
return client; return client;
@ -269,7 +281,7 @@ export const client = {
ondone(null, reply.error); ondone(null, reply.error);
} }
if (reply.debug) { if (reply.debug) {
api.event.emit("export.debug", reply.debug); console.log('worker debug', reply.debug);
} }
}); });
}, },

View file

@ -115,7 +115,7 @@ function save(quiet) {
api.conf.save(); api.conf.save();
const newWidgets = []; const newWidgets = [];
const oldWidgets = js2o(SDB.getItem('ws-widgets'), []); const oldWidgets = js2o(SDB.getItem('ws-widgets'), []);
api.widgets.each(function(widget) { api.widgets.each(widget => {
if (widget.synth) return; if (widget.synth) return;
newWidgets.push(widget.id); newWidgets.push(widget.id);
oldWidgets.remove(widget.id); oldWidgets.remove(widget.id);

View file

@ -23,6 +23,7 @@ class Widget {
this.id = id || Date.now().toString(36)+(nextId++); this.id = id || Date.now().toString(36)+(nextId++);
this.grouped = group ? true : false; this.grouped = group ? true : false;
// persisted
this.group = group || []; this.group = group || [];
this.group.push(this); this.group.push(this);
if (!this.group.id) { if (!this.group.id) {
@ -33,10 +34,8 @@ class Widget {
} }
// rotation stack (for undo) // rotation stack (for undo)
this.roto = []; this.roto = [];
// added meshes (supports, tabs, etc) // overlay meshes (supports, tabs, etc)
this.adds = []; this.adds = [];
// persisted client annotations (cam tabs, fdm supports)
this.anno = {};
// THREE Mesh and points // THREE Mesh and points
this.mesh = null; this.mesh = null;
this.points = null; this.points = null;
@ -48,6 +47,24 @@ class Widget {
this.settings = null; // used?? this.settings = null; // used??
this.modified = true; this.modified = true;
this.boundingBoxNeedsUpdate = true this.boundingBoxNeedsUpdate = true
// cache shadow geo
this.cache = {};
this.stats = {
slice_time: 0,
load_time: 0,
progress: 0
};
// if this is a synthesized support widget
this.support = false;
// persisted: client annotations (cam tabs, fdm supports)
this.anno = {};
// persisted: file meta-data
this.meta = {
url: null,
file: null,
saved: false
};
// persisted: location state
this.track = { this.track = {
// box size for packer // box size for packer
box: { box: {
@ -74,21 +91,7 @@ class Widget {
mirror: false, mirror: false,
indexed: false, indexed: false,
indexRad: 0 indexRad: 0
},
// used to cache shadow geo
this.cache = {};
this.stats = {
slice_time: 0,
load_time: 0,
progress: 0
}; };
this.meta = {
url: null,
file: null,
saved: false
};
// if this is a synthesized support widget
this.support = false;
} }
annotations() { annotations() {

View file

@ -1,7 +1,7 @@
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */ /** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
import { util } from '../../../geo/base.js'; import { util } from '../../../geo/base.js';
import { fdm_slice, slicePost, supports } from './slice.js'; import { fdm_slice, slicePost } from './slice.js';
import { fdm_prepare } from './prepare.js'; import { fdm_prepare } from './prepare.js';
import { fdm_export } from './export.js'; import { fdm_export } from './export.js';
import { codec } from '../../core/codec.js'; import { codec } from '../../core/codec.js';
@ -24,15 +24,15 @@ function extrudeMM(dist, perMM, factor) {
// defer loading until client and worker exist // defer loading until client and worker exist
function init(worker) { function init(worker) {
worker.dispatch.fdm_support_generate = function(data, send) { // worker.dispatch.fdm_support_generate = function(data, send) {
const { settings } = data; // const { settings } = data;
const widgets = Object.values(worker.cache); // const widgets = Object.values(worker.cache);
const fresh = widgets.filter(widget => supports(settings, widget)); // const fresh = widgets.filter(widget => supports(settings, widget));
send.done(codec.encode(fresh.map(widget => { return { // send.done(codec.encode(fresh.map(widget => { return {
id: widget.id, // id: widget.id,
supports: widget.supports, // supports: widget.supports,
} } ))); // } } )));
}; // };
} }

View file

@ -41,6 +41,10 @@ function optSelected(sel) {
return opt ? opt.value : undefined; return opt ? opt.value : undefined;
} }
function manualSupport() {
return api.conf.proc().sliceSupportType === 'manual';
}
function hasInfill() { function hasInfill() {
return optSelected(ui.sliceFillType) !== 'none' return optSelected(ui.sliceFillType) !== 'none'
} }
@ -111,7 +115,7 @@ export function menu() {
outputFanSpeed: newInput(LANG.ou_fans_s, {title:LANG.ou_fans_l, convert:toInt, bound:bound(0,255)}), outputFanSpeed: newInput(LANG.ou_fans_s, {title:LANG.ou_fans_l, convert:toInt, bound:bound(0,255)}),
outputMinLayerTime: newInput(LANG.ou_layt_s, { title:LANG.ou_layt_l, convert:toInt, bound:bound(0,200) }), outputMinLayerTime: newInput(LANG.ou_layt_s, { title:LANG.ou_layt_l, convert:toInt, bound:bound(0,200) }),
_____: newGroup(LANG.sp_menu, $('fdm-support'), { modes:FDM, driven, hideable, separator, group:"fdm-supp" }), _____: newGroup(LANG.sp_menu, $('fdm-support'), { modes:FDM, driven, hideable, separator, group:"fdm-supp" }),
sliceSupportType: newSelect(LANG.sp_type_s, {title:LANG.sp_type_l}, "support"), sliceSupportType: newSelect(LANG.sp_type_s, {title:LANG.sp_type_l, trigger}, "support"),
sliceSupportNozzle: newSelect(LANG.sp_nozl_s, {title:LANG.sp_nozl_l, show:isMultiHead}, "extruders"), sliceSupportNozzle: newSelect(LANG.sp_nozl_s, {title:LANG.sp_nozl_l, show:isMultiHead}, "extruders"),
separator: newBlank({ class:"set-sep", driven }), separator: newBlank({ class:"set-sep", driven }),
sliceSupportAngle: newInput(LANG.sp_angl_s, {title:LANG.sp_angl_l, convert:toFloat, bound:bound(0.0,90.0)}), sliceSupportAngle: newInput(LANG.sp_angl_s, {title:LANG.sp_angl_l, convert:toFloat, bound:bound(0.0,90.0)}),
@ -127,16 +131,16 @@ export function menu() {
sliceSupportTree: newBoolean(LANG.sp_tree_s, onBooleanClick, {title:LANG.sp_tree_s, trigger, show:isNotBelt}), sliceSupportTree: newBoolean(LANG.sp_tree_s, onBooleanClick, {title:LANG.sp_tree_s, trigger, show:isNotBelt}),
// sliceSupportEnable: newBoolean(LANG.sp_auto_s, onBooleanClick, {title:LANG.sp_auto_l, show:isNotBelt}), // sliceSupportEnable: newBoolean(LANG.sp_auto_s, onBooleanClick, {title:LANG.sp_auto_l, show:isNotBelt}),
sliceSupportOutline: newBoolean(LANG.sp_outl_s, onBooleanClick, {title:LANG.sp_outl_l, show: () => !isTree() }), sliceSupportOutline: newBoolean(LANG.sp_outl_s, onBooleanClick, {title:LANG.sp_outl_l, show: () => !isTree() }),
separator: newBlank({ class:"set-sep", driven }), separator: newBlank({ class:"set-sep", driven, show:manualSupport }),
sliceSupportGen: newRow([ sliceSupportGen: newRow([
ui.ssaGen = newButton(LANG.sp_detect, onButtonClick, {class: "f-col grow a-center"}) ui.ssaGen = newButton(LANG.sp_detect, onButtonClick, {class: "f-col grow a-center"})
], { modes: FDM, class: "ext-buttons f-row grow", show: () => !ui.sliceSupportTree.checked }), ], { modes: FDM, class: "ext-buttons f-row grow", show:manualSupport }),
separator: newBlank({ class:"set-sep", driven }), // separator: newBlank({ class:"set-sep", driven }),
sliceSupportManual: newRow([ sliceSupportManual: newRow([
(ui.ssmAdd = newButton(undefined, onButtonClick, {icon:'<i class="fas fa-plus"></i>'})), (ui.ssmAdd = newButton(undefined, onButtonClick, {icon:'<i class="fas fa-plus"></i>'})),
(ui.ssmDun = newButton(undefined, onButtonClick, {icon:'<i class="fas fa-check"></i>'})), (ui.ssmDun = newButton(undefined, onButtonClick, {icon:'<i class="fas fa-check"></i>'})),
(ui.ssmClr = newButton(undefined, onButtonClick, {icon:'<i class="fas fa-trash-alt"></i>'})) (ui.ssmClr = newButton(undefined, onButtonClick, {icon:'<i class="fas fa-trash-alt"></i>'}))
], {class:"ext-buttons f-row"}), ], {class:"ext-buttons f-row", show:manualSupport}),
_____: newGroup(LANG.fl_menu, $('fdm-base'), { modes:FDM, driven, hideable, separator, group:"fdm-base" }), _____: newGroup(LANG.fl_menu, $('fdm-base'), { modes:FDM, driven, hideable, separator, group:"fdm-base" }),
firstSliceHeight: newInput(LANG.fl_lahi_s, {title:LANG.fl_lahi_l, convert:toFloat, show:isNotBelt}), firstSliceHeight: newInput(LANG.fl_lahi_s, {title:LANG.fl_lahi_l, convert:toFloat, show:isNotBelt}),
firstLayerNozzleTemp:newInput(LANG.fl_nozl_s, {title:LANG.fl_nozl_l, convert:toInt, show:isNotBelt}), firstLayerNozzleTemp:newInput(LANG.fl_nozl_s, {title:LANG.fl_nozl_l, convert:toInt, show:isNotBelt}),

View file

@ -3,24 +3,16 @@
import { $ } from '../../../moto/webui.js'; import { $ } from '../../../moto/webui.js';
import { api } from '../../app/api.js'; import { api } from '../../app/api.js';
import { conf } from '../../app/conf/defaults.js'; import { conf } from '../../app/conf/defaults.js';
import { util } from '../../../geo/base.js'; import { addPaintOverlayTexture, updatePaintOverlay } from '../../app/paint.js';
import { client } from '../../app/workers.js';
import { addbox, delbox, getlastbox } from '../../core/boxes.js';
import { Layers } from '../../app/layers.js';
import { Stack } from '../../app/stack.js';
import { Widget, newWidget } from '../../app/widget.js';
import { space } from '../../../moto/space.js';
const { Matrix4, Vector3, BufferAttribute, BufferGeometryUtils, Raycaster, Euler } = THREE; const { Matrix4, Vector3, BufferAttribute, BufferGeometryUtils, Raycaster, Euler } = THREE;
const { VIEWS } = api.const; const { VIEWS } = api.const;
const LANG = api.language.current; const LANG = api.language.current;
let lastBox, lastMode, lastView, lastPillar, fromPillar, let lastView,
addingSupports = false, addingSupports = false,
isFdmMode = false, isFdmMode = false,
nextID = Date.now(),
p1, p2, iw,
alert = [], alert = [],
func = {}; func = {};
@ -80,16 +72,6 @@ export function init() {
} }
} }
function filterSynth() {
api.widgets.filter((widget) => {
if (widget.track.synth) {
space.world.remove(widget.mesh);
Widget.Groups.remove(widget);
}
return !widget.track.synth
});
}
function clearRanges() { function clearRanges() {
api.conf.get().process.ranges = []; api.conf.get().process.ranges = [];
ui.fdmRanges.innerHTML = ''; ui.fdmRanges.innerHTML = '';
@ -140,19 +122,15 @@ export function init() {
api.event.on("mode.set", mode => { api.event.on("mode.set", mode => {
isFdmMode = mode === 'FDM'; isFdmMode = mode === 'FDM';
lastMode = mode;
// hide/show fdm mode elements // hide/show fdm mode elements
for (let el of [...document.getElementsByClassName('mode-fdm')]) { for (let el of [...document.getElementsByClassName('mode-fdm')]) {
api.uc.setClass(el, 'hide', !isFdmMode); api.uc.setClass(el, 'hide', !isFdmMode);
} }
updateSupportVisiblity();
}); });
api.event.on("view.set", view => { api.event.on("view.set", view => {
$('top-slider').style.display = 'none'; $('top-slider').style.display = 'none';
lastView = view; lastView = view;
updateSupportVisiblity();
filterSynth();
func.sdone(); func.sdone();
// let ranges = api.conf.get().process.ranges; // let ranges = api.conf.get().process.ranges;
if (isFdmMode) { if (isFdmMode) {
@ -174,7 +152,6 @@ export function init() {
api.event.on("settings.load", (settings) => { api.event.on("settings.load", (settings) => {
if (settings.mode !== 'FDM') return; if (settings.mode !== 'FDM') return;
restoreSupports(api.widgets.all());
updateRanges(settings.process.ranges); updateRanges(settings.process.ranges);
}); });
@ -191,96 +168,89 @@ export function init() {
} }
}); });
api.event.on("fdm.supports.detect", func.sgen = () => { // auto-detect supports
func.sgen = () => {
let alerts = []; let alerts = [];
let { process, device } = api.conf.get();
if (!device.bedBelt) {
if (process.sliceSupportAngle < 10) {
alerts.push(api.show.alert("angles below 10 degrees may fail"));
}
}
alerts.push(api.show.alert("analyzing part(s)...", 1000)); alerts.push(api.show.alert("analyzing part(s)...", 1000));
support_generate(array => { setTimeout(() => api.hide.alert(undefined,alerts), 2000);
func.sclear();
api.hide.alert(undefined,alerts);
for (let rec of array) {
let { widget, supports } = rec;
let wa = api.widgets.annotate(widget.id);
let ws = wa.support || [];
for (let support of supports) {
let { from, to, mid } = support;
let dw = api.conf.get().process.sliceSupportSize / 2;
let dh = from.z - to.z;
let rec = {
x: mid.x,
y: mid.y,
z: mid.z,
dw,
dh,
id: Math.random() * 0xffffffffff
};
addWidgetSupport(widget, rec);
ws.push(Object.clone(rec));
} }
wa.support = ws;
}
api.event.emit("fdm.supports.detected");
});
});
api.event.on("fdm.supports.add", func.sadd = () => { // manual add supports
alert = api.show.alert("[esc] key cancels support editing"); func.sadd = () => {
if (addingSupports) {
return func.sdone();
}
alert = api.show.alert("[esc] key cancels support editing", 1000);
let down;
api.feature.hover = addingSupports = true; api.feature.hover = addingSupports = true;
}); api.feature.on_mouse_down = (int) => {
if (int) {
let { point } = int;
down = { point, widget: int.object.widget };
}
return api.widgets.meshes();
};
api.feature.on_mouse_drag = ({ int }) => {
if (int?.length) {
func.sadd_point(int[0].point, down.widget);
}
return [ down.widget.mesh ];
// return api.widgets.meshes();
};
api.widgets.each(w => {
let mat = w.cache.mat = w.mesh.material[0];
mat = w.mesh.material[0] = mat.clone();
mat.needsUpdate = true;
if (!w.anno.paint) {
w.anno.paint = [];
}
addPaintOverlayTexture(mat, w.anno.paint, new THREE.Color(0x4488ff));
})
api.space.update();
}
api.event.on("fdm.supports.done", func.sdone = () => { func.sadd_point = (point, widget) => {
delbox('intZ'); let { x, y, z } = point;
delbox('intW'); let rec = {
delbox('supp'); point: { x, y:-z, z:y },
api.hide.alert(alert); radius: 2
};
// return console.log({ on, widget, point });
let paint = widget.anno.paint;
paint.push(rec);
updatePaintOverlay(widget.mesh.material[0], paint);
api.space.update();
// api.conf.save();
};
// manual add done
func.sdone = () => {
if (!addingSupports) {
return;
}
api.feature.hover = addingSupports = false; api.feature.hover = addingSupports = false;
fromPillar = undefined; api.feature.on_mouse_down = undefined;
api.feature.on_mouse_drag = undefined;
api.hide.alert(alert);
api.widgets.each(w => {
w.mesh.material[0] = w.cache.mat;
delete w.cache.mat;
}); });
}
api.event.on("fdm.supports.clear", func.sclear = () => { // manual supports clear
func.sclear = () => {
func.sdone(); func.sdone();
if (clearAllWidgetSupports()) { api.widgets.each(w => clearWidgetSupports(w));
api.conf.save(); api.conf.save();
} }
});
api.event.on("slice.begin", () => { api.event.on("slice.begin", () => {
if (!isFdmMode) { if (!isFdmMode) {
return; return;
} }
func.sdone(); func.sdone();
updateSupportVisiblity();
// synth support widget for each widget group
// iow, create support meshes based on support widget annotation
for (let group of Widget.Groups.list()) {
let merge = group.filter(w => w.sups).map(w => Object.values(w.sups)).flat();
if (!merge.length) {
continue;
}
let boxen = merge.map(m => {
let geo = m.box.geometry.clone();
if (geo.index) geo = geo.toNonIndexed();
return geo.translate(m.x, m.y, m.z);
});
for (let box of boxen) {
// eliminate / normalize uv to allow other widget merge
box.setAttribute('uv',new BufferAttribute(new Float32Array(0), 3));
}
let bbg = BufferGeometryUtils.mergeGeometries(boxen);
let sw = newWidget(null, group);
let fwp = group[0].track.pos;
sw.loadGeometry(bbg);
sw._move(fwp.x, fwp.y, fwp.z);
api.widgets.add(sw);
sw.track.synth = true;
sw.track.support = true;
space.world.add(sw.mesh);
}
}); });
api.event.on("slice.end", () => { api.event.on("slice.end", () => {
@ -312,25 +282,12 @@ export function init() {
if (!isFdmMode) { if (!isFdmMode) {
return; return;
} }
let ann = api.widgets.annotate(widget.id);
if (ann.support) {
for (let supp of Object.values(ann.support)) {
supp.id = Math.abs(Math.random() * 0xffffffffffff);
}
restoreSupports([widget]);
}
}); });
api.event.on("widget.mirror", widget => { api.event.on("widget.mirror", widget => {
if (!isFdmMode) { if (!isFdmMode) {
return; return;
} }
let ann = api.widgets.annotate(widget.id);
let sups = ann.support || [];
sups.forEach(sup => {
let wsup = widget.sups[sup.id];
wsup.box.position.x = wsup.x = sup.x = -sup.x;
});
}); });
api.event.on("widget.rotate", rot => { api.event.on("widget.rotate", rot => {
@ -341,305 +298,30 @@ export function init() {
if (x || y) { if (x || y) {
clearWidgetSupports(widget); clearWidgetSupports(widget);
} else { } else {
let ann = api.widgets.annotate(widget.id); // todo rotate supports when it's only a Z rotation
let sups = ann.support || [];
sups.forEach(sup => {
let wsup = widget.sups[sup.id];
let vc = new Vector3(sup.x, sup.y, sup.z);
let m4 = new Matrix4();
m4 = m4.makeRotationFromEuler(new Euler(x || 0, y || 0, z || 0));
vc.applyMatrix4(m4);
wsup.box.position.x = wsup.x = sup.x = vc.x;
wsup.box.position.y = wsup.y = sup.y = vc.y;
wsup.box.position.z = wsup.z = sup.z = vc.z;
});
} }
}); });
api.event.on("mouse.hover.up", func.hoverup = (on = {}) => { api.event.on("mouse.hover.up", func.hoverup = (on = {}) => {
let { event } = on; if (!(addingSupports && isFdmMode)) {
if (!isFdmMode) {
return; return;
} }
if (!addingSupports) { let { object } = on;
return; if (object) {
let { point } = object;
let { widget } = object.object;
func.sadd_point(point, widget);
} }
delbox('supp');
if (lastPillar) {
removeWidgetSupport(lastPillar.widget, lastPillar);
return;
}
lastBox = getlastbox();
if (!(iw && lastBox)) return;
let { point, dim } = lastBox;
p1.y = Math.max(0, p1.y);
p2.y = Math.max(0, p2.y);
let rz = dim.rz;
let dh = dim.z;
let dw = api.conf.get().process.sliceSupportSize / 2;
let ip = iw.track.pos;
let wa = api.widgets.annotate(iw.id);
let ws = (wa.support = wa.support || []);
let id = ++nextID;
let rec = {x: point.x - ip.x, y: -point.z - ip.y, z: point.y, rz, dw, dh, id};
if (fromPillar && event && event.shiftKey) {
let targets = api.widgets.meshes().append(space.internals().platform);
let from = fromPillar,
d = {
x: rec.x - from.x,
y: rec.y - from.y,
z: rec.z - from.z
};
let dist = Math.sqrt(d.x*d.x + d.y*d.y + d.z*d.z);
let lerp = util.lerp(0,dist,dw).map(v => v/dist);
let angle = rec.rz = Math.atan2(d.y,d.x);
let fromrec = ws.filter(r => r.id === from.id)[0];
if (fromrec) {
from.rz = fromrec.rz = angle;
removeWidgetSupport(iw, fromPillar);
ws.push(Object.clone(fromrec));
addWidgetSupport(iw, fromrec);
}
lerp.pop();
let seed = Math.random();
for (let pct of lerp) {
let point = new Vector3(from.x + d.x * pct, from.z + d.z * pct, from.y + d.y * pct);
addSupportAtPoint(targets, point, ip, angle, seed++);
}
}
ws.push(Object.clone(rec));
fromPillar = addWidgetSupport(iw, rec);
api.conf.save();
}); });
function addSupportAtPoint(targets, point, ip, angle, id) {
let up = new Vector3(0,1,0)
let dn = new Vector3(0,-1,0)
let rp = new Vector3(point.x + ip.x, point.y, -point.z - ip.y);
let iup = new Raycaster(rp, up)
.intersectObjects(targets, false)
.filter(t => !t.object.pillar);
if (iup.length && iup.length % 2 === 0) {
let idn = new Raycaster(rp, dn)
.intersectObjects(targets, false)
.filter(t => !t.object.pillar);
if (idn.length) {
iw = iup[0].object.widget || iw;
let wa = api.widgets.annotate(iw.id);
let ws = (wa.support = wa.support || []);
let phi = iup[0].point;
let plo = idn[0].point;
let ply = Math.max(0, plo.y);
let mp = (phi.y + ply) / 2;
let dy = Math.abs(phi.y - ply);
let dw = api.conf.get().process.sliceSupportSize / 2;
let rec = { rz:angle, x:point.x, y:point.z, z:mp, dw, dh:dy, id };
ws.push(Object.clone(rec));
fromPillar = addWidgetSupport(iw, rec);
}
}
}
api.event.on("mouse.hover", data => { api.event.on("mouse.hover", data => {
if (!isFdmMode) { if (!(addingSupports && isFdmMode)) {
return; return;
} }
if (!addingSupports) {
return;
}
delbox('supp');
const { int, type, point, event } = data; const { int, type, point, event } = data;
const pillar = int ? int.object.pillar : undefined;
if (lastPillar) {
lastPillar.box.material.color.r = 0;
lastPillar = null;
}
if (pillar) {
pillar.box.material.color.r = 0.5;
lastPillar = pillar;
if (event && (event.metaKey || event.ctrlKey)) {
return func.hoverup();
}
return;
}
if (int && type === 'widget') {
iw = int.object.widget || iw;
} else {
iw = null;
}
p1 = point;
// looking up by default (assumes platform intersect)
let dir = new Vector3(0,1,0)
let ray = new Raycaster(point, dir);
let rz = int && int.face ? Math.atan2(int.face.normal.y, int.face.normal.x) : 0;
// when intersecting underside of object, project down
if (int && int.face && int.face.normal.z < -0.1) {
dir.y = -1;
}
let targets = api.widgets.meshes().append(space.internals().platform);
let i2 = ray.intersectObjects(targets, false)
.filter(t => !t.object.pillar) // eliminate other pillars
.filter(i => i.distance > 0.1); // false matches close to origin of ray
if (i2.length) {
p2 = i2[0].point;
iw = i2[0].object.widget || iw;
let p1y = Math.max(0, p1.y);
let p2y = Math.max(0, p2.y);
let hy = (p1y + p2y) / 2;
let dy = Math.abs(p1y - p2y);
let dw = api.conf.get().process.sliceSupportSize / 2;
addbox({x:p1.x, y:hy, z:p1.z}, 0x0000dd, 'supp', {
x:dw, y:dw, z:dy, rz
}); });
} else {
lastBox = null;
}
if (event && event.altKey) {
return func.hoverup();
}
});
api.event.on("export.debug", msg => {
if (msg.arcQ && msg.arcQ.length > 2) {
if (xpdebug) {
xpdebug.destroy();
xpdebug = undefined;
}
let poly = base.newPolygon().setOpen();
for (let rec of msg.arcQ) {
rec.z += 1;
poly.addObj(rec);
}
let layers = new Layers();
let layer = layers.setLayer("arcq", { line: 0xff00ff, face: 0xff00ff, opacity: 1 });
layer.addPoly(poly, {thin:true});
layer = layers.setLayer("arcc", { line: 0xff0000, face: 0xff0000, opacity: 1 });
for (let center of msg.arcQ.center) {
center.z += 0.5;
layer.addPoly(base.newPolygon().centerCircle(
center, center.r, 20
));
}
xpdebug = new Stack(space.world, false);
xpdebug.addLayers(layers);
xpdebug.setVisible(0,Infinity);
xpdebug.show();
space.refresh();
console.log(msg, poly);
}
});
}
let xpdebug;
let supsave = {};
let suptimer;
function support_generate(ondone) {
client.clear();
client.sync();
const settings = api.conf.get();
const widgets = api.widgets.map();
client.send("fdm_support_generate", { settings }, (gen) => {
if (gen && gen.error) {
api.show.alert('support generation canceled');
return ondone([]);
}
for (let g of gen) {
g.widget = widgets[g.id];
}
ondone(gen);
});
};
function scheduleSupportSave(widget) {
clearTimeout(suptimer);
supsave[widget.id] = widget;
suptimer = setTimeout(() => {
for (let w of Object.values(supsave)) {
w.saveState();
}
}, 50);
}
export function restoreSupports(widgets) {
widgets.forEach(widget => {
const supports = api.widgets.annotate(widget.id).support || [];
supports.forEach(pos => {
addWidgetSupport(widget, pos);
});
});
}
function addWidgetSupport(widget, pos) {
const { x, y, z, rz, dw, dh, id } = pos;
const sups = widget.sups = (widget.sups || {});
// prevent duplicate restore from repeated settings load calls
if (!sups[id]) {
pos.box = addbox(
{ x, y, z }, 0x0000dd, id,
{ x:dw, y:dw, z:dh, rz }, { group: widget.mesh }
);
pos.box.pillar = Object.assign({widget}, pos);
sups[id] = pos;
widget.adds.push(pos.box);
scheduleSupportSave(widget);
}
return sups[id];
}
function removeWidgetSupport(widget, rec) {
const { box, id } = rec;
widget.adds.remove(box);
widget.mesh.remove(box);
delete widget.sups[id];
let sa = api.widgets.annotate(widget.id).support;
let ix = 0;
sa.forEach((rec,i) => {
if (rec.id === id) {
ix = i;
}
});
sa.splice(ix,1);
api.conf.save();
scheduleSupportSave(widget);
fromPillar = undefined;
}
function updateSupportVisiblity() {
const visible = (lastMode === 'FDM' && lastView === VIEWS.ARRANGE);
for (let w of api.widgets.all()) {
setSupportVisiblity(w, visible);
}
}
function setSupportVisiblity(widget, bool) {
Object.values(widget.sups || {}).forEach(support => {
support.box.visible = bool;
});
}
function clearAllWidgetSupports() {
let cleared = 0;
api.widgets.all().forEach(widget => {
cleared += clearWidgetSupports(widget);
});
return cleared;
} }
function clearWidgetSupports(widget) { function clearWidgetSupports(widget) {
let cleared = 0; widget.anno.paint = [];
Object.values(widget.sups || {}).forEach(support => {
widget.adds.remove(support.box);
widget.mesh.remove(support.box);
cleared++;
});
widget.sups = {};
delete api.widgets.annotate(widget.id).support;
scheduleSupportSave(widget);
return cleared;
} }

View file

@ -19,7 +19,7 @@ slicer.slicePost.FDM = slicePost;
export function slicePost(data, options) { export function slicePost(data, options) {
const { groups, z } = data; const { groups, z } = data;
const { post_args, useAssembly, zIndexes } = options; const { post_args, useAssembly, zIndexes } = options;
const { isSynth, process, vaseMode } = post_args; const { process, vaseMode } = post_args;
const { compInner, compOuter, pump } = post_args; const { compInner, compOuter, pump } = post_args;
const { clipOffset, fillOffset, shellOffset } = post_args; const { clipOffset, fillOffset, shellOffset } = post_args;
@ -60,20 +60,6 @@ export function slicePost(data, options) {
data.tops = POLY.nest([...inner, ...outer]); data.tops = POLY.nest([...inner, ...outer]);
} }
if (isSynth) {
const process = post_args.process;
if (process.sliceSupportGrow > 0) {
// expand synth supports here so they can be clipped later
data.tops = POLY.expand(data.tops, process.sliceSupportGrow, data.z, []);
}
// do not shell synth widgets because
// they will be clipped against peers later
// which requires shelling post-clip
// but we still need to generate tops
delete data.groups;
return;
}
const index = zIndexes.indexOf(z); const index = zIndexes.indexOf(z);
const range = getRangeParameters(process, index); const range = getRangeParameters(process, index);
// calculate fractional shells // calculate fractional shells
@ -87,10 +73,10 @@ export function slicePost(data, options) {
let trg = shellFrac > 0.5 ? 1 : parts - 1; let trg = shellFrac > 0.5 ? 1 : parts - 1;
sliceShells += rem >= trg ? 1 : 0; sliceShells += rem >= trg ? 1 : 0;
} }
const count = isSynth ? 1 : sliceShells; const count = sliceShells;
const offset = shellOffset; const offset = shellOffset;
const fillOff = fillOffset; const fillOff = fillOffset;
const thinType = isSynth ? undefined : process.sliceDetectThin; const thinType = process.sliceDetectThin;
const nutops = []; const nutops = [];
// co-locate shell processing with top generation in slicer // co-locate shell processing with top generation in slicer
for (let top of data.tops) { for (let top of data.tops) {

View file

@ -9,7 +9,6 @@ import { newPolygon, Polygon } from '../../../geo/polygon.js';
import { polygons as POLY, fillArea } from '../../../geo/polygons.js'; import { polygons as POLY, fillArea } from '../../../geo/polygons.js';
import { newPrint } from '../../core/print.js'; import { newPrint } from '../../core/print.js';
import { newSlice } from '../../core/slice.js'; import { newSlice } from '../../core/slice.js';
import { newWidget } from '../../core/widget.js';
import { render } from '../../core/render.js'; import { render } from '../../core/render.js';
import { getRangeParameters } from './driver.js'; import { getRangeParameters } from './driver.js';
@ -116,7 +115,7 @@ export async function fdm_prepare(widgets, settings, update) {
brims = POLY.expand(brims, -extra, zheight, null, 1); brims = POLY.expand(brims, -extra, zheight, null, 1);
} }
// if raft is specified // create raft when specified
if (useRaft) { if (useRaft) {
let offset = newPoint(0,0,0), let offset = newPoint(0,0,0),
height = nozzle, height = nozzle,
@ -217,39 +216,6 @@ export async function fdm_prepare(widgets, settings, update) {
bounds.max.y = Math.max(bounds.max.y, bbounds.maxy); bounds.max.y = Math.max(bounds.max.y, bbounds.maxy);
} }
// synthesize support widgets when needed
// so that they can use a separate extruder
// compute zmin for belt purge towers
for (let widget of widgets.slice()) {
let sslices = [];
if (!widget.slices) {
console.log('invalid widget', widget);
continue;
}
for (let slice of widget.slices) {
zmin = Math.min(zmin, slice.z);
zmax = Math.max(zmax, slice.z);
if (!slice.supports) {
continue;
}
let sslice = newSlice(slice.z);
sslice.extruder = process.sliceSupportNozzle;
sslice.supports = slice.supports.slice();
sslice.height = slice.height;
sslices.push(sslice);
}
if (sslices.length) {
let swidget = newWidget(null,widget.group);
swidget.slices = sslices;
swidget.support = true;
swidget.belt = widget.belt;
swidget.track = Object.clone(widget.track);
swidget.mesh = { widget: swidget, position: swidget.track.pos };
widget.anno.extruder = process.sliceSupportNozzle;
widgets.push(swidget);
}
}
let lastPoly; let lastPoly;
let lastLayer; let lastLayer;
let lastOffset; let lastOffset;

View file

@ -127,16 +127,14 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
{ devel, assembly, threaded } = controller, { devel, assembly, threaded } = controller,
isBelt = device.bedBelt, isBelt = device.bedBelt,
isBrick = devel && process.sliceZInterleave, isBrick = devel && process.sliceZInterleave,
isSynth = widget.track.synth,
isSupport = widget.track.support,
useAssembly = assembly, useAssembly = assembly,
isConcurrent = threaded && minions.concurrent && !process.xray, isConcurrent = threaded && minions.concurrent && !process.xray,
topLayers = process.sliceTopLayers || 0, topLayers = process.sliceTopLayers || 0,
bottomLayers = process.sliceBottomLayers || 0, bottomLayers = process.sliceBottomLayers || 0,
vaseMode = process.sliceFillType === 'vase' && !isSynth, vaseMode = process.sliceFillType === 'vase',
metadata = widget.anno, metadata = widget.anno,
maxtruder = Math.max(0, device.extruders.length - 1), maxtruder = Math.max(0, device.extruders.length - 1),
extruder = Math.min(maxtruder, parseInt(isSynth ? process.sliceSupportNozzle : metadata.extruder || 0)), extruder = Math.min(maxtruder, parseInt(metadata.extruder || 0)),
sliceHeight = process.sliceHeight, sliceHeight = process.sliceHeight,
sliceHeightBase = (isBelt ? sliceHeight : process.firstSliceHeight) || sliceHeight, sliceHeightBase = (isBelt ? sliceHeight : process.firstSliceHeight) || sliceHeight,
lineWidth = process.sliceLineWidth || device.extruders[extruder].extNozzle, lineWidth = process.sliceLineWidth || device.extruders[extruder].extNozzle,
@ -233,7 +231,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
zMin: bounds.min.z, zMin: bounds.min.z,
zMax: bounds.max.z - zCut, zMax: bounds.max.z - zCut,
// support/synth usually has overlapping boxes // support/synth usually has overlapping boxes
union: controller.healMesh || isSynth, union: controller.healMesh,
indices: process.indices || process.xray, indices: process.indices || process.xray,
useAssembly, useAssembly,
post: 'FDM', post: 'FDM',
@ -245,7 +243,6 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
clipOffset, clipOffset,
lineWidth, lineWidth,
vaseMode, vaseMode,
isSynth,
process, process,
}, },
// z index generator // z index generator
@ -331,7 +328,6 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
}, },
// slicer function (worker local or minion distributed) // slicer function (worker local or minion distributed)
slicer(z, points, opts) { slicer(z, points, opts) {
// opts.debug = opts.debug || isSynth;
return (isConcurrent ? minions.sliceZ : sliceZ)(z, points, opts); return (isConcurrent ? minions.sliceZ : sliceZ)(z, points, opts);
}, },
onupdate(update) { onupdate(update) {
@ -347,7 +343,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
slice.height = heights[slice.index]; slice.height = heights[slice.index];
slice.clips = clip; slice.clips = clip;
// do not warn on merging supports // do not warn on merging supports
if (changes && !isSynth) { if (changes) {
healed = true; healed = true;
slice.changes = changes; slice.changes = changes;
if (devel) { if (devel) {
@ -431,49 +427,10 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
} }
// create shadow for non-belt supports // create shadow for non-belt supports
if (!isBelt && (isSynth || (!isSynth && supportDensity && process.sliceSupportEnable))) { if (!isBelt && supportDensity && process.sliceSupportEnable) {
await doShadow(slices); await doShadow(slices);
} }
// for synth support widgets, clip/offset to other widgets in group
if (isSynth) {
for (let slice of slices) {
let gap = sliceHeight * process.sliceSupportGap;
// clip tops to other widgets in group
let tops = slice.topPolys();
for (let peer of widget.group) {
// skip self
if (peer === widget || !peer.slices) {
continue;
}
for (let pslice of peer.slices) {
if (Math.abs(Math.abs(pslice.z - slice.z) - gap) > 0.1) {
continue;
}
let clipto = pslice.clips;
if (pslice.supportOutline) {
// merge support outlines into slice clips which
// should only happen when automatic and manual
// supports are used together
clipto.appendAll(pslice.supportOutline);
}
let ntops = [];
POLY.subtract(tops, clipto, ntops, null, slice.z, 0);
tops = ntops;
}
// trim to group's shadow if not in belt mode
if (!isBelt) {
tops = POLY.setZ(POLY.trimTo(tops, widget.shadow), slice.z);
}
}
slice.tops = [];
for (let t of tops) {
slice.addTop(t);
}
doShells(slice, 1, shellOffset / 2);
}
}
// calculate % complete and call onupdate() // calculate % complete and call onupdate()
function doupdate(index, from, to, msg) { function doupdate(index, from, to, msg) {
trackupdate(index / slices.length, from, to, msg); trackupdate(index / slices.length, from, to, msg);
@ -509,7 +466,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
let isTop = topLayers && slice.index > slices.length - topLayers - 1; let isTop = topLayers && slice.index > slices.length - topLayers - 1;
let isTopBase = isTop && slice.index === slices.length - topLayers; let isTopBase = isTop && slice.index === slices.length - topLayers;
let isDense = range.sliceFillSparse > 0.995; let isDense = range.sliceFillSparse > 0.995;
let isSolid = (isBottom || ((isTop || isDense) && !vaseMode)) && !isSynth; let isSolid = (isBottom || ((isTop || isDense) && !vaseMode));
let solidWidth = isSolid ? range.sliceFillWidth || 1 : 0; let solidWidth = isSolid ? range.sliceFillWidth || 1 : 0;
if (solidWidth) { if (solidWidth) {
let fillSpace = fillSpacing * solidWidth; let fillSpace = fillSpacing * solidWidth;
@ -526,7 +483,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
}, "solid layers"); }, "solid layers");
// add lead in anchor when specified in belt mode (but not for synths) // add lead in anchor when specified in belt mode (but not for synths)
if (isBelt && !isSynth) { if (isBelt) {
let { cosf, slope } = widget.belt; let { cosf, slope } = widget.belt;
// find adjusted zero point from slices // find adjusted zero point from slices
let smin = Infinity; let smin = Infinity;
@ -737,7 +694,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
// layer boolean diffs need to be computed to find flat areas to fill // layer boolean diffs need to be computed to find flat areas to fill
// and overhangs that need to be supported. these are stored in flats // and overhangs that need to be supported. these are stored in flats
// and bridges, projected up/down, and merged into an array of solids // and bridges, projected up/down, and merged into an array of solids
if (!vaseMode && !isSynth) { if (!vaseMode) {
profileStart("delta"); profileStart("delta");
forSlices(0.2, 0.34, slice => { forSlices(0.2, 0.34, slice => {
let params = slice.params || process; let params = slice.params || process;
@ -778,7 +735,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
// for "real" objects, fill the remaining voids with sparse fill // for "real" objects, fill the remaining voids with sparse fill
// sparse layers only present when non-vase mose and sparse % > 0 // sparse layers only present when non-vase mose and sparse % > 0
if (!isSynth && !vaseMode) { if (!vaseMode) {
let lastType; let lastType;
let promises = isConcurrent ? [] : undefined; let promises = isConcurrent ? [] : undefined;
forSlices(0.5, promises ? 0.55 : 0.7, slice => { forSlices(0.5, promises ? 0.55 : 0.7, slice => {
@ -817,44 +774,10 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
} }
} }
} }
} else if (isSynth && isSupport) {
// convert synth support widgets into support structure
let outline = process.sliceSupportOutline || false;
let promises = isConcurrent ? [] : undefined;
let resolve = [];
forSlices(0.5, promises ? 0.6 : 0.7, slice => {
let params = slice.params || process;
let density = params.sliceSupportDensity;
let supports = slice.topShells();
slice.supports = supports;
slice.tops = undefined; // remove outline leave only supports
let polys = [];
if (density) {
if (!outline) {
POLY.expand(supports, lineWidth, slice.z, polys);
} else {
POLY.expand(supports, -lineWidth/4, slice.z, polys);
}
}
fillSupportPolys({
promises, polys, lineWidth, density, z: slice.z, isBelt,
angle: process.sliceSupportFill
});
resolve.push({ slice, polys });
}, "infill");
if (promises) {
await tracker(promises, (i, t) => {
trackupdate(i / t, 0.6, 0.7);
});
}
for (let rec of resolve) {
let { slice, polys } = rec;
rec.supports = polys;
}
} }
// auto support generation // auto support generation
if (!isBelt && !isSynth && supportDensity && process.sliceSupportEnable) { if (!isBelt && supportDensity && process.sliceSupportEnable) {
doShadow(slices); doShadow(slices);
profileStart("support"); profileStart("support");
let promises = []; let promises = [];
@ -940,7 +863,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
if (render) { if (render) {
forSlices(0.9, 1.0, slice => { forSlices(0.9, 1.0, slice => {
let params = slice.params || process; let params = slice.params || process;
doRender(slice, isSynth, params, { doRender(slice, params, {
dark: controller.dark, dark: controller.dark,
devel: controller.devel devel: controller.devel
}); });
@ -999,7 +922,7 @@ function bound(v,min,max) {
return Math.max(min,Math.min(max,v)); return Math.max(min,Math.min(max,v));
} }
function doRender(slice, isSynth, params, opt = {}) { function doRender(slice, params, opt = {}) {
const { dark, devel } = opt; const { dark, devel } = opt;
const Color = dark ? COLOR_DARK : COLOR; const Color = dark ? COLOR_DARK : COLOR;
const output = slice.output(); const output = slice.output();
@ -1013,7 +936,7 @@ function doRender(slice, isSynth, params, opt = {}) {
.addPolys([top.poly]); .addPolys([top.poly]);
if (top.shells?.length) output if (top.shells?.length) output
.setLayer(isSynth ? "support" : "shells", isSynth ? Color.support : Color.shell) .setLayer("shells", Color.shell)
.addPolys(top.shells || [], vopt({ offset, height, clean: true })); .addPolys(top.shells || [], vopt({ offset, height, clean: true }));
if (isThin && top.thin_wall?.length) output if (isThin && top.thin_wall?.length) output
@ -1031,7 +954,7 @@ function doRender(slice, isSynth, params, opt = {}) {
), vopt({ offset, height })); ), vopt({ offset, height }));
if (top.fill_lines?.length) output if (top.fill_lines?.length) output
.setLayer("solid fill", isSynth ? Color.support : Color.fill) .setLayer("solid fill", Color.fill)
.addLines(top.fill_lines || [], vopt({ offset: offset * solidWidth, height, z:slice.z })); .addLines(top.fill_lines || [], vopt({ offset: offset * solidWidth, height, z:slice.z }));
if (!(slice.belt?.anchor)) output if (!(slice.belt?.anchor)) output
@ -1783,103 +1706,3 @@ function projectSolid(slice, polys, count, up, first) {
else projectSolid(slice.down, polys, count-1, false, false); else projectSolid(slice.down, polys, count-1, false, false);
} }
} }
export function supports(settings, widget) {
let isBelt = settings.device.bedBelt;
let process = settings.process;
let size = process.sliceSupportSize;
let s9 = size / 9;
let s4 = size / 4;
let s2 = size * 0.45;
let min = 0.01;
let geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(widget.vertices, 3));
let rad = (Math.PI / 180);
let deg = (180 / Math.PI);
let angle = rad * settings.process.sliceSupportAngle;
let thresh = -Math.sin(angle);
let dir = new THREE.Vector3(0,0,-1)
let add = [];
let mat = new THREE.MeshBasicMaterial();
let mesh = new THREE.Mesh(geo, mat);
let platform = new THREE.Mesh(
new THREE.PlaneGeometry(10000,10000,1), mat
);
const now = Date.now();
// test point
function tp(point) {
if (point.added) {
return;
}
// omit pillars close to existing pillars
for (let added of add) {
let p2 = new THREE.Vector2(point.x, point.y);
let pm = new THREE.Vector2(added.mid.x, added.mid.y);
if (Math.abs(point.z - added.from.z) < s2 && p2.distanceTo(pm) < s4) {
return;
}
}
let ray = new THREE.Raycaster(point, dir);
let int = ray.intersectObjects([ mesh, platform ], false);
if (int && int.length && int[0].distance > 0.5) {
let mid = new THREE.Vector3().add(point).add(int[0].point).divideScalar(2);
add.push({from: point, to: int[0].point, mid});
point.added = true;
}
}
function tf(a, b, c) {
let dab = a.distanceTo(b);
let dbc = b.distanceTo(c);
let dca = c.distanceTo(a);
let max = Math.max(dab, dbc, dca);
if (max < size) {
let min = Math.min(dab, dbc, dca);
if (min < s9 && Math.random() < 0.5) {
return;
}
// test midpoint of tri face
return tp(new THREE.Vector3().add(a).add(b).add(c).divideScalar(3));
}
if (dab === max) {
let mp = new THREE.Vector3().add(a).add(b).divideScalar(2);
tf(mp, b, c);
tf(a, mp, c);
} else if (dbc === max) {
let mp = new THREE.Vector3().add(b).add(c).divideScalar(2);
tf(a, mp, c);
tf(a, b, mp);
} else {
let mp = new THREE.Vector3().add(c).add(a).divideScalar(2);
tf(a, b, mp);
tf(mp, b, c);
}
}
let filter = isBelt ? (norm) => {
return norm.z <= thresh && norm.y < 0;
} : (norm) => {
return norm.z < thresh;
};
let { position } = geo.attributes;
let { itemSize, count, array } = position;
for (let i = 0; i<count; i += 3) {
let ip = i * itemSize;
let a = new THREE.Vector3(array[ip++], array[ip++], array[ip++]);
let b = new THREE.Vector3(array[ip++], array[ip++], array[ip++]);
let c = new THREE.Vector3(array[ip++], array[ip++], array[ip++]);
let norm = THREE.computeFaceNormal(a,b,c);
// limit to downward faces
if (!filter(norm)) {
continue;
}
// skip faces on bed
if (Math.max(a.z, b.z, c.z) < 0.1) {
continue;
}
// triangulate larger polys and test centers
tf(a,b,c);
}
console.log(`support generated in ${Date.now() - now} ms`);
widget.supports = add;
return add.length > 0;
}

View file

@ -969,7 +969,7 @@ function onMouseUp(event) {
function onMouseMove(event) { function onMouseMove(event) {
updateLastAction(); updateLastAction();
let int, vis; let int, vis, dragTrack;
const mv = new THREE.Vector2(); const mv = new THREE.Vector2();
mv.x = ( event.clientX / window.innerWidth ) * 2 - 1; mv.x = ( event.clientX / window.innerWidth ) * 2 - 1;
@ -990,13 +990,18 @@ function onMouseMove(event) {
platform.visible = vis; platform.visible = vis;
if (int && int.length > 0) platformHover(int[0].point); if (int && int.length > 0) platformHover(int[0].point);
} }
} else if (mouseDragPoint && mouseDrag && mouseDrag()) { } else if (mouseDragPoint && mouseDrag && (dragTrack = mouseDrag())) {
event.preventDefault(); event.preventDefault();
let trackTo = alignedTracking ? trackPlane : platform; let trackTo = alignedTracking ? trackPlane : platform;
let vis = trackTo.visible; let vis = trackTo.visible;
trackTo.visible = true; trackTo.visible = true;
if (dragTrack.length) {
int = intersect(dragTrack, false);
trackTo = dragTrack[0];
} else {
int = intersect([trackTo], false); int = intersect([trackTo], false);
trackTo.visible = vis; trackTo.visible = vis;
}
if (int.length > 0 && int[0].object === trackTo) { if (int.length > 0 && int[0].object === trackTo) {
let delta = mouseDragPoint.clone().sub(int[0].point); let delta = mouseDragPoint.clone().sub(int[0].point);
let offset = mouseDragStart.clone().sub(int[0].point); let offset = mouseDragStart.clone().sub(int[0].point);
@ -1010,7 +1015,7 @@ function onMouseMove(event) {
x: -offset.x, x: -offset.x,
y: offset.z, y: offset.z,
z: 0 z: 0
}); }, false, int);
requestRefresh(); requestRefresh();
} }
} }