bounding box plot
This commit is contained in:
+250
-75
@@ -18,7 +18,7 @@ class WallPlotter {
|
||||
// Static constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static VERSION = '2.3.15';
|
||||
static VERSION = '2.3.24';
|
||||
static MIN_FEED_MM_MIN = 1;
|
||||
static MAX_FEED_MM_MIN = 10000;
|
||||
/** Offline overlay size when not connected (mm) — 150×150 cm centred on origin */
|
||||
@@ -133,14 +133,16 @@ class WallPlotter {
|
||||
this._connectBtn = null;
|
||||
this._plotBtn = null;
|
||||
this._testBtn = null;
|
||||
this._bboxBtn = null;
|
||||
this._abortBtn = null;
|
||||
this._connectLabel = 'Connect';
|
||||
this._plotLabel = 'Plot';
|
||||
this._pauseLabel = 'Pause';
|
||||
this._resumeLabel = 'Resume';
|
||||
this._testLabel = 'Test';
|
||||
this._stopLabel = 'Stop';
|
||||
this._bboxLabel = 'Bounding box';
|
||||
this._abortLabel = 'Abort';
|
||||
this._bboxRunning = false;
|
||||
this._lastTestBtnClick = 0;
|
||||
this._lastPlotBtnClick = 0;
|
||||
this._pathStartX = 0;
|
||||
@@ -225,10 +227,12 @@ class WallPlotter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Connect / Plot / Test buttons. Expects global sketch functions plot() and test().
|
||||
* Connect is shown only while disconnected. Plot becomes Pause while running, Resume while paused.
|
||||
* Abort stops and resets the Test preview to the start.
|
||||
* @param {{parentId?: string, connectLabel?: string, plotLabel?: string, pauseLabel?: string, resumeLabel?: string, testLabel?: string, stopLabel?: string, abortLabel?: string}} [options]
|
||||
* Add Connect / Test / Plot / Bounding box / Abort.
|
||||
* No sketch yet: Test (+ Connect if disconnected). After Test, Bounding box
|
||||
* appears; Plot appears once connected. Plot sends that path (does not regenerate) and
|
||||
* becomes Pause / Resume while plotting. Abort only while plotting or when a plot is saved.
|
||||
* Bounding box travels pen-up around the drawing extents.
|
||||
* @param {{parentId?: string, connectLabel?: string, plotLabel?: string, pauseLabel?: string, resumeLabel?: string, testLabel?: string, bboxLabel?: string, abortLabel?: string}} [options]
|
||||
*/
|
||||
enablePlotButtons(options = {}) {
|
||||
if (this._plotButtonsEnabled) return;
|
||||
@@ -249,7 +253,7 @@ class WallPlotter {
|
||||
this._pauseLabel = options.pauseLabel ?? 'Pause';
|
||||
this._resumeLabel = options.resumeLabel ?? 'Resume';
|
||||
this._testLabel = options.testLabel ?? 'Test';
|
||||
this._stopLabel = options.stopLabel ?? 'Stop';
|
||||
this._bboxLabel = options.bboxLabel ?? 'Bounding box';
|
||||
this._abortLabel = options.abortLabel ?? 'Abort';
|
||||
|
||||
const plotter = this;
|
||||
@@ -258,19 +262,24 @@ class WallPlotter {
|
||||
connectBtn.mousePressed(() => plotter._onConnectButton());
|
||||
this._connectBtn = connectBtn;
|
||||
|
||||
const plotBtn = createButton(this._plotLabel);
|
||||
plotBtn.parent(parent);
|
||||
plotBtn.mousePressed(() => plotter._onPlotButton());
|
||||
this._plotBtn = plotBtn;
|
||||
|
||||
const testBtn = createButton(this._testLabel);
|
||||
testBtn.parent(parent);
|
||||
testBtn.mousePressed(() => plotter._onTestButton());
|
||||
this._testBtn = testBtn;
|
||||
|
||||
const plotBtn = createButton(this._plotLabel);
|
||||
plotBtn.parent(parent);
|
||||
plotBtn.mousePressed(() => plotter._onPlotButton());
|
||||
this._plotBtn = plotBtn;
|
||||
|
||||
const bboxBtn = createButton(this._bboxLabel);
|
||||
bboxBtn.parent(parent);
|
||||
bboxBtn.mousePressed(() => plotter._onBoundsButton());
|
||||
this._bboxBtn = bboxBtn;
|
||||
|
||||
const abortBtn = createButton(this._abortLabel);
|
||||
abortBtn.parent(parent);
|
||||
abortBtn.mousePressed(() => plotter.abortToStart());
|
||||
abortBtn.mousePressed(() => plotter._abortLivePlot());
|
||||
this._abortBtn = abortBtn;
|
||||
this._updatePlotButtons();
|
||||
}
|
||||
@@ -954,6 +963,10 @@ class WallPlotter {
|
||||
|
||||
/** G92 X0 Y0 — set current pose as home. Refused while plotting or waiting for Marlin. */
|
||||
async setHomeHere() {
|
||||
if (this._bboxRunning) {
|
||||
this.onLog('! cannot set home while travelling the bounding box');
|
||||
return;
|
||||
}
|
||||
if (this._job !== 'idle') {
|
||||
this.onLog('! cannot set home while plotting — resume or abort first');
|
||||
this.statusText = 'Finish or abort plot before H';
|
||||
@@ -1530,7 +1543,7 @@ class WallPlotter {
|
||||
}
|
||||
|
||||
_isMachineIdle() {
|
||||
return this._job === 'idle' && !this._okWaiter;
|
||||
return this._job === 'idle' && !this._okWaiter && !this._bboxRunning;
|
||||
}
|
||||
|
||||
_showHomeRequiredWarning() {
|
||||
@@ -1561,17 +1574,26 @@ class WallPlotter {
|
||||
return false;
|
||||
}
|
||||
if (k === 's') {
|
||||
await this.stopPlot();
|
||||
if (!this._isLivePlot()) return false;
|
||||
if (this._job === 'paused') this.resumePlot();
|
||||
else if (this._job === 'running') this.pausePlot();
|
||||
return false;
|
||||
}
|
||||
if (k === 'a') {
|
||||
this.abortToStart();
|
||||
if (!this._isLivePlot() && !this._hasSavedPlot()) return false;
|
||||
this._abortLivePlot();
|
||||
return false;
|
||||
}
|
||||
if (k === 't') {
|
||||
if (this._isLivePlot() || this._bboxRunning) return false;
|
||||
this._onTestButton();
|
||||
return false;
|
||||
}
|
||||
if (k === 'b') {
|
||||
if (this._isLivePlot() || this._bboxRunning) return false;
|
||||
this._onBoundsButton();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this._keysEnabled || !this.connected) return;
|
||||
|
||||
@@ -1676,41 +1698,136 @@ class WallPlotter {
|
||||
this._toggleConnection();
|
||||
}
|
||||
|
||||
_isLivePlot() {
|
||||
return this._job !== 'idle' && !this._plotTestOnly;
|
||||
}
|
||||
|
||||
_hasSavedPlot() {
|
||||
return !!(this._interrupted && !this._interrupted.testOnly);
|
||||
}
|
||||
|
||||
_hasDrawnSketch() {
|
||||
return this.previewPath.length > 0;
|
||||
}
|
||||
|
||||
_abortLivePlot() {
|
||||
if (this._hasSavedPlot() && this._job === 'idle') {
|
||||
this._clearResumeState();
|
||||
this.previewPath = [];
|
||||
this._path = [];
|
||||
this.plotProgress = 0;
|
||||
this.onLog('saved plot discarded — Test this sketch before Plot');
|
||||
this._syncStatus();
|
||||
this._updatePlotButtons();
|
||||
this._redrawSketch();
|
||||
return;
|
||||
}
|
||||
if (!this._isLivePlot()) return;
|
||||
this._clearResumeState();
|
||||
this._abortJob();
|
||||
this.onLog(
|
||||
'plot aborted — motors finish the in-flight move (unplug power for a hard stop)'
|
||||
);
|
||||
}
|
||||
|
||||
async _plotExistingPath() {
|
||||
const commands = this.previewPath;
|
||||
if (!commands.length || !this.connected) return;
|
||||
if (this._pathLoopActive) this._abortJob();
|
||||
while (this._pathLoopActive) await this._delay(16);
|
||||
await this._executePath(commands, false);
|
||||
}
|
||||
|
||||
_onPlotButton() {
|
||||
if (this._job === 'paused') {
|
||||
if (this._bboxRunning) return;
|
||||
if (this._isLivePlot() && this._job === 'paused') {
|
||||
this.resumePlot();
|
||||
return;
|
||||
}
|
||||
if (this._job === 'running') {
|
||||
if (this._isLivePlot() && this._job === 'running') {
|
||||
this.pausePlot();
|
||||
return;
|
||||
}
|
||||
if (this._job === 'aborting') return;
|
||||
if (this._job !== 'idle' && this._plotTestOnly) {
|
||||
this._plotExistingPath();
|
||||
return;
|
||||
}
|
||||
if (this._job !== 'idle' || !this.connected) return;
|
||||
const now = typeof millis === 'function' ? millis() : Date.now();
|
||||
if (this._interrupted && !this._interrupted.testOnly) {
|
||||
if (now - this._lastPlotBtnClick < 450) {
|
||||
this._lastPlotBtnClick = now;
|
||||
this._clearResumeState();
|
||||
this._triggerSketchFn('plot');
|
||||
return;
|
||||
}
|
||||
this._lastPlotBtnClick = now;
|
||||
this._resumeInterruptedPath();
|
||||
return;
|
||||
}
|
||||
this._lastPlotBtnClick = now;
|
||||
this._triggerSketchFn('plot');
|
||||
this._plotExistingPath();
|
||||
}
|
||||
|
||||
_previewBounds() {
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
let n = 0;
|
||||
for (const cmd of this.previewPath) {
|
||||
if (cmd.type !== 'move' && cmd.type !== 'draw') continue;
|
||||
minX = Math.min(minX, cmd.x);
|
||||
maxX = Math.max(maxX, cmd.x);
|
||||
minY = Math.min(minY, cmd.y);
|
||||
maxY = Math.max(maxY, cmd.y);
|
||||
n += 1;
|
||||
}
|
||||
if (!n) return null;
|
||||
return { minX, minY, maxX, maxY };
|
||||
}
|
||||
|
||||
async _onBoundsButton() {
|
||||
if (this._isLivePlot() || this._bboxRunning) return;
|
||||
if (!this.connected || !this._requireHome()) return;
|
||||
const box = this._previewBounds();
|
||||
if (!box) return;
|
||||
if (this._job !== 'idle' && this._plotTestOnly) {
|
||||
this._abortJob();
|
||||
while (this._pathLoopActive) await this._delay(16);
|
||||
}
|
||||
if (this._job !== 'idle') return;
|
||||
this._bboxRunning = true;
|
||||
this._updatePlotButtons();
|
||||
const { minX, minY, maxX, maxY } = box;
|
||||
this.onLog(
|
||||
`bounding box ${minX.toFixed(0)},${minY.toFixed(0)} … ${maxX.toFixed(0)},${maxY.toFixed(0)} mm (pen up)`
|
||||
);
|
||||
try {
|
||||
await this.penUp();
|
||||
const corners = [
|
||||
{ type: 'move', x: minX, y: minY },
|
||||
{ type: 'move', x: maxX, y: minY },
|
||||
{ type: 'move', x: maxX, y: maxY },
|
||||
{ type: 'move', x: minX, y: maxY },
|
||||
{ type: 'move', x: minX, y: minY },
|
||||
];
|
||||
let px = this.x;
|
||||
let py = this.y;
|
||||
this.statusText = 'Bounding box (pen up)';
|
||||
for (const cmd of corners) {
|
||||
const duration = this._segmentDurationMs(px, py, cmd);
|
||||
this._lastSegmentMs = duration;
|
||||
await Promise.all([
|
||||
this._sendSegmentAndWait(cmd),
|
||||
this._animatePosition(px, py, cmd, duration),
|
||||
]);
|
||||
px = cmd.x;
|
||||
py = cmd.y;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.name !== 'AbortError') this.onLog('! ' + err.message);
|
||||
} finally {
|
||||
this._bboxRunning = false;
|
||||
this._syncStatus();
|
||||
this._updatePlotButtons();
|
||||
}
|
||||
}
|
||||
|
||||
_onTestButton() {
|
||||
const now = millis();
|
||||
if (this._job === 'running' && this._plotTestOnly) {
|
||||
if (now - this._lastTestBtnClick < 450) this._restartPath('test');
|
||||
else this.stopPlot();
|
||||
this._lastTestBtnClick = now;
|
||||
return;
|
||||
}
|
||||
this._lastTestBtnClick = now;
|
||||
if (this._isLivePlot() || this._bboxRunning) return;
|
||||
this._restartPath('test');
|
||||
}
|
||||
|
||||
@@ -1721,31 +1838,44 @@ class WallPlotter {
|
||||
}
|
||||
|
||||
_updatePlotButtons() {
|
||||
const livePlot = this._isLivePlot();
|
||||
const savedPlot = this._hasSavedPlot();
|
||||
const sketchVisible = this._hasDrawnSketch() || livePlot || savedPlot;
|
||||
|
||||
if (this._connectBtn) {
|
||||
if (this.connected) this._connectBtn.hide();
|
||||
else this._connectBtn.show();
|
||||
this._connectBtn.elt.disabled = this._link === 'connecting';
|
||||
}
|
||||
if (this._testBtn) {
|
||||
if (livePlot) this._testBtn.hide();
|
||||
else this._testBtn.show();
|
||||
this._testBtn.html(this._testLabel);
|
||||
this._testBtn.elt.disabled = this._bboxRunning;
|
||||
}
|
||||
if (this._plotBtn) {
|
||||
if (this.connected && sketchVisible) this._plotBtn.show();
|
||||
else this._plotBtn.hide();
|
||||
let label = this._plotLabel;
|
||||
if (this._job === 'paused') label = this._resumeLabel;
|
||||
else if (this._interrupted && this._job === 'idle') label = this._resumeLabel;
|
||||
else if (this._job === 'running' || this._job === 'aborting') {
|
||||
if (this._job === 'paused' && livePlot) label = this._resumeLabel;
|
||||
else if (savedPlot && this._job === 'idle') label = this._resumeLabel;
|
||||
else if (livePlot && (this._job === 'running' || this._job === 'aborting')) {
|
||||
label = this._pauseLabel;
|
||||
}
|
||||
this._plotBtn.html(label);
|
||||
this._plotBtn.elt.disabled =
|
||||
this._job === 'aborting' || (!this.connected && this._job === 'idle');
|
||||
this._job === 'aborting' || this._bboxRunning;
|
||||
}
|
||||
if (this._testBtn) {
|
||||
this._testBtn.html(
|
||||
this._job !== 'idle' && this._plotTestOnly ? this._stopLabel : this._testLabel
|
||||
);
|
||||
if (this._bboxBtn) {
|
||||
if (sketchVisible && !livePlot) this._bboxBtn.show();
|
||||
else this._bboxBtn.hide();
|
||||
this._bboxBtn.elt.disabled =
|
||||
this._bboxRunning || !this.connected || !this.homeSet;
|
||||
}
|
||||
if (this._abortBtn) {
|
||||
const active =
|
||||
this._job !== 'idle' || !!this._interrupted || this.previewPath.length > 0;
|
||||
this._abortBtn.elt.disabled = !active;
|
||||
if (livePlot || savedPlot) this._abortBtn.show();
|
||||
else this._abortBtn.hide();
|
||||
this._abortBtn.elt.disabled = this._job === 'aborting';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1762,9 +1892,12 @@ class WallPlotter {
|
||||
}
|
||||
|
||||
_injectPlotButtonStyles() {
|
||||
if (document.getElementById('wallplotter-btn-styles')) return;
|
||||
const style = document.createElement('style');
|
||||
style.id = 'wallplotter-btn-styles';
|
||||
let style = document.getElementById('wallplotter-btn-styles');
|
||||
if (!style) {
|
||||
style = document.createElement('style');
|
||||
style.id = 'wallplotter-btn-styles';
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
style.textContent = `
|
||||
#wallplotter-controls {
|
||||
position: absolute;
|
||||
@@ -1772,10 +1905,14 @@ class WallPlotter {
|
||||
right: 0.75rem;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
margin: 0;
|
||||
max-width: calc(100vw - 1.5rem);
|
||||
}
|
||||
#wallplotter-controls button {
|
||||
#wallplotter-controls button,
|
||||
#wallplotter-log-toggle {
|
||||
background: rgba(42, 42, 48, 0.9);
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
@@ -1786,7 +1923,8 @@ class WallPlotter {
|
||||
line-height: 1;
|
||||
padding: 0.4rem 1rem;
|
||||
}
|
||||
#wallplotter-controls button:hover {
|
||||
#wallplotter-controls button:hover,
|
||||
#wallplotter-log-toggle:hover {
|
||||
background: #35353c;
|
||||
border-color: #666;
|
||||
color: #eee;
|
||||
@@ -1806,29 +1944,38 @@ class WallPlotter {
|
||||
right: 0.75rem;
|
||||
bottom: 0.75rem;
|
||||
z-index: 20;
|
||||
max-width: min(52rem, calc(100vw - 1.5rem));
|
||||
box-sizing: border-box;
|
||||
width: min(40rem, calc(100vw - 1.5rem));
|
||||
pointer-events: auto;
|
||||
background: #141418;
|
||||
border: none;
|
||||
border: 1px solid #333;
|
||||
border-radius: 6px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
#wallplotter-log-toggle {
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: #141418;
|
||||
#wallplotter-log.collapsed {
|
||||
width: auto;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #aaa;
|
||||
cursor: pointer;
|
||||
}
|
||||
#wallplotter-log-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
padding: 0.35rem 0.4rem 0.35rem 0.65rem;
|
||||
}
|
||||
#wallplotter-log.collapsed #wallplotter-log-header {
|
||||
padding: 0;
|
||||
}
|
||||
#wallplotter-log-title {
|
||||
color: #ccc;
|
||||
font-family: system-ui, sans-serif;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.04em;
|
||||
text-align: left;
|
||||
padding: 0.4rem 0.65rem;
|
||||
}
|
||||
#wallplotter-log-toggle:hover {
|
||||
color: #eee;
|
||||
#wallplotter-log.collapsed #wallplotter-log-title {
|
||||
display: none;
|
||||
}
|
||||
#wallplotter-log-body {
|
||||
max-height: 38vh;
|
||||
@@ -1844,13 +1991,13 @@ class WallPlotter {
|
||||
margin: 0;
|
||||
padding: 0 0.65rem 0.5rem;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
#wallplotter-log.collapsed #wallplotter-log-body {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
@@ -1932,6 +2079,7 @@ class WallPlotter {
|
||||
this._drawBoundSizeLabels(bounds, tl, br, w, h);
|
||||
|
||||
this._drawPreviewPath(layout);
|
||||
this._drawSketchBounds(layout);
|
||||
this._drawGondola(layout);
|
||||
|
||||
pop();
|
||||
@@ -1958,6 +2106,20 @@ class WallPlotter {
|
||||
pop();
|
||||
}
|
||||
|
||||
_drawSketchBounds(layout) {
|
||||
const box = this._previewBounds();
|
||||
if (!box) return;
|
||||
const tl = this._worldToScreen(box.minX, box.maxY, layout);
|
||||
const br = this._worldToScreen(box.maxX, box.minY, layout);
|
||||
noFill();
|
||||
stroke(90, 200, 255, 180);
|
||||
strokeWeight(1);
|
||||
drawingContext.setLineDash([6, 5]);
|
||||
rectMode(CORNER);
|
||||
rect(tl.x, tl.y, br.x - tl.x, br.y - tl.y);
|
||||
drawingContext.setLineDash([]);
|
||||
}
|
||||
|
||||
_drawPreviewPath(layout) {
|
||||
if (!this.previewPath.length) return;
|
||||
|
||||
@@ -1990,6 +2152,7 @@ class WallPlotter {
|
||||
|
||||
_drawGondola(layout) {
|
||||
const pos = this._worldToScreen(this.x, this.y, layout);
|
||||
const moving = this.isPlotting || this._bboxRunning;
|
||||
if (this.isPenDown) {
|
||||
stroke(255, 90, 90);
|
||||
fill(255, 90, 90);
|
||||
@@ -1998,8 +2161,8 @@ class WallPlotter {
|
||||
fill(90, 200, 255);
|
||||
}
|
||||
strokeWeight(2);
|
||||
circle(pos.x, pos.y, this.isPlotting ? 18 : 12);
|
||||
if (this.isPlotting) {
|
||||
circle(pos.x, pos.y, moving ? 18 : 12);
|
||||
if (moving) {
|
||||
noFill();
|
||||
stroke(255, 255, 255, 120);
|
||||
strokeWeight(1);
|
||||
@@ -2085,8 +2248,9 @@ class WallPlotter {
|
||||
'0 go to origin (0, 0)',
|
||||
'P toggle pen (test up/down angles)',
|
||||
'T run test (preview)',
|
||||
'A abort / reset test to start',
|
||||
'S stop sending (in-flight stroke still finishes)',
|
||||
'B travel drawing bounds (pen up)',
|
||||
'A abort plot (in-flight stroke still finishes)',
|
||||
'S pause / resume plot',
|
||||
'C connect / disconnect',
|
||||
];
|
||||
for (let i = 0; i < help.length; i++) {
|
||||
@@ -2099,28 +2263,39 @@ class WallPlotter {
|
||||
this._injectPlotButtonStyles();
|
||||
let wrap = document.getElementById('wallplotter-log');
|
||||
let body = document.getElementById('wallplotter-log-body');
|
||||
if (wrap && body) return body;
|
||||
const header = document.getElementById('wallplotter-log-header');
|
||||
if (wrap && body && header) return body;
|
||||
if (wrap) wrap.remove();
|
||||
|
||||
wrap = document.createElement('div');
|
||||
wrap.id = 'wallplotter-log';
|
||||
wrap.setAttribute('aria-label', 'Serial log');
|
||||
|
||||
const headerEl = document.createElement('div');
|
||||
headerEl.id = 'wallplotter-log-header';
|
||||
|
||||
const title = document.createElement('span');
|
||||
title.id = 'wallplotter-log-title';
|
||||
title.textContent = 'Serial';
|
||||
|
||||
const toggle = document.createElement('button');
|
||||
toggle.id = 'wallplotter-log-toggle';
|
||||
toggle.type = 'button';
|
||||
toggle.textContent = 'Serial';
|
||||
toggle.textContent = 'Hide';
|
||||
toggle.setAttribute('aria-expanded', 'true');
|
||||
toggle.addEventListener('click', () => {
|
||||
wrap.classList.toggle('collapsed');
|
||||
toggle.textContent = wrap.classList.contains('collapsed') ? 'Serial ▸' : 'Serial ▾';
|
||||
const collapsed = wrap.classList.toggle('collapsed');
|
||||
toggle.textContent = collapsed ? 'serial-log' : 'Hide';
|
||||
toggle.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
||||
});
|
||||
|
||||
body = document.createElement('pre');
|
||||
body.id = 'wallplotter-log-body';
|
||||
wrap.appendChild(toggle);
|
||||
headerEl.appendChild(title);
|
||||
headerEl.appendChild(toggle);
|
||||
wrap.appendChild(headerEl);
|
||||
wrap.appendChild(body);
|
||||
document.body.appendChild(wrap);
|
||||
toggle.textContent = 'Serial ▾';
|
||||
return body;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user