checkpoint material painting
This commit is contained in:
parent
c31c723392
commit
dff3cf9b1e
14 changed files with 267 additions and 712 deletions
|
|
@ -177,7 +177,7 @@ export function init_input() {
|
|||
space.sky.showGrid(false);
|
||||
space.sky.setColor(controller.dark ? 0 : 0xffffff);
|
||||
space.setAntiAlias(controller.antiAlias);
|
||||
space.init(container, function (delta) {
|
||||
space.init(container, (delta) => {
|
||||
const { lo, hi, max } = slider.getRange();
|
||||
if (max === 0 || !delta) return;
|
||||
if (controller.reverseZoom) delta = -delta;
|
||||
|
|
@ -471,7 +471,7 @@ export function init_input() {
|
|||
DOC.body.requestFullscreen();
|
||||
} catch (e) {
|
||||
event.emit('resize');
|
||||
moto.space.event.onResize();
|
||||
space.event.onResize();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -113,10 +113,7 @@ class InteractionControl {
|
|||
this.#space.mouse.downSelect((int, event) => {
|
||||
// Feature hook for custom mouse down handling
|
||||
if (this.#api.feature.on_mouse_down) {
|
||||
if (int) {
|
||||
this.#api.feature.on_mouse_down(int, event);
|
||||
return;
|
||||
}
|
||||
return this.#api.feature.on_mouse_down(int, event);
|
||||
}
|
||||
|
||||
// Hover mode handling
|
||||
|
|
@ -178,9 +175,10 @@ class InteractionControl {
|
|||
});
|
||||
|
||||
// Drag handler
|
||||
this.#space.mouse.onDrag((delta, offset, up = false) => {
|
||||
this.#space.mouse.onDrag((delta, offset, up = false, int) => {
|
||||
if (this.#api.feature.hover) {
|
||||
return;
|
||||
return this.#api.feature?.on_mouse_drag({ delta, offset, up, int });
|
||||
// return;
|
||||
}
|
||||
|
||||
if (up) {
|
||||
|
|
|
|||
|
|
@ -71,21 +71,45 @@ import { THREE } from '../../ext/three.js';
|
|||
* @returns {THREE.Material} The modified material
|
||||
*/
|
||||
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) => {
|
||||
// Store shader reference for later updates
|
||||
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)
|
||||
const points = paintPoints.map(p =>
|
||||
new THREE.Vector4(p.point.x, p.point.y, p.point.z, p.radius)
|
||||
);
|
||||
// Filter out invalid entries
|
||||
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
|
||||
shader.uniforms.paintPoints = { value: points };
|
||||
shader.uniforms.paintCount = { value: points.length };
|
||||
shader.uniforms.paintCount = { value: validPoints.length };
|
||||
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(
|
||||
`#include <worldpos_vertex>`,
|
||||
`
|
||||
|
|
@ -128,9 +152,14 @@ function addPaintOverlaySimple(material, paintPoints, paintColor = new THREE.Col
|
|||
}
|
||||
|
||||
if (painted) {
|
||||
// Blend paint color with existing fragment color
|
||||
gl_FragColor.rgb = mix(gl_FragColor.rgb, paintColor, 0.5);
|
||||
// Blend paint color with existing fragment color (increased to 0.7 for visibility)
|
||||
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
|
||||
*/
|
||||
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) => {
|
||||
// Store shader reference for later updates
|
||||
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
|
||||
// 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 texHeight = Math.ceil(pointCount / texWidth);
|
||||
const data = new Float32Array(texWidth * texHeight * 4);
|
||||
|
||||
// Pack paint points into texture data
|
||||
// Each point = 4 floats (x, y, z, radius)
|
||||
for (let i = 0; i < paintPoints.length; i++) {
|
||||
const p = paintPoints[i];
|
||||
for (let i = 0; i < validPoints.length; i++) {
|
||||
const p = validPoints[i];
|
||||
data[i * 4 + 0] = p.point.x;
|
||||
data[i * 4 + 1] = p.point.y;
|
||||
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.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(
|
||||
`#include <worldpos_vertex>`,
|
||||
`
|
||||
|
|
@ -245,9 +290,14 @@ function addPaintOverlayTexture(material, paintPoints, paintColor = new THREE.Co
|
|||
}
|
||||
|
||||
if (painted) {
|
||||
// Blend paint color with existing fragment color
|
||||
gl_FragColor.rgb = mix(gl_FragColor.rgb, paintColor, 0.5);
|
||||
// Blend paint color with existing fragment color (increased to 0.7 for visibility)
|
||||
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
|
||||
* 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 {Array<{point: {x,y,z}, radius: number}>} paintPoints - Updated array of paint spheres
|
||||
*/
|
||||
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
|
||||
// After onBeforeCompile runs once, userData.shader contains the compiled program
|
||||
const shader = material.userData?.shader;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -315,13 +381,20 @@ function updatePaintOverlaySimple(shader, paintPoints) {
|
|||
const uniforms = shader.uniforms;
|
||||
|
||||
// Convert paint points to vec4 array (x, y, z, radius)
|
||||
const points = paintPoints.map(p =>
|
||||
new THREE.Vector4(p.point.x, p.point.y, p.point.z, p.radius)
|
||||
);
|
||||
// Filter out invalid entries
|
||||
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
|
||||
uniforms.paintPoints.value = points;
|
||||
uniforms.paintCount.value = points.length;
|
||||
uniforms.paintCount.value = validPoints.length;
|
||||
|
||||
// Note: No need to set needsUpdate on the material
|
||||
// Uniform updates are automatically detected by THREE.js
|
||||
|
|
@ -334,15 +407,18 @@ function updatePaintOverlaySimple(shader, paintPoints) {
|
|||
function updatePaintOverlayTexture(shader, paintPoints) {
|
||||
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
|
||||
const pointCount = paintPoints.length;
|
||||
const pointCount = validPoints.length;
|
||||
const texWidth = Math.ceil(Math.sqrt(pointCount));
|
||||
const texHeight = Math.ceil(pointCount / texWidth);
|
||||
const data = new Float32Array(texWidth * texHeight * 4);
|
||||
|
||||
// Pack paint points into texture data
|
||||
for (let i = 0; i < paintPoints.length; i++) {
|
||||
const p = paintPoints[i];
|
||||
for (let i = 0; i < validPoints.length; i++) {
|
||||
const p = validPoints[i];
|
||||
data[i * 4 + 0] = p.point.x;
|
||||
data[i * 4 + 1] = p.point.y;
|
||||
data[i * 4 + 2] = p.point.z;
|
||||
|
|
|
|||
|
|
@ -49,11 +49,11 @@ class Widget extends WidgetCore {
|
|||
}
|
||||
const widget = this;
|
||||
index().put('ws-save-'+this.id, {
|
||||
anno: this.annotations(),
|
||||
geo: widget.getGeoVertices({ unroll: false }).slice(),
|
||||
track: widget.track,
|
||||
group: this.group.id,
|
||||
meta: this.meta,
|
||||
anno: this.annotations()
|
||||
track: widget.track
|
||||
}, result => {
|
||||
widget.meta.saved = Date.now();
|
||||
if (ondone) ondone();
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ let debug = self.debug === true,
|
|||
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 {Function} onreply function to call on reply messages
|
||||
* @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
|
||||
running[seq] = { fn:onreply };
|
||||
}
|
||||
|
||||
let msg = {
|
||||
seq: seq,
|
||||
task: fn,
|
||||
time: time(),
|
||||
data: data
|
||||
};
|
||||
|
||||
// console.log('client send', msg);
|
||||
// if (!fn) { console.trace({empty_fn: data}) };
|
||||
|
||||
try {
|
||||
worker.postMessage(msg, zerocopy);
|
||||
} 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
|
||||
export const client = {
|
||||
send,
|
||||
|
||||
sendp,
|
||||
|
||||
setWorkPath(path) {
|
||||
workpath = path;
|
||||
return client;
|
||||
|
|
@ -269,7 +281,7 @@ export const client = {
|
|||
ondone(null, reply.error);
|
||||
}
|
||||
if (reply.debug) {
|
||||
api.event.emit("export.debug", reply.debug);
|
||||
console.log('worker debug', reply.debug);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ function save(quiet) {
|
|||
api.conf.save();
|
||||
const newWidgets = [];
|
||||
const oldWidgets = js2o(SDB.getItem('ws-widgets'), []);
|
||||
api.widgets.each(function(widget) {
|
||||
api.widgets.each(widget => {
|
||||
if (widget.synth) return;
|
||||
newWidgets.push(widget.id);
|
||||
oldWidgets.remove(widget.id);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ class Widget {
|
|||
|
||||
this.id = id || Date.now().toString(36)+(nextId++);
|
||||
this.grouped = group ? true : false;
|
||||
// persisted
|
||||
this.group = group || [];
|
||||
this.group.push(this);
|
||||
if (!this.group.id) {
|
||||
|
|
@ -33,10 +34,8 @@ class Widget {
|
|||
}
|
||||
// rotation stack (for undo)
|
||||
this.roto = [];
|
||||
// added meshes (supports, tabs, etc)
|
||||
// overlay meshes (supports, tabs, etc)
|
||||
this.adds = [];
|
||||
// persisted client annotations (cam tabs, fdm supports)
|
||||
this.anno = {};
|
||||
// THREE Mesh and points
|
||||
this.mesh = null;
|
||||
this.points = null;
|
||||
|
|
@ -48,6 +47,24 @@ class Widget {
|
|||
this.settings = null; // used??
|
||||
this.modified = 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 = {
|
||||
// box size for packer
|
||||
box: {
|
||||
|
|
@ -74,21 +91,7 @@ class Widget {
|
|||
mirror: false,
|
||||
indexed: false,
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
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_export } from './export.js';
|
||||
import { codec } from '../../core/codec.js';
|
||||
|
|
@ -24,15 +24,15 @@ function extrudeMM(dist, perMM, factor) {
|
|||
// defer loading until client and worker exist
|
||||
function init(worker) {
|
||||
|
||||
worker.dispatch.fdm_support_generate = function(data, send) {
|
||||
const { settings } = data;
|
||||
const widgets = Object.values(worker.cache);
|
||||
const fresh = widgets.filter(widget => supports(settings, widget));
|
||||
send.done(codec.encode(fresh.map(widget => { return {
|
||||
id: widget.id,
|
||||
supports: widget.supports,
|
||||
} } )));
|
||||
};
|
||||
// worker.dispatch.fdm_support_generate = function(data, send) {
|
||||
// const { settings } = data;
|
||||
// const widgets = Object.values(worker.cache);
|
||||
// const fresh = widgets.filter(widget => supports(settings, widget));
|
||||
// send.done(codec.encode(fresh.map(widget => { return {
|
||||
// id: widget.id,
|
||||
// supports: widget.supports,
|
||||
// } } )));
|
||||
// };
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ function optSelected(sel) {
|
|||
return opt ? opt.value : undefined;
|
||||
}
|
||||
|
||||
function manualSupport() {
|
||||
return api.conf.proc().sliceSupportType === 'manual';
|
||||
}
|
||||
|
||||
function hasInfill() {
|
||||
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)}),
|
||||
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" }),
|
||||
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"),
|
||||
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)}),
|
||||
|
|
@ -127,16 +131,16 @@ export function menu() {
|
|||
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}),
|
||||
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([
|
||||
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 }),
|
||||
separator: newBlank({ class:"set-sep", driven }),
|
||||
], { modes: FDM, class: "ext-buttons f-row grow", show:manualSupport }),
|
||||
// separator: newBlank({ class:"set-sep", driven }),
|
||||
sliceSupportManual: newRow([
|
||||
(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.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" }),
|
||||
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}),
|
||||
|
|
|
|||
|
|
@ -3,24 +3,16 @@
|
|||
import { $ } from '../../../moto/webui.js';
|
||||
import { api } from '../../app/api.js';
|
||||
import { conf } from '../../app/conf/defaults.js';
|
||||
import { util } from '../../../geo/base.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';
|
||||
import { addPaintOverlayTexture, updatePaintOverlay } from '../../app/paint.js';
|
||||
|
||||
const { Matrix4, Vector3, BufferAttribute, BufferGeometryUtils, Raycaster, Euler } = THREE;
|
||||
const { VIEWS } = api.const;
|
||||
|
||||
const LANG = api.language.current;
|
||||
|
||||
let lastBox, lastMode, lastView, lastPillar, fromPillar,
|
||||
let lastView,
|
||||
addingSupports = false,
|
||||
isFdmMode = false,
|
||||
nextID = Date.now(),
|
||||
p1, p2, iw,
|
||||
alert = [],
|
||||
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() {
|
||||
api.conf.get().process.ranges = [];
|
||||
ui.fdmRanges.innerHTML = '';
|
||||
|
|
@ -140,19 +122,15 @@ export function init() {
|
|||
|
||||
api.event.on("mode.set", mode => {
|
||||
isFdmMode = mode === 'FDM';
|
||||
lastMode = mode;
|
||||
// hide/show fdm mode elements
|
||||
for (let el of [...document.getElementsByClassName('mode-fdm')]) {
|
||||
api.uc.setClass(el, 'hide', !isFdmMode);
|
||||
}
|
||||
updateSupportVisiblity();
|
||||
});
|
||||
|
||||
api.event.on("view.set", view => {
|
||||
$('top-slider').style.display = 'none';
|
||||
lastView = view;
|
||||
updateSupportVisiblity();
|
||||
filterSynth();
|
||||
func.sdone();
|
||||
// let ranges = api.conf.get().process.ranges;
|
||||
if (isFdmMode) {
|
||||
|
|
@ -174,7 +152,6 @@ export function init() {
|
|||
|
||||
api.event.on("settings.load", (settings) => {
|
||||
if (settings.mode !== 'FDM') return;
|
||||
restoreSupports(api.widgets.all());
|
||||
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 { 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));
|
||||
support_generate(array => {
|
||||
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));
|
||||
setTimeout(() => api.hide.alert(undefined,alerts), 2000);
|
||||
}
|
||||
wa.support = ws;
|
||||
}
|
||||
api.event.emit("fdm.supports.detected");
|
||||
});
|
||||
});
|
||||
|
||||
api.event.on("fdm.supports.add", func.sadd = () => {
|
||||
alert = api.show.alert("[esc] key cancels support editing");
|
||||
// manual add supports
|
||||
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.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 = () => {
|
||||
delbox('intZ');
|
||||
delbox('intW');
|
||||
delbox('supp');
|
||||
api.hide.alert(alert);
|
||||
func.sadd_point = (point, widget) => {
|
||||
let { x, y, z } = point;
|
||||
let rec = {
|
||||
point: { x, y:-z, z:y },
|
||||
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;
|
||||
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();
|
||||
if (clearAllWidgetSupports()) {
|
||||
api.widgets.each(w => clearWidgetSupports(w));
|
||||
api.conf.save();
|
||||
}
|
||||
});
|
||||
|
||||
api.event.on("slice.begin", () => {
|
||||
if (!isFdmMode) {
|
||||
return;
|
||||
}
|
||||
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", () => {
|
||||
|
|
@ -312,25 +282,12 @@ export function init() {
|
|||
if (!isFdmMode) {
|
||||
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 => {
|
||||
if (!isFdmMode) {
|
||||
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 => {
|
||||
|
|
@ -341,305 +298,30 @@ export function init() {
|
|||
if (x || y) {
|
||||
clearWidgetSupports(widget);
|
||||
} else {
|
||||
let ann = api.widgets.annotate(widget.id);
|
||||
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;
|
||||
});
|
||||
// todo rotate supports when it's only a Z rotation
|
||||
}
|
||||
});
|
||||
|
||||
api.event.on("mouse.hover.up", func.hoverup = (on = {}) => {
|
||||
let { event } = on;
|
||||
if (!isFdmMode) {
|
||||
if (!(addingSupports && isFdmMode)) {
|
||||
return;
|
||||
}
|
||||
if (!addingSupports) {
|
||||
return;
|
||||
let { object } = on;
|
||||
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 => {
|
||||
if (!isFdmMode) {
|
||||
if (!(addingSupports && isFdmMode)) {
|
||||
return;
|
||||
}
|
||||
if (!addingSupports) {
|
||||
return;
|
||||
}
|
||||
delbox('supp');
|
||||
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) {
|
||||
let cleared = 0;
|
||||
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;
|
||||
widget.anno.paint = [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ slicer.slicePost.FDM = slicePost;
|
|||
export function slicePost(data, options) {
|
||||
const { groups, z } = data;
|
||||
const { post_args, useAssembly, zIndexes } = options;
|
||||
const { isSynth, process, vaseMode } = post_args;
|
||||
const { process, vaseMode } = post_args;
|
||||
const { compInner, compOuter, pump } = post_args;
|
||||
const { clipOffset, fillOffset, shellOffset } = post_args;
|
||||
|
||||
|
|
@ -60,20 +60,6 @@ export function slicePost(data, options) {
|
|||
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 range = getRangeParameters(process, index);
|
||||
// calculate fractional shells
|
||||
|
|
@ -87,10 +73,10 @@ export function slicePost(data, options) {
|
|||
let trg = shellFrac > 0.5 ? 1 : parts - 1;
|
||||
sliceShells += rem >= trg ? 1 : 0;
|
||||
}
|
||||
const count = isSynth ? 1 : sliceShells;
|
||||
const count = sliceShells;
|
||||
const offset = shellOffset;
|
||||
const fillOff = fillOffset;
|
||||
const thinType = isSynth ? undefined : process.sliceDetectThin;
|
||||
const thinType = process.sliceDetectThin;
|
||||
const nutops = [];
|
||||
// co-locate shell processing with top generation in slicer
|
||||
for (let top of data.tops) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import { newPolygon, Polygon } from '../../../geo/polygon.js';
|
|||
import { polygons as POLY, fillArea } from '../../../geo/polygons.js';
|
||||
import { newPrint } from '../../core/print.js';
|
||||
import { newSlice } from '../../core/slice.js';
|
||||
import { newWidget } from '../../core/widget.js';
|
||||
import { render } from '../../core/render.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);
|
||||
}
|
||||
|
||||
// if raft is specified
|
||||
// create raft when specified
|
||||
if (useRaft) {
|
||||
let offset = newPoint(0,0,0),
|
||||
height = nozzle,
|
||||
|
|
@ -217,39 +216,6 @@ export async function fdm_prepare(widgets, settings, update) {
|
|||
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 lastLayer;
|
||||
let lastOffset;
|
||||
|
|
|
|||
|
|
@ -127,16 +127,14 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
|
|||
{ devel, assembly, threaded } = controller,
|
||||
isBelt = device.bedBelt,
|
||||
isBrick = devel && process.sliceZInterleave,
|
||||
isSynth = widget.track.synth,
|
||||
isSupport = widget.track.support,
|
||||
useAssembly = assembly,
|
||||
isConcurrent = threaded && minions.concurrent && !process.xray,
|
||||
topLayers = process.sliceTopLayers || 0,
|
||||
bottomLayers = process.sliceBottomLayers || 0,
|
||||
vaseMode = process.sliceFillType === 'vase' && !isSynth,
|
||||
vaseMode = process.sliceFillType === 'vase',
|
||||
metadata = widget.anno,
|
||||
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,
|
||||
sliceHeightBase = (isBelt ? sliceHeight : process.firstSliceHeight) || sliceHeight,
|
||||
lineWidth = process.sliceLineWidth || device.extruders[extruder].extNozzle,
|
||||
|
|
@ -233,7 +231,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
|
|||
zMin: bounds.min.z,
|
||||
zMax: bounds.max.z - zCut,
|
||||
// support/synth usually has overlapping boxes
|
||||
union: controller.healMesh || isSynth,
|
||||
union: controller.healMesh,
|
||||
indices: process.indices || process.xray,
|
||||
useAssembly,
|
||||
post: 'FDM',
|
||||
|
|
@ -245,7 +243,6 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
|
|||
clipOffset,
|
||||
lineWidth,
|
||||
vaseMode,
|
||||
isSynth,
|
||||
process,
|
||||
},
|
||||
// z index generator
|
||||
|
|
@ -331,7 +328,6 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
|
|||
},
|
||||
// slicer function (worker local or minion distributed)
|
||||
slicer(z, points, opts) {
|
||||
// opts.debug = opts.debug || isSynth;
|
||||
return (isConcurrent ? minions.sliceZ : sliceZ)(z, points, opts);
|
||||
},
|
||||
onupdate(update) {
|
||||
|
|
@ -347,7 +343,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
|
|||
slice.height = heights[slice.index];
|
||||
slice.clips = clip;
|
||||
// do not warn on merging supports
|
||||
if (changes && !isSynth) {
|
||||
if (changes) {
|
||||
healed = true;
|
||||
slice.changes = changes;
|
||||
if (devel) {
|
||||
|
|
@ -431,49 +427,10 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
|
|||
}
|
||||
|
||||
// create shadow for non-belt supports
|
||||
if (!isBelt && (isSynth || (!isSynth && supportDensity && process.sliceSupportEnable))) {
|
||||
if (!isBelt && supportDensity && process.sliceSupportEnable) {
|
||||
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()
|
||||
function doupdate(index, 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 isTopBase = isTop && slice.index === slices.length - topLayers;
|
||||
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;
|
||||
if (solidWidth) {
|
||||
let fillSpace = fillSpacing * solidWidth;
|
||||
|
|
@ -526,7 +483,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
|
|||
}, "solid layers");
|
||||
|
||||
// add lead in anchor when specified in belt mode (but not for synths)
|
||||
if (isBelt && !isSynth) {
|
||||
if (isBelt) {
|
||||
let { cosf, slope } = widget.belt;
|
||||
// find adjusted zero point from slices
|
||||
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
|
||||
// and overhangs that need to be supported. these are stored in flats
|
||||
// and bridges, projected up/down, and merged into an array of solids
|
||||
if (!vaseMode && !isSynth) {
|
||||
if (!vaseMode) {
|
||||
profileStart("delta");
|
||||
forSlices(0.2, 0.34, slice => {
|
||||
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
|
||||
// sparse layers only present when non-vase mose and sparse % > 0
|
||||
if (!isSynth && !vaseMode) {
|
||||
if (!vaseMode) {
|
||||
let lastType;
|
||||
let promises = isConcurrent ? [] : undefined;
|
||||
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
|
||||
if (!isBelt && !isSynth && supportDensity && process.sliceSupportEnable) {
|
||||
if (!isBelt && supportDensity && process.sliceSupportEnable) {
|
||||
doShadow(slices);
|
||||
profileStart("support");
|
||||
let promises = [];
|
||||
|
|
@ -940,7 +863,7 @@ export function fdm_slice(settings, widget, onupdate, ondone) {
|
|||
if (render) {
|
||||
forSlices(0.9, 1.0, slice => {
|
||||
let params = slice.params || process;
|
||||
doRender(slice, isSynth, params, {
|
||||
doRender(slice, params, {
|
||||
dark: controller.dark,
|
||||
devel: controller.devel
|
||||
});
|
||||
|
|
@ -999,7 +922,7 @@ function bound(v,min,max) {
|
|||
return Math.max(min,Math.min(max,v));
|
||||
}
|
||||
|
||||
function doRender(slice, isSynth, params, opt = {}) {
|
||||
function doRender(slice, params, opt = {}) {
|
||||
const { dark, devel } = opt;
|
||||
const Color = dark ? COLOR_DARK : COLOR;
|
||||
const output = slice.output();
|
||||
|
|
@ -1013,7 +936,7 @@ function doRender(slice, isSynth, params, opt = {}) {
|
|||
.addPolys([top.poly]);
|
||||
|
||||
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 }));
|
||||
|
||||
if (isThin && top.thin_wall?.length) output
|
||||
|
|
@ -1031,7 +954,7 @@ function doRender(slice, isSynth, params, opt = {}) {
|
|||
), vopt({ offset, height }));
|
||||
|
||||
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 }));
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -969,7 +969,7 @@ function onMouseUp(event) {
|
|||
|
||||
function onMouseMove(event) {
|
||||
updateLastAction();
|
||||
let int, vis;
|
||||
let int, vis, dragTrack;
|
||||
|
||||
const mv = new THREE.Vector2();
|
||||
mv.x = ( event.clientX / window.innerWidth ) * 2 - 1;
|
||||
|
|
@ -990,13 +990,18 @@ function onMouseMove(event) {
|
|||
platform.visible = vis;
|
||||
if (int && int.length > 0) platformHover(int[0].point);
|
||||
}
|
||||
} else if (mouseDragPoint && mouseDrag && mouseDrag()) {
|
||||
} else if (mouseDragPoint && mouseDrag && (dragTrack = mouseDrag())) {
|
||||
event.preventDefault();
|
||||
let trackTo = alignedTracking ? trackPlane : platform;
|
||||
let vis = trackTo.visible;
|
||||
trackTo.visible = true;
|
||||
if (dragTrack.length) {
|
||||
int = intersect(dragTrack, false);
|
||||
trackTo = dragTrack[0];
|
||||
} else {
|
||||
int = intersect([trackTo], false);
|
||||
trackTo.visible = vis;
|
||||
}
|
||||
if (int.length > 0 && int[0].object === trackTo) {
|
||||
let delta = mouseDragPoint.clone().sub(int[0].point);
|
||||
let offset = mouseDragStart.clone().sub(int[0].point);
|
||||
|
|
@ -1010,7 +1015,7 @@ function onMouseMove(event) {
|
|||
x: -offset.x,
|
||||
y: offset.z,
|
||||
z: 0
|
||||
});
|
||||
}, false, int);
|
||||
requestRefresh();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue