Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions examples/visualizer/modules/loaders.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,82 @@

import * as THREE from 'three';
import { GCodeLoaderExtended } from '../libs/GCodeLoaderExtended.js';
import { loadSlicingSettings } from './state.js';

// File extensions
export const MODEL_EXTENSIONS = ['stl', 'obj', '3mf', 'amf', 'ply', 'gltf', 'glb', 'dae'];
export const GCODE_EXTENSIONS = ['gcode', 'gco', 'nc'];

// Fallback build plate dimensions (mm) for known printers when Polyslice is unavailable.
const PRINTER_BUILD_PLATES = {
Ender3: { width: 220, length: 220, height: 250 },
Ender3V2: { width: 220, length: 220, height: 250 },
Ender3Pro: { width: 220, length: 220, height: 250 },
PrusaI3MK3S: { width: 250, length: 210, height: 210 },
AnycubicI3Mega: { width: 210, length: 210, height: 205 },
UltimakerS5: { width: 330, length: 240, height: 300 },
BambuLabP1P: { width: 256, length: 256, height: 256 }
};

const DEFAULT_BUILD_PLATE_WIDTH = 220;
const DEFAULT_BUILD_PLATE_LENGTH = 220;
const DEFAULT_BUILD_PLATE_HEIGHT = 250;

/**
* Get the build plate dimensions for the currently selected printer.
* Tries to use window.Polyslice.Printer when available; falls back to a
* lookup table and then to the Ender3 defaults (220 × 220 × 250 mm).
*
* @returns {{ width: number, length: number, height: number }}
*/
export function getBuildPlateDimensions() {
try {
const savedSettings = loadSlicingSettings();
const printerName = (savedSettings && savedSettings.printer) ? savedSettings.printer : 'Ender3';

if (window.Polyslice?.Printer) {
const printer = new window.Polyslice.Printer(printerName);
return { width: printer.getSizeX(), length: printer.getSizeY(), height: printer.getSizeZ() };
}

const preset = PRINTER_BUILD_PLATES[printerName];
if (preset) {
return preset;
}
} catch (error) {
console.warn('Could not determine build plate dimensions:', error);
}

return { width: DEFAULT_BUILD_PLATE_WIDTH, length: DEFAULT_BUILD_PLATE_LENGTH, height: DEFAULT_BUILD_PLATE_HEIGHT };
}

/**
* Position a loaded mesh on the build plate by:
* 1. Placing its bottom face at Z = 0 (lay flat on the build plate).
* 2. Centering it in XY at (buildPlateWidth / 2, buildPlateLength / 2).
*
* This matches the positioning applied by the Polyslice slicer so that the
* model's visual location in the viewport corresponds to its printed location
* in the generated G-code.
*/
export function positionMeshOnBuildPlate(object) {
const { width: buildPlateWidth, length: buildPlateLength } = getBuildPlateDimensions();

const box = new THREE.Box3().setFromObject(object);
if (box.isEmpty()) {
console.warn('positionMeshOnBuildPlate: bounding box is empty, skipping positioning');
return;
}

const centerX = (box.min.x + box.max.x) / 2;
const centerY = (box.min.y + box.max.y) / 2;
const minZ = box.min.z;

object.position.x += (buildPlateWidth / 2) - centerX;
object.position.y += (buildPlateLength / 2) - centerY;
object.position.z += -minZ;
}

/**
* Handle file upload event.
*/
Expand Down Expand Up @@ -136,6 +207,10 @@ export function loadModel(file, scene, callbacks) {
export function displayMesh(object, filename, scene, callbacks) {
const { centerCamera, hideForkMeBanner, hideGCodeLegends, createSlicingGUI, updateMeshInfo } = callbacks;

// Center the model on the build plate and lay it flat at Z = 0 so its
// position matches the location it will occupy in the sliced G-code.
positionMeshOnBuildPlate(object);

scene.add(object);

// Update info panel
Expand Down
87 changes: 77 additions & 10 deletions examples/visualizer/modules/scene.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// Constants
// Default build volume dimensions (mm) – matches the Ender3 preset.
export const AXIS_LENGTH = 220;

// Scene state
Expand Down Expand Up @@ -69,16 +69,18 @@ export function initScene() {

/**
* Create custom axes with proper colors and thickness.
* @param {number} sizeX - Length of the X axis in mm.
* @param {number} sizeY - Length of the Y axis in mm.
* @param {number} sizeZ - Length of the Z axis in mm.
*/
function createAxes() {
function createAxes(sizeX = AXIS_LENGTH, sizeY = AXIS_LENGTH, sizeZ = AXIS_LENGTH) {

const axisLength = AXIS_LENGTH;
const axisThickness = 3;

// Create X axis (red)
const xGeometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(axisLength, 0, 0),
new THREE.Vector3(sizeX, 0, 0),
]);
const xMaterial = new THREE.LineBasicMaterial({
color: 0xff0000,
Expand All @@ -90,7 +92,7 @@ function createAxes() {
// Create Y axis (green)
const yGeometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0, axisLength, 0),
new THREE.Vector3(0, sizeY, 0),
]);
const yMaterial = new THREE.LineBasicMaterial({
color: 0x00ff00,
Expand All @@ -102,7 +104,7 @@ function createAxes() {
// Create Z axis (blue)
const zGeometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0, 0, axisLength),
new THREE.Vector3(0, 0, sizeZ),
]);
const zMaterial = new THREE.LineBasicMaterial({
color: 0x0000ff,
Expand All @@ -117,12 +119,11 @@ function createAxes() {

/**
* Create grid helper on the XY plane.
* @param {number} sizeX - Width of the grid in mm (X direction).
* @param {number} sizeY - Length of the grid in mm (Y direction).
*/
function createGridHelper() {
function createGridHelper(sizeX = AXIS_LENGTH, sizeY = AXIS_LENGTH) {

// Draw grid in X+ / Y+ quadrant on the XY plane (Z=0)
const sizeX = AXIS_LENGTH;
const sizeY = AXIS_LENGTH;
const divisions = 20;
const colorCenterLine = 0x888888;
const colorGrid = 0x444444;
Expand Down Expand Up @@ -163,6 +164,72 @@ function createGridHelper() {

}

/**
* Update the scene axes and grid to match new printer build volume dimensions.
* Updates the existing Three.js objects in-place so that all references held by
* event listeners (axis visibility toggles) continue to work correctly.
*
* @param {number} width - Build plate width in mm (X axis).
* @param {number} length - Build plate length in mm (Y axis).
* @param {number} height - Build volume height in mm (Z axis).
*/
export function updateBuildVolume(width, length, height) {

if (axesLines) {
// Update each axis line's endpoint position in-place.
const xPos = axesLines[0].geometry.attributes.position;
xPos.setXYZ(1, width, 0, 0);
xPos.needsUpdate = true;

const yPos = axesLines[1].geometry.attributes.position;
yPos.setXYZ(1, 0, length, 0);
yPos.needsUpdate = true;

const zPos = axesLines[2].geometry.attributes.position;
zPos.setXYZ(1, 0, 0, height);
zPos.needsUpdate = true;
}

if (gridHelper) {
// Dispose existing child geometries and remove them from the group.
while (gridHelper.children.length > 0) {
const child = gridHelper.children[0];
child.geometry.dispose();
gridHelper.remove(child);
}

// Rebuild the grid for the new build plate dimensions.
const divisions = 20;
const colorCenterLine = 0x888888;
const colorGrid = 0x444444;

const materialCenter = new THREE.LineBasicMaterial({ color: colorCenterLine });
const materialGrid = new THREE.LineBasicMaterial({ color: colorGrid });

const stepX = width / divisions;
const stepY = length / divisions;

for (let x = 0; x <= width + 0.001; x += stepX) {
const geom = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(x, 0, 0),
new THREE.Vector3(x, length, 0),
]);
const isCenter = Math.abs(x) < 1e-6;
gridHelper.add(new THREE.Line(geom, isCenter ? materialCenter : materialGrid));
}

for (let y = 0; y <= length + 0.001; y += stepY) {
const geom = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(0, y, 0),
new THREE.Vector3(width, y, 0),
]);
const isCenter = Math.abs(y) < 1e-6;
gridHelper.add(new THREE.Line(geom, isCenter ? materialCenter : materialGrid));
}
}

}

/**
* Handle window resize.
*/
Expand Down
13 changes: 11 additions & 2 deletions examples/visualizer/modules/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export function createMoveSlider() {
/**
* Create the slicing GUI for loaded 3D models.
*/
export function createSlicingGUI(sliceCallback, useDefaults = false, rotateCallback = null) {
export function createSlicingGUI(sliceCallback, useDefaults = false, rotateCallback = null, repositionCallback = null) {

let slicingGUI = window.slicingGUI;

Expand Down Expand Up @@ -195,8 +195,17 @@ export function createSlicingGUI(sliceCallback, useDefaults = false, rotateCallb
rotateCallback('z', params.rotationZ);
}

// Re-center after all three initial rotations have been applied.
if (repositionCallback) {
repositionCallback();
}

h = trackFolder('Printer & Filament', slicingGUI.addFolder('Printer & Filament'), true);
h.add(params, 'printer', PRINTER_OPTIONS).name('Printer').onChange(() => saveSlicingSettings(params));
h.add(params, 'printer', PRINTER_OPTIONS).name('Printer').onChange(() => {
saveSlicingSettings(params);
// Re-center the mesh for the newly selected printer's build plate dimensions.
if (repositionCallback) repositionCallback();
});
h.add(params, 'filament', FILAMENT_OPTIONS).name('Filament').onChange(() => saveSlicingSettings(params));
h.add(params, 'nozzleTemperature', 150, 300, 5).name('Nozzle Temp (°C)').onFinishChange(() => saveSlicingSettings(params));
h.add(params, 'bedTemperature', 0, 120, 5).name('Bed Temp (°C)').onFinishChange(() => saveSlicingSettings(params));
Expand Down
43 changes: 37 additions & 6 deletions examples/visualizer/visualizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { ColladaLoader } from 'three/addons/loaders/ColladaLoader.js';
import { TransformControls } from 'three/addons/controls/TransformControls.js';

// Import modules
import { initScene, scene, camera, renderer, controls, axesLines, gridHelper, onWindowResize, animate } from './modules/scene.js';
import { initScene, scene, camera, renderer, controls, axesLines, gridHelper, onWindowResize, animate, updateBuildVolume } from './modules/scene.js';
import {
createLegend,
createLayerSlider,
Expand Down Expand Up @@ -51,7 +51,9 @@ import {
loadModel,
displayMesh as displayMeshHelper,
loadGCode as loadGCodeHelper,
updateMeshInfo
updateMeshInfo,
positionMeshOnBuildPlate,
getBuildPlateDimensions
} from './modules/loaders.js';
import {
setupLayerSlider as setupLayerSliderHelper,
Expand Down Expand Up @@ -178,6 +180,8 @@ function radiansToDegrees(radians) {

/**
* Sync the TransformControls current rotation back to the slicing GUI sliders.
* Called on every objectChange event during a gizmo drag; does NOT reposition
* the mesh (repositioning happens once at drag-end via dragging-changed).
*/
function syncTransformToSliders() {
const mesh = transformControls?.object;
Expand Down Expand Up @@ -214,9 +218,19 @@ function initTransformControls() {

// Disable orbit controls while the user is dragging the rotation gizmo.
// Track that a drag occurred so the pointerup handler can ignore it.
// When the drag ends (event.value === false), reposition the mesh on the
// build plate so the final G-code position and the viewport position match.
transformControls.addEventListener('dragging-changed', (event) => {
controls.enabled = !event.value;
if (event.value) wasDraggingGizmo = true;
if (event.value) {
wasDraggingGizmo = true;
} else {
// Drag just ended – reposition once so the viewport matches the slicer.
const mesh = transformControls.object;
if (mesh) {
positionMeshOnBuildPlate(mesh);
}
}
});

// Sync drag interactions back to the GUI sliders.
Expand Down Expand Up @@ -262,12 +276,27 @@ function initTransformControls() {
}

/**
* Apply a rotation in degrees to one axis of a mesh.
* Apply a rotation in degrees to one axis of a mesh and re-center the mesh on
* the build plate so the displayed position continues to match the sliced G-code.
*/
function applyMeshRotation(mesh, axis, degrees) {
if (!mesh) return;
mesh.rotation[axis] = degrees * Math.PI / 180;
mesh.updateMatrixWorld(true);
positionMeshOnBuildPlate(mesh);
}

/**
* Re-position the currently loaded mesh on the build plate and update the
* scene axes and grid to reflect the selected printer's build volume.
* Called when the printer selection changes.
*/
function repositionMesh() {
if (meshObject) {
positionMeshOnBuildPlate(meshObject);
}
const { width, length, height } = getBuildPlateDimensions();
updateBuildVolume(width, length, height);
}

/**
Expand Down Expand Up @@ -298,7 +327,8 @@ function loadModelWrapper(file) {
sliceModel(loadedModelForSlicing, currentFilename, loadGCodeWrapper);
},
false,
(axis, degrees) => applyMeshRotation(meshObject, axis, degrees)
(axis, degrees) => applyMeshRotation(meshObject, axis, degrees),
repositionMesh
);
},
updateMeshInfo
Expand Down Expand Up @@ -499,7 +529,8 @@ function resetView() {
createSlicingGUI(
() => sliceModel(loadedModelForSlicing, currentFilename, loadGCodeWrapper),
true,
(axis, degrees) => applyMeshRotation(meshObject, axis, degrees)
(axis, degrees) => applyMeshRotation(meshObject, axis, degrees),
repositionMesh
);
}

Expand Down
Loading