From e4df2327b67f42dc1c73e5db51e2d1a52c85b98b Mon Sep 17 00:00:00 2001 From: Manuel Thomassen Date: Sun, 20 Apr 2025 11:43:05 +0200 Subject: [PATCH 1/8] fix probe --- src/components/jog.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/jog.js b/src/components/jog.js index 58605870..d88f2884 100644 --- a/src/components/jog.js +++ b/src/components/jog.js @@ -313,10 +313,11 @@ class Jog extends React.Component { probe(axis) { console.log('probe'); + let offset if (axis.indexOf('z') === 0) { - let offset = this.props.settings.machineZProbeOffset; + offset = this.props.settings.machineZProbeOffset; } else { - let offset = this.props.settings.machineXYProbeOffset; + offset = this.props.settings.machineXYProbeOffset; } probe(axis, offset); } From 0aef7fb71ba6efe27e581ebe57932ad97c789910 Mon Sep 17 00:00:00 2001 From: Manuel Thomassen Date: Sun, 20 Apr 2025 12:36:46 +0200 Subject: [PATCH 2/8] remove postProcessRaster since it's broken if gcodeToolOn contains $INTENSITY the regexp will choke and at least in marlin mode the result even if it would work seems questionable --- .../generators/abstract-generator.js | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/lib/action2gcode/generators/abstract-generator.js b/src/lib/action2gcode/generators/abstract-generator.js index 9e58f70b..9cafbe7c 100644 --- a/src/lib/action2gcode/generators/abstract-generator.js +++ b/src/lib/action2gcode/generators/abstract-generator.js @@ -8,12 +8,24 @@ class AbstractGenerator { } postProcessRaster(gcode){ - if (this.settings.gcodeToolOn && this.settings.gcodeToolOff){ - gcode = XRegExp.replace(gcode,new XRegExp("G0(.*?)G1","gis"),'G0$1\n'+this.settings.gcodeToolOn+'\nG1') - gcode = XRegExp.replace(gcode,new XRegExp("G1(.*?)G0","gis"),'G1$1\n'+this.settings.gcodeToolOff+'\nG0') - return gcode; - //return gcode.replace(new XRegExp("G0(.*?)G1","gis"),'G0$1\n'+this.settings.gcodeToolOn+'\nG1').replace(new XRegExp("G1(.*?)G0","gis"),'G1$1\n'+this.settings.gcodeToolOff+'\nG0') - } + // if (this.settings.gcodeToolOn && this.settings.gcodeToolOff){ + // if (this.settings.gcodeToolOn.indexOf('$INTENSITY') > -1) { + // // FIXME in marlin mode this grabs the wrong intensity. the right one is in the line before the G1 + // gcode = XRegExp.replace(gcode,new XRegExp("G0(.*?)G1(?:(.*?)("+(this.settings.gcodeLaserIntensity||'S')+"[0-9.]+))?","gis"), (_,m1,m2,m3) => { + // return 'G0'+m1+'\n'+this.settings.gcodeToolOn.split('$INTENSITY').join(m3)+'\nG1'+m2+m3 + // }) + // } else { + // gcode = XRegExp.replace(gcode,new XRegExp("G0(.*?)G1","gis"),'G0$1\n'+this.settings.gcodeToolOn+'\nG1') + // } + + // if (this.settings.gcodeToolOff.indexOf('$INTENSITY') > -1) { + // gcode = XRegExp.replace(gcode,new XRegExp("G1(.*?)G0(?:(.*?)("+(this.settings.gcodeLaserIntensity||'S')+"[0-9.]+))?","gis"), (_,m1,m2,m3) => { + // return 'G1'+m1+'\n'+this.settings.gcodeToolOff.split('$INTENSITY').join(m3)+'\nG0'+m2+m3 + // }) + // } else { + // gcode = XRegExp.replace(gcode,new XRegExp("G1(.*?)G0","gis"),'G1$1\n'+this.settings.gcodeToolOff+'\nG0') + // } + // } return gcode; } From afee11f6a8504c54bcb5501e486e1e70e5380d95 Mon Sep 17 00:00:00 2001 From: Manuel Thomassen Date: Sun, 20 Apr 2025 13:02:17 +0200 Subject: [PATCH 3/8] fix checkSize crash if no gcode was generated --- src/components/jog.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/jog.js b/src/components/jog.js index d88f2884..4723849d 100644 --- a/src/components/jog.js +++ b/src/components/jog.js @@ -347,7 +347,11 @@ class Jog extends React.Component { if (units == 'mm/s') mult = 60; feedrate = jQuery('#jogfeedxy').val() * mult; - let bounds=this.getGcodeBounds(this.props.gcode) + let bounds = this.getGcodeBounds(this.props.gcode); + if (!bounds) { + CommandHistory.error('no gcode to check size of'); + return; + } let power = this.props.settings.gcodeCheckSizePower / 100 * this.props.settings.gcodeSMaxValue; let moves = ` G90\n From 3166ab12fc8e98e69a44f3285f990994fe1439ca Mon Sep 17 00:00:00 2001 From: Manuel Thomassen Date: Sun, 20 Apr 2025 16:38:55 +0200 Subject: [PATCH 4/8] syncronize playing state in com.js and jog.js --- src/components/com.js | 50 +++++++++---------------------------------- src/components/jog.js | 32 ++++++++++++++++++++------- 2 files changed, 34 insertions(+), 48 deletions(-) diff --git a/src/components/com.js b/src/components/com.js index a56c0641..d7a5720e 100644 --- a/src/components/com.js +++ b/src/components/com.js @@ -286,27 +286,11 @@ class Com extends React.Component { socket.on('runStatus', function (status) { //CommandHistory.write('runStatus: ' + status); console.log('runStatus: ' + status); - if (status === 'running') { - playing = true; - paused = false; - } else if (status === 'paused') { - paused = true; - } else if (status === 'm0') { - paused = true; - m0 = true; - } else if (status === 'resumed') { - paused = false; - } else if (status === 'stopped') { - playing = false; - paused = false; - } else if (status === 'finished') { - playing = false; - paused = false; - } else if (status === 'alarm') { + if (status === 'alarm') { CommandHistory.error('ALARM!') //socket.emit('clearAlarm', 2); } - runStatus(status); + ({ playing, paused, m0 } = runStatus(status, true)); }); socket.on('data', function (data) { @@ -457,10 +441,8 @@ class Com extends React.Component { } $('#queueCnt').html(queueState); if (playing && data === 0) { - playing = false; - paused = false; jobLines = 0; - runStatus('stopped'); + ({ playing, paused, m0 } = runStatus('stopped')); $('#playicon').removeClass('fa-pause'); $('#playicon').addClass('fa-play'); @@ -580,10 +562,8 @@ class Com extends React.Component { CommandHistory.write('Disconnecting Machine', CommandHistory.INFO); console.log('Machine Disconnected by user'); socket.emit('closePort'); - playing = false; - paused = false; jobLines = 0; - runStatus('stopped'); + ({ playing, paused, m0 } = runStatus('stopped')); $("#machineStatus").removeClass('badge-ok'); $("#machineStatus").addClass('badge-notify'); $("#machineStatus").removeClass('badge-warn'); @@ -750,9 +730,7 @@ export function runJob(job) { if (job.length > 0) { jobLines = job.split(/\r\n|\r|\n/).length CommandHistory.write('Running Job; ' + jobLines + ' lines', CommandHistory.INFO); - playing = true; - jobLines = job.split(/\r\n|\r|\n/).length - runStatus('running'); + ({ playing, paused, m0 } = runStatus('running')); $('#playicon').removeClass('fa-play'); $('#playicon').addClass('fa-pause'); jobStartTime = new Date(Date.now()); @@ -775,8 +753,7 @@ export function pauseJob() { console.log('pauseJob'); if (serverConnected) { if (machineConnected){ - paused = true; - runStatus('paused'); + ({ playing, paused, m0 } = runStatus('paused')); $('#playicon').removeClass('fa-pause'); $('#playicon').addClass('fa-play'); socket.emit('pause'); @@ -792,9 +769,7 @@ export function resumeJob() { console.log('resumeJob'); if (serverConnected) { if (machineConnected){ - paused = false; - m0 = false; - runStatus('running'); + ({ playing, paused, m0 } = runStatus('running')); $('#playicon').removeClass('fa-play'); $('#playicon').addClass('fa-pause'); socket.emit('resume'); @@ -811,11 +786,8 @@ export function abortJob() { if (serverConnected) { if (machineConnected){ CommandHistory.write('Aborting job', CommandHistory.INFO); - playing = false; - paused = false; - m0 = false; jobLines = 0; - runStatus('stopped'); + ({ playing, paused, m0 } = runStatus('stopped')); $('#playicon').removeClass('fa-pause'); $('#playicon').addClass('fa-play'); socket.emit('stop'); @@ -995,8 +967,7 @@ export function playpauseMachine() { laseroncmd = 0; } socket.emit('resume', laseroncmd); - paused = false; - runStatus('running'); + ({ playing, paused, m0 } = runStatus('running')); $('#playicon').removeClass('fa-play'); $('#playicon').addClass('fa-pause'); // end ifPaused @@ -1007,8 +978,7 @@ export function playpauseMachine() { laseroffcmd = 0; } socket.emit('pause', laseroffcmd); - paused = true; - runStatus('paused'); + ({ playing, paused, m0 } = runStatus('paused')); $('#playicon').removeClass('fa-pause'); $('#playicon').addClass('fa-play'); } diff --git a/src/components/jog.js b/src/components/jog.js index 4723849d..7126ce48 100644 --- a/src/components/jog.js +++ b/src/components/jog.js @@ -33,6 +33,7 @@ var ovLoop; var playing = false; var paused = false; var m0 = false; +var thatComponent; $('body').on('keydown', function (ev) { if (ev.keyCode === 17) { @@ -123,6 +124,7 @@ class Jog extends React.Component { componentDidMount() { + thatComponent = this; this.checkGcodeBounds(this.props.gcode); bindKeys(this.bindings); @@ -179,6 +181,7 @@ class Jog extends React.Component { } componentWillUnmount() { + thatComponent = undefined; liveJoggingState = this.state.liveJogging; // unbindKeys(this.bindings) @@ -244,14 +247,16 @@ class Jog extends React.Component { let cmd = this.props.gcode; //alert(cmd); console.log('runJob(' + cmd.length + ')'); - playing = true; + if (cmd.length > 0) { + playing = true; - this.setState({ - isPlaying: true, - liveJogging: { - ... this.state.liveJogging, disabled: true, hasHomed: false - } - }) + this.setState({ + isPlaying: true, + liveJogging: { + ... this.state.liveJogging, disabled: true, hasHomed: false + } + }) + } runJob(cmd); } else { @@ -927,11 +932,13 @@ Jog = connect( // Exports export default Jog +export { liveJoggingState } -export function runStatus(status) { +export function runStatus(status, applyState = true) { if (status === 'running') { playing = true; paused = false; + m0 = false; $('#playicon').removeClass('fa-play'); $('#playicon').addClass('fa-pause'); $('#xP').attr('disabled', true); @@ -982,6 +989,15 @@ export function runStatus(status) { } else if (status === 'alarm') { //socket.emit('clearAlarm', 2); } + if (applyState && thatComponent) { + thatComponent.setState({ + isPlaying: playing, + isPaused: paused, + isM0: m0, + liveJogging: { ...thatComponent.state.liveJogging, disabled: (playing && !m0) } + }); + } + return { playing, paused, m0 }; }; export class LiveJogging extends React.Component { From f2ff9ba278df3e3766d199dcbc3747f4843cf68b Mon Sep 17 00:00:00 2001 From: Manuel Thomassen Date: Sun, 20 Apr 2025 16:43:13 +0200 Subject: [PATCH 5/8] allow to reset setZero with alt+click --- src/components/com.js | 4 ++-- src/components/jog.js | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/com.js b/src/components/com.js index d7a5720e..99cfa7f0 100644 --- a/src/components/com.js +++ b/src/components/com.js @@ -813,11 +813,11 @@ export function clearAlarm(method) { } } -export function setZero(axis) { +export function setZero(axis, reset) { if (serverConnected) { if (machineConnected){ CommandHistory.write('Set ' + axis + ' Axis zero', CommandHistory.INFO); - socket.emit('setZero', axis); + socket.emit('setZero', axis, reset); } else { CommandHistory.error('Machine is not connected!') } diff --git a/src/components/jog.js b/src/components/jog.js index 7126ce48..59e7f2e9 100644 --- a/src/components/jog.js +++ b/src/components/jog.js @@ -327,12 +327,12 @@ class Jog extends React.Component { probe(axis, offset); } - setZero(axis) { + setZero(axis, event) { if (!this.state.isPlaying) this.setState({ liveJogging: { ... this.state.liveJogging, hasHomed: true, disabled: false } }) - console.log('setZero(' + axis + ')'); - setZero(axis); + console.log('setZero(' + axis + ', ' + event.altKey + ')'); + setZero(axis, event.altKey); } setPosition(pos) { @@ -549,7 +549,7 @@ class Jog extends React.Component {
  • Work Coordinates
  • { this.home('x') }}>Home X Axis
  • -
  • { this.setZero('x') }}>Set X Axis Zero
  • +
  • { this.setZero('x', e) }}>Set X Axis Zero
  • Move
  • { this.gotoZero('x') }}>G0 to X0
  • @@ -572,7 +572,7 @@ class Jog extends React.Component {
  • Work Coordinates
  • { this.home('y') }}>Home Y Axis
  • -
  • { this.setZero('y') }}>Set Y Axis Zero
  • +
  • { this.setZero('y', e) }}>Set Y Axis Zero
  • Move
  • { this.gotoZero('y') }}>G0 to Y0
  • @@ -596,7 +596,7 @@ class Jog extends React.Component {
  • Work Coordinates
  • { this.home('z') }}>Home Z Axis
  • -
  • { this.setZero('z') }}>Set Z Axis Zero
  • +
  • { this.setZero('z', e) }}>Set Z Axis Zero
  • Move
  • { this.gotoZero('z') }}>G0 to Z0
  • @@ -623,7 +623,7 @@ class Jog extends React.Component {
  • Work Coordinates
  • { this.home('a') }}>Home A Axis
  • -
  • { this.setZero('a') }}>Set A Axis Zero
  • +
  • { this.setZero('a', e) }}>Set A Axis Zero
  • Move
  • { this.gotoZero('a') }}>G0 to A0
  • @@ -716,7 +716,7 @@ class Jog extends React.Component {
    - @@ -589,23 +1024,36 @@ class Com extends React.Component { - - +
    - - + + + +
    + + +
    +
    + +
    + +
    +
    + + + + +
    - +
    - + + + +
    - - - - -
    @@ -799,10 +1247,28 @@ export function abortJob() { } } +export function clearJob() { + console.log('clearJob'); + if (serverConnected) { + if (machineConnected){ + if (!jobProgress || jobProgress === 100) { + CommandHistory.write('Clearing Finished job', CommandHistory.INFO); + socket.emit('clearJob'); + } else { + CommandHistory.error('Job is still running!') + } + } else { + CommandHistory.error('Machine is not connected!') + } + } else { + CommandHistory.error('Server is not connected!') + } +} + export function clearAlarm(method) { console.log('clearAlarm'); if (serverConnected) { - if (machineConnected){ + if (machineConnected || (socket && socket.moonraker)){ CommandHistory.write('Resetting alarm', CommandHistory.INFO); socket.emit('clearAlarm', method); } else { @@ -945,7 +1411,7 @@ export function spindleOverride(step) { export function resetMachine() { if (serverConnected) { - if (machineConnected){ + if (machineConnected || (socket && socket.moonraker)){ CommandHistory.error('Resetting Machine') socket.emit('resetMachine'); } else { diff --git a/src/components/jog.js b/src/components/jog.js index 59e7f2e9..fc9a6e4f 100644 --- a/src/components/jog.js +++ b/src/components/jog.js @@ -14,7 +14,7 @@ import { xOffset, yOffset } from './com'; import CommandHistory from './command-history'; import { Input, TextField, NumberField, ToggleField, SelectField } from './forms'; -import { runCommand, runJob, pauseJob, resumeJob, abortJob, clearAlarm, setZero, gotoZero, setPosition, home, probe, checkSize, laserTest, jog, jogTo, feedOverride, spindleOverride, resetMachine } from './com.js'; +import { runCommand, runJob, pauseJob, resumeJob, abortJob, clearJob, clearAlarm, setZero, gotoZero, setPosition, home, probe, checkSize, laserTest, jog, jogTo, feedOverride, spindleOverride, resetMachine, serverConnected, socket } from './com.js'; import { MacrosBar } from './macros'; import '../styles/index.css' @@ -178,6 +178,9 @@ class Jog extends React.Component { } } + if (serverConnected && socket && socket.moonraker) { + socket.emit('getServerConfig'); + } } componentWillUnmount() { @@ -301,6 +304,18 @@ class Jog extends React.Component { } } + clearJob() { + if (!socket || !socket.moonraker) { + return; + } + + if (playing) { + CommandHistory.log('clearJob ignored, because job is running'); + } else { + clearJob(); + } + } + homeAll() { console.log('homeAll'); let cmd = this.props.settings.gcodeHoming; @@ -422,11 +437,9 @@ class Jog extends React.Component { } laserTest() { - console.log('laserTest'); let power = this.props.settings.gcodeToolTestPower; let duration = this.props.settings.gcodeToolTestDuration; let maxS = this.props.settings.gcodeSMaxValue; - console.log('laserTest(' + power + ',' + duration + ',' + maxS + ')'); laserTest(power, duration, maxS); } @@ -533,7 +546,7 @@ class Jog extends React.Component { return (
    Not Connected - Queued: 0 + { this.clearJob() }} title="Job details, based on gcode lines completed and queued" id="queueCnt" style={{ marginRight: 5 }}>Queued: 0
    X:
    @@ -932,7 +945,14 @@ Jog = connect( // Exports export default Jog -export { liveJoggingState } +export function hasHomed(hasHomed) { + liveJoggingState.hasHomed = hasHomed + if (thatComponent) { + thatComponent.setState({ + liveJogging: { ...thatComponent.state.liveJogging, hasHomed: hasHomed } + }); + } +} export function runStatus(status, applyState = true) { if (status === 'running') { diff --git a/src/components/settings.js b/src/components/settings.js index c09888bb..1e2a694d 100644 --- a/src/components/settings.js +++ b/src/components/settings.js @@ -225,17 +225,20 @@ class Settings extends React.Component { + + If enabled, machine dimensions and origin offsets will be automatically set from the machine's firmware limits.
    Only supported by machines connected via Moonraker. +

    ,"Machine Size from firmware") }} />
    Machine Dimensions
    The total width and height (X and Y) size of the machine work area -

    ,"Working Dimensions") }} /> - +

    ,"Working Dimensions"), disabled: this.props.settings.machineSizeFromMachine }} /> +
    Machine Origin offsets
    X and Y offsets for the machine work area relative to the Home position.
    For a machine that homes to the top-right corner use negative values. -

    ,"Working Area Offset") }} /> - +

    ,"Working Area Offset"), disabled: this.props.settings.machineSizeFromMachine }} /> +
    Tool head
    Beam Ø), info: Info(

    The diameter of the laser spot when cutting and marking.
    Used for the suggested width in laser cut, fill and raster operations. diff --git a/src/reducers/settings.js b/src/reducers/settings.js index 03496b43..d94c5d29 100644 --- a/src/reducers/settings.js +++ b/src/reducers/settings.js @@ -83,6 +83,7 @@ export const SETTINGS_INITIALSTATE = { machineLaserHasIntensity: true, machineBottomLeftX: 0, machineBottomLeftY: 0, + machineSizeFromMachine: false, machineFeedRange: { XY: {min: 1, max:50000}, @@ -178,6 +179,7 @@ export const SETTINGS_INITIALSTATE = { comAccumulatedJobTime: 0, connectVia: '', + connectServerVia: 'lw.comm-server', connectPort: '', connectBaud: '115200', connectIP: '', From e4c424ee71c700839a3e475b4291512d77991704 Mon Sep 17 00:00:00 2001 From: Manuel Thomassen Date: Sun, 20 Apr 2025 17:21:01 +0200 Subject: [PATCH 7/8] add thumbnails to gcode --- src/components/com.js | 4 +- src/components/settings.js | 17 ++ src/components/workspace.js | 274 ++++++++++++++++++++++++++---- src/draw-commands/GcodePreview.js | 36 ++++ src/draw-commands/LaserPreview.js | 12 ++ src/reducers/settings.js | 5 + 6 files changed, 314 insertions(+), 34 deletions(-) diff --git a/src/components/com.js b/src/components/com.js index d5590c66..a0fbfd74 100644 --- a/src/components/com.js +++ b/src/components/com.js @@ -11,6 +11,7 @@ import { setWorkspaceAttrs } from '../actions/workspace'; // import { setGcode } from '../actions/gcode'; import CommandHistory from './command-history'; import { strftime } from '../lib/strftime.js' +import { thumbnails } from './workspace.js' import { alert, prompt, confirm} from './laserweb'; @@ -334,10 +335,11 @@ class Com extends React.Component { send('printer.gcode.script', { script }); }, runJob(data) { + const thumbs = thumbnails(); var form = new FormData(); form.append('print', true) const filename = strftime(that.props.settings.gcodeFilename) + that.props.settings.gcodeExtension - const blob = new Blob([data], { type: 'text/plain' }) + const blob = new Blob([data+thumbs], { type: 'text/plain' }) form.append('file', blob, filename) fetch(addr.replace('ws', 'http').replace(/\/websocket|$/, '/server/files/upload'), { method: 'POST', body: form }).then((response) => { if (response.status > 201) { diff --git a/src/components/settings.js b/src/components/settings.js index 1e2a694d..cae2c486 100644 --- a/src/components/settings.js +++ b/src/components/settings.js @@ -425,6 +425,23 @@ class Settings extends React.Component { Increasing this can improve gcode generation performance when working with with lots of individual operations.
    This is applied per operation, indvidual operations are not threaded and will not benefit from this option.

    ,"Gcode Generation Threads"), units: '' }} /> +
    Thumbnails
    + + Comma separated list of thumbnail sizes and Grid Opacity, eg 32x32,400x300.
    + Be aware that Moonraker only parses thumbnails if it knows the gcode generator, to trick it put something like PrusaSlicer thumbnail hack on in the gcode header.
    +

    ,"Thumbnail Sizes"), style: { resize: "vertical", fontFamily: "monospace, monospace" } }} /> + + Don't show grid in thumbnails smaller than this size. +

    ,"Minimum Grid Size"), units: 'px' }} /> + + Include Tool preview in thumbnails. +

    ,"Show Tool in Thumbnail") }} /> + + Include Gcode preview in thumbnails. +

    ,"Show Gcode in Thumbnail") }} /> + + Include Document preview in thumbnails. +

    ,"Show Document in Thumbnail") }} />
    Text here will be placed in a commented (;) block at the start of the gcode.
    diff --git a/src/components/workspace.js b/src/components/workspace.js index 0ff2c7a1..d9a260e1 100644 --- a/src/components/workspace.js +++ b/src/components/workspace.js @@ -23,13 +23,14 @@ import '../styles/simbar.css'; import { GlobalStore } from '..'; import { setCameraAttrs, zoomArea } from '../actions/camera' +import { zoomArea as calcZoomArea } from '../reducers/camera.js' import { selectDocument, toggleSelectDocument, transform2dSelectedDocuments, removeDocumentSelected, cloneDocumentSelected } from '../actions/document'; import { setWorkspaceAttrs } from '../actions/workspace'; import { setSettingsAttrs } from '../actions/settings'; import { runCommand, jogTo } from './com.js'; -import { withDocumentCache } from './document-cache' +import { DocumentCacheHolder, withDocumentCache } from './document-cache' import { Dom3d, Text3d } from './dom3d'; import { DrawCommands } from '../draw-commands' import { GcodePreview } from '../draw-commands/GcodePreview' @@ -104,7 +105,7 @@ class LightenMachineBounds { }; class Grid { - draw(drawCommands, { perspective, view, width, height, major = MAJOR_GRID_SPACING, minor = MINOR_GRID_SPACING, xcolor, ycolor }) { + draw(drawCommands, { perspective, view, width, height, major = MAJOR_GRID_SPACING, minor = MINOR_GRID_SPACING, xcolor, ycolor, opacity = 1 }) { if (!this.maingrid || !this.origin || this.width !== width || this.height !== height) { this.width = width; this.height = height; @@ -142,13 +143,13 @@ class Grid { this.origincount = c.length / 3 } - drawCommands.basic({ perspective, view, position: this.maingrid, offset: 0, count: this.maincount, color: [0.7, 0.7, 0.7, 0.95], scale: [1, 1, 1], translate: [0, 0, 0], primitive: drawCommands.gl.LINES }); // Gray grid - drawCommands.basic({ perspective, view, position: this.darkgrid, offset: 0, count: this.darkcount, color: [0.5, 0.5, 0.5, 0.95], scale: [1, 1, 1], translate: [0, 0, 0], primitive: drawCommands.gl.LINES }); // dark grid + drawCommands.basic({ perspective, view, position: this.maingrid, offset: 0, count: this.maincount, color: [0.7, 0.7, 0.7, 0.95*opacity], scale: [1, 1, 1], translate: [0, 0, 0], primitive: drawCommands.gl.LINES }); // Gray grid + drawCommands.basic({ perspective, view, position: this.darkgrid, offset: 0, count: this.darkcount, color: [0.5, 0.5, 0.5, 0.95*opacity], scale: [1, 1, 1], translate: [0, 0, 0], primitive: drawCommands.gl.LINES }); // dark grid let rgbx = [...convert.hex.rgb(xcolor)]; let rgby = [...convert.hex.rgb(ycolor)]; - drawCommands.basic({ perspective, view, position: this.origin, offset: 0, count: 2, color: [rgbx[0]/255,rgbx[1]/255,rgbx[2]/255,1], scale: [1, 1, 1], translate: [0, 0, 0], primitive: drawCommands.gl.LINES }); - drawCommands.basic({ perspective, view, position: this.origin, offset: 2, count: 2, color: [rgby[0]/255,rgby[1]/255,rgby[2]/255,1], scale: [1, 1, 1], translate: [0, 0, 0], primitive: drawCommands.gl.LINES }); + drawCommands.basic({ perspective, view, position: this.origin, offset: 0, count: 2, color: [rgbx[0]/255,rgbx[1]/255,rgbx[2]/255,opacity], scale: [1, 1, 1], translate: [0, 0, 0], primitive: drawCommands.gl.LINES }); + drawCommands.basic({ perspective, view, position: this.origin, offset: 2, count: 2, color: [rgby[0]/255,rgby[1]/255,rgby[2]/255,opacity], scale: [1, 1, 1], translate: [0, 0, 0], primitive: drawCommands.gl.LINES }); } }; @@ -516,6 +517,178 @@ function cacheDrawing(fn, state, args) { }); } +var workspaceContext; +var thumbnailContext; // = null; // to use a dedicated canvas for thumbnails +var thumbnailDebug; // = true; // to show a debug canvas +export function thumbnails(gcode) { + const settings = workspaceContext.props.settings; + if (!settings.gcodeThumbnailGcode && !settings.gcodeThumbnailLaser && !settings.gcodeThumbnailDocument) return ''; + + const sizes = (settings.gcodeThumbnailSizes || '').trim().split(/[ ,]+/).map(s => s.split(/x|g/)) + + if (thumbnailContext === null) { + const thumbnailCanvas = document.createElement('canvas'); + const gl = thumbnailCanvas.getContext('webgl', { alpha: true, depth: true, antialias: true, preserveDrawingBuffer: true }); + thumbnailContext = { + canvas: thumbnailCanvas, + drawCommands: new DrawCommands(gl), + grid: new Grid(), + gcodePreview: new GcodePreview(), + laserPreview: new LaserPreview(), + documentCacheHolder: new DocumentCacheHolder(), + } + } + const { canvas, drawCommands, grid, gcodePreview, laserPreview, documentCacheHolder } = thumbnailContext || { + canvas: workspaceContext.canvas, + drawCommands: workspaceContext.drawCommands, + grid: workspaceContext.grid, + gcodePreview: workspaceContext.props.gcodePreview, + laserPreview: workspaceContext.props.laserPreview, + documentCacheHolder: workspaceContext.props.documentCacheHolder, + } + let gl = canvas.getContext('webgl', { alpha: true, depth: true, antialias: true, preserveDrawingBuffer: true }); + + if (thumbnailDebug === true) { + thumbnailDebug = document.createElement('canvas'); + thumbnailDebug.style.position = 'fixed' + thumbnailDebug.style.top = 0 + thumbnailDebug.style.right = 0 + thumbnailDebug.style.transformOrigin = 'top right' + thumbnailDebug.style.transform = `scale(${1/window.devicePixelRatio})` + document.body.append(thumbnailDebug) + } + + if (canvas !== workspaceContext.canvas) { + if (gcode) { + const parsedGcode = parseGcode(gcode); + gcodePreview.setParsedGcode(parsedGcode); + laserPreview.setParsedGcode(parsedGcode); + } else { + gcodePreview.setParsedGcodeFromPreview(workspaceContext.props.gcodePreview); + laserPreview.setParsedGcodeFromPreview(workspaceContext.props.laserPreview); + } + + documentCacheHolder.setDocuments(workspaceContext.props.documents); + } + + const originalWidth = canvas.width; + const originalHeight = canvas.height; + + let thumbs = ''; + for (let [width, height, opacity] of sizes) { + if (!(width > 0 && height > 0)) continue; + if (opacity) { + opacity = +opacity / 100; + } else { + opacity = 1; + }; + + canvas.width = width; + canvas.height = height; + const workspace = { workOffsetX: 0, workOffsetY: 0, width, height }; + + const machineX = settings.machineBottomLeftX - workspace.workOffsetX; + const machineY = settings.machineBottomLeftY - workspace.workOffsetY; + + let area; + if (settings.gcodeThumbnailGcode || settings.gcodeThumbnailLaser) { + area = WorkspaceClass.zoomGcodeArea(gcodePreview); + } + if (settings.gcodeThumbnailDocument) { + const docArea = WorkspaceClass.zoomDocArea(workspaceContext.props.documentCacheHolder, {}, true); + if (area && docArea) { + area = [Math.min(area[0], docArea[0]), Math.min(area[1], docArea[1]), Math.max(area[2], docArea[2]), Math.max(area[3], docArea[3])]; + } else if (docArea) { + area = docArea; + } + } + if (!area) { + area = WorkspaceClass.zoomMachineArea(settings, workspace); + } + const zoom = calcZoomArea(null, settings, workspace, { x1: area[0], y1: area[1], x2: area[2], y2: area[3] }); + const size = Math.min(area[2] - area[0], area[3] - area[1]); + const beamSize = Math.max(settings.machineBeamDiameter, size / Math.max(width, height) * 0.9) + + const camera = calcCamera({ + viewportWidth: width, + viewportHeight: height, + fovy: zoom.fovy, + near: .1, + far: 2000, + eye: zoom.eye, + center: zoom.center, + up: [0, 1, 0], + showPerspective: false, + machineX, + machineY, + }); + + gl.viewport(0, 0, width, height); + + let rgbWorkSpace = [...convert.hex.rgb(settings.workBedColor)]; + gl.clearColor(rgbWorkSpace[0]/255,rgbWorkSpace[1]/255,rgbWorkSpace[2]/255,1); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + gl.enable(gl.BLEND); + + if (opacity > 0 && Math.min(width, height) >= settings.gcodeThumbnailGridMinSize) { + grid.draw(drawCommands, { + perspective: camera.perspective, view: camera.view, + width: settings.toolGridWidth, height: settings.toolGridHeight, + minor: Math.max(settings.toolGridMinorSpacing,0.1), + major: Math.max(settings.toolGridMajorSpacing,1), + xcolor: settings.toolGridXColor, + ycolor: settings.toolGridYColor, + opacity: opacity, + }); + } + + if (settings.gcodeThumbnailDocument) { + if (canvas === workspaceContext.canvas) { + drawDocuments({ perspective: camera.perspective, view: camera.view, drawCommands, documentCacheHolder }); + } else { + for (let cachedDocument of documentCacheHolder.cache.values()) + if (cachedDocument.document.visible) + drawDocument(camera.perspective, camera.view, drawCommands, cachedDocument, !cachedDocument.texture); + } + } + + if (settings.gcodeThumbnailGcode) { + gcodePreview.draw( + drawCommands, camera.perspective, camera.view, + settings.simG0Rate, 1e10, settings.machineAAxisDiameter); + } + + if (settings.gcodeThumbnailLaser) { + gl.blendEquation(drawCommands.EXT_blend_minmax.MIN_EXT); + gl.blendFunc(gl.ONE, gl.ONE); + laserPreview.draw( + drawCommands, camera.perspective, camera.view, beamSize, + settings.gcodeSMaxValue, settings.simG0Rate, 1e10, settings.machineAAxisDiameter); + gl.blendEquation(gl.FUNC_ADD); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + } + + const base64 = canvas.toDataURL().replace(/^.*?,/, ''); + const lines = base64.split(/(.{78})/).filter(Boolean).join('\n; '); + thumbs += `\n;\n; thumbnail begin ${width}x${height} ${base64.length}\n; ${lines}\n; thumbnail end\n;\n`; + + if (thumbnailDebug) { + thumbnailDebug.width = width; + thumbnailDebug.height = height; + thumbnailDebug.getContext('2d').drawImage(canvas, 0, 0); + } + } + + if (canvas === workspaceContext.canvas) { + canvas.width = originalWidth; + canvas.height = originalHeight; + } + + return thumbs; +} + + export function drawDocument(perspective, view, drawCommands, cachedDocument, createTextures) { let { document } = cachedDocument; if (document.rawPaths) { @@ -682,6 +855,7 @@ class WorkspaceContent extends React.Component { this.drawDocsState = {}; this.drawGcodeState = {}; this.drawSelDocsState = {}; + workspaceContext = this } UNSAFE_componentWillMount() { @@ -1315,37 +1489,56 @@ class Workspace extends React.Component { this.showControls = true; } - zoomMachine() { - let x = this.props.settings.machineBottomLeftX; - let y = this.props.settings.machineBottomLeftY; - if (!this.props.settings.showMachine) { + static zoomMachineArea(settings, workspace) { + let x = settings.machineBottomLeftX; + let y = settings.machineBottomLeftY; + if (!settings.showMachine) { x = 0; y = 0; - } - this.props.dispatch(zoomArea( - x - 10 - this.props.workspace.workOffsetX, - y - 10 - this.props.workspace.workOffsetY, - x + this.props.settings.machineWidth + 10 - this.props.workspace.workOffsetX, - y + this.props.settings.machineHeight + 10 - this.props.workspace.workOffsetY - )); + } + return [ + x - 10 - workspace.workOffsetX, + y - 10 - workspace.workOffsetY, + x + settings.machineWidth + 10 - workspace.workOffsetX, + y + settings.machineHeight + 10 - workspace.workOffsetY + ]; } - zoomDoc() { + zoomMachine() { + this.props.dispatch(zoomArea(...this.constructor.zoomMachineArea(this.props.settings, this.props.workspace))); + } + + static zoomDocArea(documentCacheHolder, that, notSelected) { let found = false; - let bounds = this.bounds = { x1: Number.MAX_VALUE, y1: Number.MAX_VALUE, x2: -Number.MAX_VALUE, y2: -Number.MAX_VALUE }; - for (let cache of this.props.documentCacheHolder.cache.values()) { - let doc = cache.document; - if (doc.selected && doc.transform2d && cache.bounds) { - found = true; - bounds.x1 = Math.min(bounds.x1, cache.bounds.x1 + doc.transform2d[4]); - bounds.y1 = Math.min(bounds.y1, cache.bounds.y1 + doc.transform2d[5]); - bounds.x2 = Math.max(bounds.x2, cache.bounds.x2 + doc.transform2d[4]); - bounds.y2 = Math.max(bounds.y2, cache.bounds.y2 + doc.transform2d[5]); + let bounds = that.bounds = { x1: Number.MAX_VALUE, y1: Number.MAX_VALUE, x2: -Number.MAX_VALUE, y2: -Number.MAX_VALUE }; + if (!notSelected) { + for (let cache of documentCacheHolder.cache.values()) { + let doc = cache.document; + if (doc.selected && doc.transform2d && cache.bounds) { + found = true; + bounds.x1 = Math.min(bounds.x1, cache.bounds.x1 + doc.transform2d[4]); + bounds.y1 = Math.min(bounds.y1, cache.bounds.y1 + doc.transform2d[5]); + bounds.x2 = Math.max(bounds.x2, cache.bounds.x2 + doc.transform2d[4]); + bounds.y2 = Math.max(bounds.y2, cache.bounds.y2 + doc.transform2d[5]); + } } } if (!found) { - for (let cache of this.props.documentCacheHolder.cache.values()) { + for (let cache of documentCacheHolder.cache.values()) { + let doc = cache.document; + if (doc.visible && doc.transform2d && cache.bounds) { + found = true; + bounds.x1 = Math.min(bounds.x1, cache.bounds.x1 + doc.transform2d[4]); + bounds.y1 = Math.min(bounds.y1, cache.bounds.y1 + doc.transform2d[5]); + bounds.x2 = Math.max(bounds.x2, cache.bounds.x2 + doc.transform2d[4]); + bounds.y2 = Math.max(bounds.y2, cache.bounds.y2 + doc.transform2d[5]); + } + } + } + + if (!found) { + for (let cache of documentCacheHolder.cache.values()) { let doc = cache.document; if (doc.transform2d && cache.bounds) { found = true; @@ -1360,15 +1553,29 @@ class Workspace extends React.Component { if (found) { let marginX = (bounds.x2 - bounds.x1) / 50; let marginY = (bounds.y2 - bounds.y1) / 50; - this.props.dispatch(zoomArea(bounds.x1 - marginX, bounds.y1 - marginY, bounds.x2 + marginX, bounds.y2 + marginY)); + return [bounds.x1 - marginX, bounds.y1 - marginY, bounds.x2 + marginX, bounds.y2 + marginY]; + } + } + + zoomDoc() { + const found = this.constructor.zoomDocArea(this.props.documentCacheHolder, this, false); + if (found) { + this.props.dispatch(zoomArea(...found)); + } + } + + static zoomGcodeArea(gcodePreview) { + if (gcodePreview.array) { + let marginX = (gcodePreview.maxX - gcodePreview.minX) / 50; + let marginY = (gcodePreview.maxY - gcodePreview.minY) / 50; + return [gcodePreview.minX - marginX, gcodePreview.minY - marginY, gcodePreview.maxX + marginX, gcodePreview.maxY + marginY]; } } zoomGcode() { - if (this.gcodePreview.array) { - let marginX = (this.gcodePreview.maxX - this.gcodePreview.minX) / 50; - let marginY = (this.gcodePreview.maxY - this.gcodePreview.minY) / 50; - this.props.dispatch(zoomArea(this.gcodePreview.minX - marginX, this.gcodePreview.minY - marginY, this.gcodePreview.maxX + marginX, this.gcodePreview.maxY + marginY)); + const found = this.constructor.zoomGcodeArea(this.gcodePreview); + if (found) { + this.props.dispatch(zoomArea(...found)); } } @@ -1507,6 +1714,7 @@ class Workspace extends React.Component { ) } } +const WorkspaceClass = Workspace; Workspace = connect( state => ({ camera: state.camera, gcode: state.gcode.content, workspace: state.workspace, settings: state.settings, enableVideo: ((state.settings.toolVideoDevice !== null) || (!!state.settings.toolWebcamUrl)) }), dispatch => ({ diff --git a/src/draw-commands/GcodePreview.js b/src/draw-commands/GcodePreview.js index fd8adfcf..abfa33e9 100644 --- a/src/draw-commands/GcodePreview.js +++ b/src/draw-commands/GcodePreview.js @@ -170,6 +170,42 @@ export class GcodePreview { } } + setParsedGcodeFromPreview(preview) { + if (this.array === preview.array) + return; + + this.arrayChanged = true; + ++this.arrayVersion; + if (!preview.array) { + this.array = null; + this.g0Dist = 0; + this.g1Time = 0; + this.moves = 0; + } else { + this.array = preview.array; + this.g0Dist = preview.g0Dist; + this.g1Time = preview.g1Time; + this.moves = preview.moves; + this.minX = preview.minX; + this.maxX = preview.maxX; + this.minY = preview.minY; + this.maxY = preview.maxY; + this.minA = preview.minA; + this.maxA = preview.maxA; + console.log('setParsedGcodeFromPreviewG', { + g0Dist: this.g0Dist, + g1Time: this.g1Time, + moves: this.moves, + minX: this.minX, + maxX: this.maxX, + minY: this.minY, + maxY: this.maxY, + minA: this.minA, + maxA: this.maxA, + }); + } + } + draw(drawCommands, perspective, view, g0Rate, simTime, rotaryDiameter) { if (this.drawCommands !== drawCommands) { this.drawCommands = drawCommands; diff --git a/src/draw-commands/LaserPreview.js b/src/draw-commands/LaserPreview.js index 8dba5941..07a69b51 100644 --- a/src/draw-commands/LaserPreview.js +++ b/src/draw-commands/LaserPreview.js @@ -167,6 +167,18 @@ export class LaserPreview { } } + setParsedGcodeFromPreview(preview) { + if (this.array === preview.array) + return; + + this.arrayChanged = true; + if (!preview.array) { + this.array = null; + } else { + this.array = preview.array; + } + } + draw(drawCommands, perspective, view, diameter, gcodeSMaxValue, g0Rate, simTime, rotaryDiameter) { if (this.drawCommands !== drawCommands) { this.drawCommands = drawCommands; diff --git a/src/reducers/settings.js b/src/reducers/settings.js index d94c5d29..d5cbec40 100644 --- a/src/reducers/settings.js +++ b/src/reducers/settings.js @@ -169,6 +169,11 @@ export const SETTINGS_INITIALSTATE = { gcodeConcurrency: 2, gcodeSegmentLength: 0, gcodeCurvePrecision: 0.1, + gcodeThumbnailSizes: "", + gcodeThumbnailGridMinSize: 128, + gcodeThumbnailLaser: true, + gcodeThumbnailGcode: false, + gcodeThumbnailDocument: false, comServerVersion: 'not connected', comApiVersion: 'N/A', From dbb18f9cc55d6ac349f8501a79293b013ab3c0bd Mon Sep 17 00:00:00 2001 From: Manuel Thomassen Date: Sun, 20 Apr 2025 18:10:47 +0200 Subject: [PATCH 8/8] make docker release build actually use the app from this repo --- DOCKER.md | 2 +- Dockerfile | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/DOCKER.md b/DOCKER.md index 3cc94a44..9ff986af 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -21,7 +21,7 @@ docker run -it --device=/dev/ttyUSB0 --rm -p 8000:8000 laserweb4:dev ## Release You can run the current lw.comm-server version of the app in Docker using the commands below. -**Warning:** This will bundle the current (head) [lw.comm-server head](https://github.com/LaserWeb/lw.comm-server/) Git version + the LW app bundled with that, it *does not* build the latest LW app from this repo! Use the 'dev' target for that. +**Warning:** This will bundle the current (head) [lw.comm-server head](https://github.com/LaserWeb/lw.comm-server/) Git version + the LW app from this repo. - build release image: ``` diff --git a/Dockerfile b/Dockerfile index a2d394f6..b35502ab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ # # ---- Base Node ---- -FROM node:16-bullseye AS base +FROM node:22-bookworm AS base # set working directory WORKDIR /usr/src/app # Set up Apt, install build tooling and udev RUN apt update -RUN apt install -y build-essential udev +RUN apt install -y build-essential udev libudev-dev libusb-1.0-0-dev # Upgrade npm and set node options RUN npm install -g npm RUN npm set progress=false @@ -19,12 +19,21 @@ FROM base AS comm-server # (Currently use --force to allow for broken deps, this should be removed once the dep tree is fixed RUN npm install -g nodemon && npm install --force lw.comm-server@git+https://github.com/LaserWeb/lw.comm-server.git -# ---- Release ---- -# This will use the git head version of lw.comm-server + the LW app version bundled with that. -# it DOES NOT build and serve the version of LaserWeb in this repo # +# ---- build Laserweb ---- +FROM base as build-prod +COPY package.json package-lock.json ./ +RUN npm ci --force +COPY . . +RUN npm run bundle-prod + +# ---- Release ---- +# This will use the git head version of lw.comm-server and Laserweb from the build stage FROM comm-server AS release WORKDIR /usr/src/app +# Replace the bundled app with the freshly built Laserweb +RUN rm -rf node_modules/lw.comm-server/app/ +COPY --from=build-prod /usr/src/app/dist /usr/src/app/node_modules/lw.comm-server/app/ # define CMD CMD [ "node", "node_modules/lw.comm-server/server.js"]