import _get from 'lodash/get';
import _each from 'lodash/each';
import _isEqual from 'lodash/isEqual';
import _tail from 'lodash/tail';
import _throttle from 'lodash/throttle';
import colornames from 'colornames';
import pubsub from 'pubsub-js';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import * as THREE from 'three';
import {
IMPERIAL_UNITS,
METRIC_UNITS
} from 'app/constants';
import CombinedCamera from 'app/lib/three/CombinedCamera';
import TrackballControls from 'app/lib/three/TrackballControls';
import * as WebGL from 'app/lib/three/WebGL';
import log from 'app/lib/log';
import { getRenderPixelRatio } from 'app/lib/pixel-ratio';
import { mapValueToUnits } from 'app/lib/units';
import store from 'app/store';
import { getBoundingBox, loadSTL, loadTexture } from './helpers';
import Viewport from './Viewport';
import CoordinateAxes from './CoordinateAxes';
import Cuboid from './Cuboid';
import CuttingPointer from './CuttingPointer';
import GridLine from './GridLine';
import PivotPoint3 from './PivotPoint3';
import TextSprite from './TextSprite';
import GCodeVisualizer from './GCodeVisualizer';
import ProbeVisualization from './ProbeVisualization';
import {
CAMERA_MODE_PAN,
CAMERA_MODE_ROTATE
} from './constants';
const IMPERIAL_GRID_COUNT = 32;
const IMPERIAL_GRID_SPACING = 25.4;
const IMPERIAL_AXIS_LENGTH = IMPERIAL_GRID_SPACING * 12;
const METRIC_GRID_COUNT = 60;
const METRIC_GRID_SPACING = 10;
const METRIC_AXIS_LENGTH = METRIC_GRID_SPACING * 30;
const CAMERA_VIEWPORT_WIDTH = 300;
const CAMERA_VIEWPORT_HEIGHT = 300;
const PERSPECTIVE_FOV = 70;
const PERSPECTIVE_NEAR = 0.001;
const PERSPECTIVE_FAR = 2000;
const ORTHOGRAPHIC_FOV = 35;
const ORTHOGRAPHIC_NEAR = 0.001;
const ORTHOGRAPHIC_FAR = 2000;
const CAMERA_DISTANCE = 200;
const TRACKBALL_CONTROLS_MIN_DISTANCE = 1;
const TRACKBALL_CONTROLS_MAX_DISTANCE = 2000;
class Visualizer extends Component {
static propTypes = {
show: PropTypes.bool,
cameraPosition: PropTypes.oneOf(['top', '3d', 'front', 'left', 'right']),
state: PropTypes.object
};
pubsubTokens = [];
isAgitated = false;
machinePosition = {
x: 0,
y: 0,
z: 0
};
workPosition = {
x: 0,
y: 0,
z: 0
};
machineProfile = null;
group = new THREE.Group();
probeVisualization = null;
pivotPoint = new PivotPoint3({ x: 0, y: 0, z: 0 }, (x, y, z) => {
_each(this.group.children, (o) => {
o.translateX(x);
o.translateY(y);
o.translateZ(z);
});
});
node = null;
setRef = (node) => {
this.node = node;
};
throttledResize = _throttle(() => {
this.resizeRenderer();
}, 32);
changeMachineProfile = () => {
const machineProfile = store.get('workspace.machineProfile');
if (_isEqual(machineProfile, this.machineProfile)) {
return;
}
this.machineProfile = machineProfile ? { ...machineProfile } : null;
if (!machineProfile) {
if (!this.gcodeVisualizer) {
this.pivotPoint.set(0, 0, 0);
}
this.updateCuttingToolPosition();
this.updateCuttingPointerPosition();
this.updateLimitsPosition();
this.updateProbeVisualizationPosition();
if (this.group) {
this.rebuildCoordinateSystems();
}
this.updateScene();
return;
}
if (this.limits) {
this.group.remove(this.limits);
this.limits = null;
}
const state = this.props.state;
const limits = _get(this.machineProfile, 'limits');
const { xmin = 0, xmax = 0, ymin = 0, ymax = 0, zmin = 0, zmax = 0 } = { ...limits };
this.limits = this.createLimits(xmin, xmax, ymin, ymax, zmin, zmax);
this.limits.name = 'Limits';
this.limits.visible = state.objects.limits.visible;
this.group.add(this.limits);
if (!this.gcodeVisualizer) {
const centerX = (xmin + xmax) / 2;
const centerY = (ymin + ymax) / 2;
this.pivotPoint.set(centerX, centerY, 0);
}
this.updateCuttingToolPosition();
this.updateCuttingPointerPosition();
this.updateLimitsPosition();
this.updateProbeVisualizationPosition();
if (this.group) {
this.rebuildCoordinateSystems();
}
this.updateScene();
};
renderAnimationLoop = () => {
if (this.isAgitated) {
requestAnimationFrame(this.renderAnimationLoop);
const rpm = 600;
this.rotateCuttingTool(rpm);
} else {
const rpm = 0;
this.rotateCuttingTool(rpm);
}
this.updateScene();
};
constructor(props) {
super(props);
this.renderer = null;
this.scene = null;
this.camera = null;
this.controls = null;
this.viewport = null;
this.cuttingTool = null;
this.cuttingPointer = null;
this.limits = null;
this.gcodeVisualizer = null;
}
componentDidMount() {
this.subscribe();
this.addResizeEventListener();
store.on('change', this.changeMachineProfile);
if (this.node) {
const el = ReactDOM.findDOMNode(this.node);
this.createScene(el);
this.resizeRenderer();
}
this.changeMachineProfile();
}
componentDidUpdate(prevProps) {
let forceUpdate = false;
let needUpdateScene = false;
const prevState = prevProps.state;
const state = this.props.state;
if ((prevProps.show !== this.props.show) && (this.props.show === true)) {
this.viewport.update();
forceUpdate = true;
needUpdateScene = true;
}
if (this.gcodeVisualizer) {
const frameIndex = state.gcode.sent;
this.gcodeVisualizer.setFrameIndex(frameIndex);
}
if (prevState.projection !== state.projection) {
if (state.projection === 'orthographic') {
this.camera.toOrthographic();
this.camera.setZoom(1);
this.camera.setFov(ORTHOGRAPHIC_FOV);
} else {
this.camera.toPerspective();
this.camera.setZoom(1);
this.camera.setFov(PERSPECTIVE_FOV);
}
if (this.viewport) {
this.viewport.update();
}
needUpdateScene = true;
}
if (prevState.cameraMode !== state.cameraMode) {
this.setCameraMode(state.cameraMode);
needUpdateScene = true;
}
if ((prevState.units !== state.units) ||
(prevState.objects.coordinateSystem.visible !== state.objects.coordinateSystem.visible)) {
const visible = state.objects.coordinateSystem.visible;
const imperialCoordinateSystem = this.group.getObjectByName('ImperialCoordinateSystem');
if (imperialCoordinateSystem) {
imperialCoordinateSystem.visible = visible && (state.units === IMPERIAL_UNITS);
}
const metricCoordinateSystem = this.group.getObjectByName('MetricCoordinateSystem');
if (metricCoordinateSystem) {
metricCoordinateSystem.visible = visible && (state.units === METRIC_UNITS);
}
needUpdateScene = true;
}
if ((prevState.units !== state.units) ||
(prevState.objects.gridLineNumbers.visible !== state.objects.gridLineNumbers.visible)) {
const visible = state.objects.gridLineNumbers.visible;
const imperialGridLineNumbers = this.group.getObjectByName('ImperialGridLineNumbers');
if (imperialGridLineNumbers) {
imperialGridLineNumbers.visible = visible && (state.units === IMPERIAL_UNITS);
}
const metricGridLineNumbers = this.group.getObjectByName('MetricGridLineNumbers');
if (metricGridLineNumbers) {
metricGridLineNumbers.visible = visible && (state.units === METRIC_UNITS);
}
needUpdateScene = true;
}
if (this.limits && (this.limits.visible !== state.objects.limits.visible)) {
this.limits.visible = state.objects.limits.visible;
needUpdateScene = true;
}
if (this.cuttingTool && this.cuttingPointer && (this.cuttingTool.visible !== state.objects.cuttingTool.visible)) {
this.cuttingTool.visible = state.objects.cuttingTool.visible;
this.cuttingPointer.visible = !state.objects.cuttingTool.visible;
needUpdateScene = true;
}
{
let needUpdatePosition = false;
const { x: mpox0, y: mpoy0, z: mpoz0 } = this.machinePosition;
const { x: mpox1, y: mpoy1, z: mpoz1 } = state.machinePosition;
if (mpox0 !== mpox1 || mpoy0 !== mpoy1 || mpoz0 !== mpoz1) {
this.machinePosition = state.machinePosition;
needUpdatePosition = true;
needUpdateScene = true;
}
const { x: wpox0, y: wpoy0, z: wpoz0 } = this.workPosition;
const { x: wpox1, y: wpoy1, z: wpoz1 } = state.workPosition;
if (wpox0 !== wpox1 || wpoy0 !== wpoy1 || wpoz0 !== wpoz1) {
this.workPosition = state.workPosition;
needUpdatePosition = true;
needUpdateScene = true;
}
if (needUpdatePosition) {
this.updateCuttingToolPosition();
this.updateCuttingPointerPosition();
this.updateLimitsPosition();
this.updateProbeVisualizationPosition();
}
}
if (needUpdateScene) {
this.updateScene({ forceUpdate: forceUpdate });
}
if (this.isAgitated !== state.isAgitated) {
this.isAgitated = state.isAgitated;
if (this.isAgitated) {
requestAnimationFrame(this.renderAnimationLoop);
}
}
if (prevProps.cameraPosition !== this.props.cameraPosition) {
if (this.props.cameraPosition === 'top') {
this.toTopView();
}
if (this.props.cameraPosition === '3d') {
this.to3DView();
}
if (this.props.cameraPosition === 'front') {
this.toFrontView();
}
if (this.props.cameraPosition === 'left') {
this.toLeftSideView();
}
if (this.props.cameraPosition === 'right') {
this.toRightSideView();
}
}
}
componentWillUnmount() {
this.unsubscribe();
this.removeResizeEventListener();
store.removeListener('change', this.changeMachineProfile);
this.clearScene();
}
subscribe() {
const tokens = [
pubsub.subscribe('resize', (msg) => {
this.resizeRenderer();
}),
pubsub.subscribe('autolevel:showProbeVisualization', (msg, data) => {
this.showProbeVisualization(data);
}),
pubsub.subscribe('autolevel:hideProbeVisualization', (msg) => {
this.hideProbeVisualization();
}),
pubsub.subscribe('autolevel:updateProbeVisualization', (msg, data) => {
log.info('[Visualizer] Received updateProbeVisualization event:', data);
if (this.probeVisualization && typeof this.probeVisualization.updateBounds === 'function') {
const { startX, startY, endX, endY, snapX, snapY, interactable } = data.config;
log.info('[Visualizer] Updating bounds to:', { startX, startY, endX, endY, snapX, snapY, interactable });
if (this.probeVisualization.config) {
if (snapX !== undefined) {
this.probeVisualization.config.snapX = snapX;
}
if (snapY !== undefined) {
this.probeVisualization.config.snapY = snapY;
}
}
this.probeVisualization.updateBounds(startX, startY, endX, endY);
if (typeof this.probeVisualization.setInteractable === 'function' && interactable !== undefined) {
this.probeVisualization.setInteractable(interactable);
}
this.updateScene({ forceUpdate: true });
log.debug('[Visualizer] Updated probe visualization bounds from AutoLevel');
} else {
log.warn('[Visualizer] Cannot update bounds - probeVisualization or updateBounds not available');
}
})
];
this.pubsubTokens = this.pubsubTokens.concat(tokens);
}
unsubscribe() {
this.pubsubTokens.forEach((token) => {
pubsub.unsubscribe(token);
});
this.pubsubTokens = [];
}
showProbeVisualization(data) {
const { probeData = [], config = {} } = data;
log.debug('[Visualizer] showProbeVisualization', { probeData: probeData.length, config });
if (!this.probeVisualization) {
this.probeVisualization = new ProbeVisualization(
probeData,
config,
this.camera,
this.renderer.domElement,
this.controls,
() => this.updateScene({ forceUpdate: true })
);
this.probeVisualization.group.visible = false;
this.group.add(this.probeVisualization.group);
log.info('[Visualizer] Created and added probe visualization to group');
} else {
const { startX, startY, endX, endY, snapX, snapY, interactable, units } = config;
if (this.probeVisualization.config) {
if (snapX !== undefined) {
this.probeVisualization.config.snapX = snapX;
}
if (snapY !== undefined) {
this.probeVisualization.config.snapY = snapY;
}
if (units !== undefined) {
this.probeVisualization.config.units = units;
}
if (interactable !== undefined) {
this.probeVisualization.config.interactable = interactable;
}
}
if (typeof this.probeVisualization.updateProbeData === 'function') {
this.probeVisualization.updateProbeData(probeData);
}
if (typeof this.probeVisualization.updateBounds === 'function') {
this.probeVisualization.updateBounds(startX, startY, endX, endY);
}
if (typeof this.probeVisualization.recreateInteractiveElements === 'function') {
this.probeVisualization.recreateInteractiveElements();
}
if (typeof this.probeVisualization.setInteractable === 'function') {
this.probeVisualization.setInteractable(interactable !== undefined ? interactable : false);
}
}
this.updateProbeVisualizationPosition();
this.probeVisualization.group.visible = true;
this.updateScene({ forceUpdate: true });
}
hideProbeVisualization() {
if (this.probeVisualization) {
log.debug('[Visualizer] hideProbeVisualization');
this.probeVisualization.group.visible = false;
this.updateScene({ forceUpdate: true });
}
}
hasVerticalScrollbar() {
return window.innerWidth > document.documentElement.clientWidth;
}
hasHorizontalScrollbar() {
return window.innerHeight > document.documentElement.clientHeight;
}
getScrollbarWidth() {
const inner = document.createElement('p');
inner.style.width = '100%';
inner.style.height = '200px';
const outer = document.createElement('div');
outer.style.position = 'absolute';
outer.style.top = '0px';
outer.style.left = '0px';
outer.style.visibility = 'hidden';
outer.style.width = '200px';
outer.style.height = '150px';
outer.style.overflow = 'hidden';
outer.appendChild(inner);
document.body.appendChild(outer);
const w1 = inner.offsetWidth;
outer.style.overflow = 'scroll';
const w2 = (w1 === inner.offsetWidth) ? outer.clientWidth : inner.offsetWidth;
document.body.removeChild(outer);
return (w1 - w2);
}
getVisibleWidth() {
const el = ReactDOM.findDOMNode(this.node);
const visibleWidth = Math.max(
Number(el && el.parentNode && el.parentNode.clientWidth) || 0,
360
);
return visibleWidth;
}
getVisibleHeight() {
const clientHeight = document.documentElement.clientHeight;
const navbarHeight = 50;
const widgetHeaderHeight = 38;
const widgetFooterHeight = 38;
const visibleHeight = (
clientHeight - navbarHeight - widgetHeaderHeight - widgetFooterHeight - 1
);
return visibleHeight;
}
addResizeEventListener() {
window.addEventListener('resize', this.throttledResize);
}
removeResizeEventListener() {
window.removeEventListener('resize', this.throttledResize);
}
resizeRenderer() {
if (!(this.camera && this.renderer)) {
return;
}
const width = this.getVisibleWidth();
const height = this.getVisibleHeight();
if (width === 0 || height === 0) {
log.warn(`The width (${width}) and height (${height}) cannot be a zero value`);
}
this.camera.setSize(width, height);
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
if (!this.viewport) {
this.viewport = new Viewport(this.camera, CAMERA_VIEWPORT_WIDTH, CAMERA_VIEWPORT_HEIGHT);
}
this.controls.handleResize();
this.renderer.setPixelRatio(getRenderPixelRatio());
this.renderer.setSize(width, height);
this.updateScene();
}
createLimits(xmin, xmax, ymin, ymax, zmin, zmax) {
const dx = Math.abs(xmax - xmin) || Number.MIN_VALUE;
const dy = Math.abs(ymax - ymin) || Number.MIN_VALUE;
const dz = Math.abs(zmax - zmin) || Number.MIN_VALUE;
const color = colornames('indianred');
const opacity = 0.5;
const transparent = true;
const dashed = true;
const dashSize = 3;
const gapSize = 1;
const linewidth = 1;
const scale = 1;
const limits = new Cuboid({
dx,
dy,
dz,
color,
opacity,
transparent,
linewidth,
dashed,
dashSize,
gapSize,
scale,
});
return limits;
}
getCoordinateBounds(units) {
const gridSpacing = (units === IMPERIAL_UNITS) ? IMPERIAL_GRID_SPACING : METRIC_GRID_SPACING;
const limits = _get(this.machineProfile, 'limits');
const { xmin = 0, xmax = 0, ymin = 0, ymax = 0, zmin = 0, zmax = 0 } = { ...limits };
const hasMachineProfile = (xmax - xmin) > 0 || (ymax - ymin) > 0;
if (hasMachineProfile) {
return {
minX: xmin,
maxX: xmax,
minY: ymin,
maxY: ymax,
minZ: zmin,
maxZ: zmax,
gridSpacing,
};
}
const gridCount = (units === IMPERIAL_UNITS) ? IMPERIAL_GRID_COUNT : METRIC_GRID_COUNT;
const axisLength = (units === IMPERIAL_UNITS) ? IMPERIAL_AXIS_LENGTH : METRIC_AXIS_LENGTH;
const size = gridCount * gridSpacing;
return {
minX: -size,
maxX: size,
minY: -size,
maxY: size,
minZ: -axisLength,
maxZ: axisLength,
gridSpacing,
};
}
createCoordinateSystem(units) {
const { minX, maxX, minY, maxY, minZ, maxZ, gridSpacing } = this.getCoordinateBounds(units);
const labelOffset = gridSpacing * 2;
const group = new THREE.Group();
{
const gridLine = new GridLine(minX, maxX, gridSpacing, minY, maxY, gridSpacing, colornames('blue'), colornames('gray 44'));
_each(gridLine.children, (o) => {
o.material.opacity = 0.15;
o.material.transparent = true;
o.material.depthWrite = false;
});
gridLine.name = 'GridLine';
group.add(gridLine);
}
{
const coordinateAxes = new CoordinateAxes({ minX, maxX, minY, maxY, minZ, maxZ });
coordinateAxes.name = 'CoordinateAxes';
group.add(coordinateAxes);
}
{
const axisXLabel = new TextSprite({
x: maxX + labelOffset,
y: 0,
z: 0,
size: 20,
text: 'X',
color: colornames('red')
});
const axisYLabel = new TextSprite({
x: 0,
y: maxY + labelOffset,
z: 0,
size: 20,
text: 'Y',
color: colornames('green')
});
const axisZLabel = new TextSprite({
x: 0,
y: 0,
z: maxZ + labelOffset,
size: 20,
text: 'Z',
color: colornames('blue')
});
group.add(axisXLabel);
group.add(axisYLabel);
group.add(axisZLabel);
}
return group;
}
createGridLineNumbers(units) {
const { minX, maxX, minY, maxY, gridSpacing } = this.getCoordinateBounds(units);
const textSize = (units === IMPERIAL_UNITS) ? (25.4 / 3) : (10 / 3);
const textOffset = (units === IMPERIAL_UNITS) ? (25.4 / 5) : (10 / 5);
const group = new THREE.Group();
for (let x = minX; x <= maxX; x += gridSpacing) {
if (x !== 0) {
group.add(new TextSprite({
x,
y: textOffset,
z: 0,
size: textSize,
text: mapValueToUnits(x, units),
textAlign: 'center',
textBaseline: 'bottom',
color: colornames('red'),
opacity: 0.5
}));
}
}
for (let y = minY; y <= maxY; y += gridSpacing) {
if (y !== 0) {
group.add(new TextSprite({
x: -textOffset,
y,
z: 0,
size: textSize,
text: mapValueToUnits(y, units),
textAlign: 'right',
textBaseline: 'middle',
color: colornames('green'),
opacity: 0.5
}));
}
}
return group;
}
rebuildCoordinateSystems() {
const { state } = this.props;
const { units, objects } = state;
['ImperialCoordinateSystem', 'MetricCoordinateSystem',
'ImperialGridLineNumbers', 'MetricGridLineNumbers'].forEach(name => {
const obj = this.group.getObjectByName(name);
if (obj) {
this.group.remove(obj);
}
});
const pp = this.pivotPoint.get();
const positionAtPivot = (group) => {
group.position.set(-pp.x, -pp.y, -pp.z);
};
const imperialCoordinateSystem = this.createCoordinateSystem(IMPERIAL_UNITS);
imperialCoordinateSystem.name = 'ImperialCoordinateSystem';
imperialCoordinateSystem.visible = objects.coordinateSystem.visible && (units === IMPERIAL_UNITS);
positionAtPivot(imperialCoordinateSystem);
this.group.add(imperialCoordinateSystem);
const metricCoordinateSystem = this.createCoordinateSystem(METRIC_UNITS);
metricCoordinateSystem.name = 'MetricCoordinateSystem';
metricCoordinateSystem.visible = objects.coordinateSystem.visible && (units === METRIC_UNITS);
positionAtPivot(metricCoordinateSystem);
this.group.add(metricCoordinateSystem);
const imperialGridLineNumbers = this.createGridLineNumbers(IMPERIAL_UNITS);
imperialGridLineNumbers.name = 'ImperialGridLineNumbers';
imperialGridLineNumbers.visible = objects.gridLineNumbers.visible && (units === IMPERIAL_UNITS);
positionAtPivot(imperialGridLineNumbers);
this.group.add(imperialGridLineNumbers);
const metricGridLineNumbers = this.createGridLineNumbers(METRIC_UNITS);
metricGridLineNumbers.name = 'MetricGridLineNumbers';
metricGridLineNumbers.visible = objects.gridLineNumbers.visible && (units === METRIC_UNITS);
positionAtPivot(metricGridLineNumbers);
this.group.add(metricGridLineNumbers);
}
createScene(el) {
if (!el) {
return;
}
const { state } = this.props;
const { units, objects } = state;
const width = this.getVisibleWidth();
const height = this.getVisibleHeight();
this.renderer = new THREE.WebGLRenderer({
autoClearColor: true,
antialias: true,
alpha: true
});
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
this.renderer.setClearColor(new THREE.Color(colornames('white')), 1);
this.renderer.setPixelRatio(getRenderPixelRatio());
this.renderer.setSize(width, height);
this.renderer.clear();
el.appendChild(this.renderer.domElement);
this.scene = new THREE.Scene();
this.camera = this.createCombinedCamera(width, height);
this.controls = this.createTrackballControls(this.camera, this.renderer.domElement);
this.setCameraMode(state.cameraMode);
if (state.projection === 'orthographic') {
this.camera.toOrthographic();
this.camera.setZoom(1);
this.camera.setFov(ORTHOGRAPHIC_FOV);
} else {
this.camera.toPerspective();
this.camera.setZoom(1);
this.camera.setFov(PERSPECTIVE_FOV);
}
{
const color = 0xffffff;
const intensity = 1;
let light;
light = new THREE.DirectionalLight(color, intensity);
light.position.set(-1, -1, 1);
this.scene.add(light);
light = new THREE.DirectionalLight(color, intensity);
light.position.set(1, -1, 1);
this.scene.add(light);
}
{
const light = new THREE.AmbientLight(colornames('gray 25'));
this.scene.add(light);
}
this.rebuildCoordinateSystems();
{
Promise.all([
loadSTL('assets/models/stl/bit.stl').then(geometry => geometry),
loadTexture('assets/textures/brushed-steel-texture.jpg').then(texture => texture),
]).then(result => {
const [geometry, texture] = result;
geometry.rotateX(-Math.PI / 2);
geometry.scale(0.5, 0.5, 0.5);
geometry.computeBoundingBox();
const height = geometry.boundingBox.max.z - geometry.boundingBox.min.z;
geometry.translate(0, 0, (height / 2));
let material;
if (geometry.hasColors) {
material = new THREE.MeshLambertMaterial({
map: texture,
opacity: 0.9,
transparent: false
});
}
const object = new THREE.Object3D();
object.add(new THREE.Mesh(geometry, material));
this.cuttingTool = object;
this.cuttingTool.name = 'CuttingTool';
this.cuttingTool.visible = objects.cuttingTool.visible;
this.group.add(this.cuttingTool);
this.updateCuttingToolPosition();
this.updateScene();
});
}
{
this.cuttingPointer = new CuttingPointer({
color: colornames('indianred'),
diameter: 2
});
this.cuttingPointer.name = 'CuttingPointer';
this.cuttingPointer.visible = !objects.cuttingTool.visible;
this.group.add(this.cuttingPointer);
}
{
const limits = _get(this.machineProfile, 'limits');
const { xmin = 0, xmax = 0, ymin = 0, ymax = 0, zmin = 0, zmax = 0 } = { ...limits };
this.limits = this.createLimits(xmin, xmax, ymin, ymax, zmin, zmax);
this.limits.name = 'Limits';
this.limits.visible = objects.limits.visible;
this.group.add(this.limits);
this.updateLimitsPosition();
}
{
const defaultConfig = {
startX: 0,
startY: 0,
endX: 10,
endY: 10,
units: units
};
this.probeVisualization = new ProbeVisualization(
[],
defaultConfig,
this.camera,
this.renderer.domElement,
this.controls,
() => this.updateScene({ forceUpdate: true })
);
this.probeVisualization.group.name = 'ProbeVisualization';
this.probeVisualization.group.visible = false;
this.group.add(this.probeVisualization.group);
}
this.scene.add(this.group);
}
updateScene(options) {
const { forceUpdate = false } = { ...options };
const needUpdateScene = this.props.show || forceUpdate;
if (this.renderer && needUpdateScene) {
this.renderer.render(this.scene, this.camera);
}
}
clearScene() {
if (this.probeVisualization && typeof this.probeVisualization.dispose === 'function') {
this.probeVisualization.dispose();
}
const objsToRemove = _tail(this.scene.children);
_each(objsToRemove, (obj) => {
this.scene.remove(obj);
});
if (this.controls) {
this.controls.dispose();
}
this.updateScene();
}
createCombinedCamera(width, height) {
const frustumWidth = width / 2;
const frustumHeight = (height || width) / 2;
const fov = PERSPECTIVE_FOV;
const near = PERSPECTIVE_NEAR;
const far = PERSPECTIVE_FAR;
const orthoNear = ORTHOGRAPHIC_NEAR;
const orthoFar = ORTHOGRAPHIC_FAR;
const camera = new CombinedCamera(
frustumWidth,
frustumHeight,
fov,
near,
far,
orthoNear,
orthoFar
);
camera.position.x = 0;
camera.position.y = 0;
camera.position.z = CAMERA_DISTANCE;
return camera;
}
createPerspectiveCamera(width, height) {
const fov = PERSPECTIVE_FOV;
const aspect = (width > 0 && height > 0) ? Number(width) / Number(height) : 1;
const near = PERSPECTIVE_NEAR;
const far = PERSPECTIVE_FAR;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.x = 0;
camera.position.y = 0;
camera.position.z = CAMERA_DISTANCE;
return camera;
}
createOrthographicCamera(width, height) {
const left = -width / 2;
const right = width / 2;
const top = height / 2;
const bottom = -height / 2;
const near = ORTHOGRAPHIC_NEAR;
const far = ORTHOGRAPHIC_FAR;
const camera = new THREE.OrthographicCamera(left, right, top, bottom, near, far);
return camera;
}
createTrackballControls(object, domElement) {
const controls = new TrackballControls(object, domElement);
controls.rotateSpeed = Math.PI;
controls.zoomSpeed = 1.2;
controls.panSpeed = 1.0;
controls.noZoom = false;
controls.noPan = false;
controls.staticMoving = true;
controls.dynamicDampingFactor = 0.3;
controls.keys = [65, 83, 68];
controls.minDistance = TRACKBALL_CONTROLS_MIN_DISTANCE;
controls.maxDistance = TRACKBALL_CONTROLS_MAX_DISTANCE;
let shouldAnimate = false;
const animate = () => {
controls.update();
this.updateScene();
if (shouldAnimate) {
requestAnimationFrame(animate);
}
};
controls.addEventListener('start', () => {
shouldAnimate = true;
animate();
});
controls.addEventListener('end', () => {
shouldAnimate = false;
this.updateScene();
});
controls.addEventListener('change', () => {
this.updateScene();
});
return controls;
}
rotateCuttingTool(rpm = 0, fps = 60) {
if (!this.cuttingTool) {
return;
}
const delta = 1 / fps;
const degrees = 360 * (delta * Math.PI / 180);
this.cuttingTool.rotateZ(-(rpm / 60 * degrees));
}
updateCuttingToolPosition() {
if (!this.cuttingTool) {
return;
}
const pivotPoint = this.pivotPoint.get();
const { x: wpox, y: wpoy, z: wpoz } = this.workPosition;
const x0 = wpox - pivotPoint.x;
const y0 = wpoy - pivotPoint.y;
const z0 = wpoz - pivotPoint.z;
this.cuttingTool.position.set(x0, y0, z0);
}
updateCuttingPointerPosition() {
if (!this.cuttingPointer) {
return;
}
const pivotPoint = this.pivotPoint.get();
const { x: wpox, y: wpoy, z: wpoz } = this.workPosition;
const x0 = wpox - pivotPoint.x;
const y0 = wpoy - pivotPoint.y;
const z0 = wpoz - pivotPoint.z;
this.cuttingPointer.position.set(x0, y0, z0);
}
updateLimitsPosition() {
if (!this.limits) {
return;
}
const limits = _get(this.machineProfile, 'limits');
const { xmin = 0, xmax = 0, ymin = 0, ymax = 0, zmin = 0, zmax = 0 } = { ...limits };
const pivotPoint = this.pivotPoint.get();
const x0 = ((xmin + xmax) / 2) - pivotPoint.x;
const y0 = ((ymin + ymax) / 2) - pivotPoint.y;
const z0 = ((zmin + zmax) / 2) - pivotPoint.z;
this.limits.position.set(x0, y0, z0);
}
updateProbeVisualizationPosition() {
if (!this.probeVisualization) {
return;
}
const pivotPoint = this.pivotPoint.get();
this.probeVisualization.group.position.set(-pivotPoint.x, -pivotPoint.y, -pivotPoint.z);
}
lookAt(x, y, z) {
this.controls.target.x = x;
this.controls.target.y = y;
this.controls.target.z = z;
this.controls.update();
}
lookAtCenter() {
if (this.viewport) {
this.viewport.update();
}
if (this.controls) {
this.controls.reset();
}
this.updateScene();
}
load(name, gcode, callback) {
this.unload();
this.gcodeVisualizer = new GCodeVisualizer();
const obj = this.gcodeVisualizer.render(gcode);
obj.name = 'Visualizer';
this.group.add(obj);
const bbox = getBoundingBox(obj);
const dX = bbox.max.x - bbox.min.x;
const dY = bbox.max.y - bbox.min.y;
const dZ = bbox.max.z - bbox.min.z;
const center = new THREE.Vector3(
bbox.min.x + (dX / 2),
bbox.min.y + (dY / 2),
bbox.min.z + (dZ / 2)
);
this.pivotPoint.set(center.x, center.y, center.z);
obj.position.set(-center.x, -center.y, -center.z);
this.updateCuttingToolPosition();
this.updateCuttingPointerPosition();
this.updateLimitsPosition();
this.updateProbeVisualizationPosition();
if (this.viewport && dX > 0 && dY > 0) {
const width = Math.max(dX, 50);
const height = Math.max(dY, 50);
const target = new THREE.Vector3(0, 0, bbox.max.z);
this.viewport.set(width, height, target);
}
this.updateScene();
(typeof callback === 'function') && callback({ bbox: bbox });
}
unload() {
const visualizerObject = this.group.getObjectByName('Visualizer');
if (visualizerObject) {
this.group.remove(visualizerObject);
}
if (this.gcodeVisualizer) {
this.gcodeVisualizer = null;
}
if (this.pivotPoint) {
if (!this.machineProfile) {
this.pivotPoint.set(0, 0, 0);
} else {
const limits = _get(this.machineProfile, 'limits');
const { xmin = 0, xmax = 0, ymin = 0, ymax = 0 } = { ...limits };
const centerX = (xmin + xmax) / 2;
const centerY = (ymin + ymax) / 2;
this.pivotPoint.set(centerX, centerY, 0);
}
this.updateCuttingToolPosition();
this.updateCuttingPointerPosition();
this.updateLimitsPosition();
this.updateProbeVisualizationPosition();
if (this.group) {
this.rebuildCoordinateSystems();
}
}
if (this.controls) {
this.controls.reset();
}
if (this.viewport) {
this.viewport.reset();
}
this.updateScene();
}
setCameraMode(mode) {
const MAIN_BUTTON = 0;
const ROTATE = 0;
const PAN = 2;
if (mode === CAMERA_MODE_ROTATE) {
this.controls && this.controls.setMouseButtonState(MAIN_BUTTON, ROTATE);
}
if (mode === CAMERA_MODE_PAN) {
this.controls && this.controls.setMouseButtonState(MAIN_BUTTON, PAN);
}
}
toTopView() {
if (this.controls) {
this.controls.reset();
}
this.camera.up.set(0, 1, 0);
this.camera.position.set(0, 0, CAMERA_DISTANCE);
if (this.viewport) {
this.viewport.update();
}
if (this.controls) {
this.controls.update();
}
this.updateScene();
}
to3DView() {
if (this.controls) {
this.controls.reset();
}
this.camera.up.set(0, 0, 1);
this.camera.position.set(CAMERA_DISTANCE, -CAMERA_DISTANCE, CAMERA_DISTANCE);
if (this.viewport) {
this.viewport.update();
}
if (this.controls) {
this.controls.update();
}
this.updateScene();
}
toFrontView() {
if (this.controls) {
this.controls.reset();
}
this.camera.up.set(0, 0, 1);
this.camera.position.set(0, -CAMERA_DISTANCE, 0);
if (this.viewport) {
this.viewport.update();
}
if (this.controls) {
this.controls.update();
}
this.updateScene();
}
toLeftSideView() {
if (this.controls) {
this.controls.reset();
}
this.camera.up.set(0, 0, 1);
this.camera.position.set(CAMERA_DISTANCE, 0, 0);
if (this.viewport) {
this.viewport.update();
}
if (this.controls) {
this.controls.update();
}
}
toRightSideView() {
if (this.controls) {
this.controls.reset();
}
this.camera.up.set(0, 0, 1);
this.camera.position.set(-CAMERA_DISTANCE, 0, 0);
if (this.viewport) {
this.viewport.update();
}
if (this.controls) {
this.controls.update();
}
this.updateScene();
}
zoomFit() {
if (this.viewport) {
this.viewport.update();
}
this.updateScene();
}
zoomIn(delta = 0.1) {
const { noZoom } = this.controls;
if (noZoom) {
return;
}
this.controls.zoomIn(delta);
this.controls.update();
this.updateScene();
}
zoomOut(delta = 0.1) {
const { noZoom } = this.controls;
if (noZoom) {
return;
}
this.controls.zoomOut(delta);
this.controls.update();
this.updateScene();
}
pan(deltaX, deltaY) {
const eye = new THREE.Vector3();
const pan = new THREE.Vector3();
const objectUp = new THREE.Vector3();
eye.subVectors(this.controls.object.position, this.controls.target);
objectUp.copy(this.controls.object.up);
pan.copy(eye).cross(objectUp.clone()).setLength(deltaX);
pan.add(objectUp.clone().setLength(deltaY));
this.controls.object.position.add(pan);
this.controls.target.add(pan);
this.controls.update();
}
panUp() {
const { noPan, panSpeed } = this.controls;
!noPan && this.pan(0, 1 * panSpeed);
}
panDown() {
const { noPan, panSpeed } = this.controls;
!noPan && this.pan(0, -1 * panSpeed);
}
panLeft() {
const { noPan, panSpeed } = this.controls;
!noPan && this.pan(1 * panSpeed, 0);
}
panRight() {
const { noPan, panSpeed } = this.controls;
!noPan && this.pan(-1 * panSpeed, 0);
}
render() {
if (!WebGL.isWebGLAvailable()) {
return null;
}
return (
<div
aria-label="3D Visualizer"
style={{
visibility: this.props.show ? 'visible' : 'hidden'
}}
ref={this.setRef}
/>
);
}
}
export default Visualizer;