collapsable toolbars

This commit is contained in:
Ladd 2025-12-28 15:19:58 -06:00
parent 54868aeba8
commit a6eda0226b
14 changed files with 223 additions and 152 deletions

View File

@ -2,7 +2,7 @@
export const DISPLAY_OBJECTS_INFO = false; export const DISPLAY_OBJECTS_INFO = false;
export const DISPLAY_CURSOR_INFO = false; export const DISPLAY_CURSOR_INFO = false;
export const DISPLAY_CANVAS_SIZE = false; export const DISPLAY_CANVAS_SIZE = false;
export const DISPLAY_CURRENT_SCALE = true; export const DISPLAY_CURRENT_SCALE = false;
export const DISPLAY_CURRENT_MODE = false; export const DISPLAY_CURRENT_MODE = false;
export const DISPLAY_VELOCITY_VECTORS = true; export const DISPLAY_VELOCITY_VECTORS = true;
export const DISPLAY_ACCELERATION_VECTORS = true; export const DISPLAY_ACCELERATION_VECTORS = true;
@ -21,7 +21,6 @@ export const ACCELERATION_VECTOR_WIDTH = 1.5;
export const ACCELERATION_VECTOR_ARROWHEAD = true; export const ACCELERATION_VECTOR_ARROWHEAD = true;
// PATH TRACES // PATH TRACES
// export const PATH_TRACES_COLOR = 'rgb(128, 128, 0)'; // optionally set to 'object color'
export const PATH_TRACES_COLOR = 'object color'; export const PATH_TRACES_COLOR = 'object color';
export const PATH_TRACES_OPACITY = 0.8; export const PATH_TRACES_OPACITY = 0.8;
export const PATH_TRACES_WIDTH = 1.5; export const PATH_TRACES_WIDTH = 1.5;
@ -46,13 +45,15 @@ export const SCALE_POWER_MIN = -8;
export const GRAVITATIONAL_CONSTANT = 1E5; export const GRAVITATIONAL_CONSTANT = 1E5;
// CSS CLASS NAMES // CSS CLASS NAMES
export const DRAGGABLE_ELEMENT_CLASSNAME = 'lhg-draggable-element';
export const TOOL_CLASSNAME = 'lhg-tool'; export const TOOL_CLASSNAME = 'lhg-tool';
export const TOOL_INFO_CLASSNAME = 'lhg-tool-info';
export const TOOLBAR_CLASSNAME = 'lhg-toolbar'; export const TOOLBAR_CLASSNAME = 'lhg-toolbar';
export const WIDE_CLASSNAME = 'lhg-wide';
// EVENT NAMES // EVENT NAMES
export const EVENT_MODE_LEAVE = 'lhg-mode-leave'; export const EVENT_MODE_LEAVE = 'lhg-mode-leave';
export const EVENT_MODE_ENTER = 'lhg-mode-enter'; export const EVENT_MODE_ENTER = 'lhg-mode-enter';
export const EVENT_ZOOM = 'lhg-zoom-event';
// MODES // MODES
export const MODE_MASS_GENERATION = 'mass-gen'; export const MODE_MASS_GENERATION = 'mass-gen';

View File

@ -3,60 +3,7 @@
<head> <head>
<title>Gravity Simulator</title> <title>Gravity Simulator</title>
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<style> <link rel="stylesheet" href="./style.css" />
body {
background-color: #000;
color: #8f8;
font-family: monospace;
font-size: 12pt;
overflow: hidden;
user-select: none;
-webkit-user-select: none;
}
div[id=simulator] {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
button {
width: 8em;
border-radius: 0.5EM;
border-color: #000;
border-width: 2px;
/* margin-left: 0.5em; */
/* margin-right: 0.5em; */
/* padding-top: 0; */
/* padding-bottom: 0; */
}
button.wide {
width: 16em;
}
div.lhg-tool {
position: relative;
top: 0;
left: 0;
width: 14EM;
/* border: 1px #0fb solid; */
/* margin: 1EM; */
padding: 1EM;
}
div.lhg-toolbar {
position: fixed;
z-index: 2;
width: fit-content;
/* border: 2px #00f dashed; */
/* margin: '1EM'; */
/* padding: '1EM'; */
}
</style>
<script type="module"> <script type="module">
import { Sim } from './simulator.js'; import { Sim } from './simulator.js';
const sim = new Sim('simulator'); const sim = new Sim('simulator');

View File

@ -1,5 +1,3 @@
import {DRAGGABLE_ELEMENT_CLASSNAME} from './config.js';
export class Overlay { export class Overlay {
sim = undefined; sim = undefined;
constructor(sim) { constructor(sim) {
@ -12,10 +10,9 @@ export class Overlay {
infoBox.style.position = 'relative'; infoBox.style.position = 'relative';
infoBox.style.display = 'inline-block'; infoBox.style.display = 'inline-block';
infoBox.style.top = 0; infoBox.style.top = 0;
infoBox.style.left = 0; infoBox.style.left = '14em';
infoBox.width = 'fit-content'; infoBox.width = 'fit-content';
infoBox.style.zIndex = 1; infoBox.style.zIndex = 1;
infoBox.classList.add(DRAGGABLE_ELEMENT_CLASSNAME);
} }
renderInfo() { renderInfo() {
@ -36,23 +33,4 @@ export class Overlay {
} }
this.infoBox.appendChild(table); this.infoBox.appendChild(table);
} }
// Update positions of draggable items
updateDraggable() {
const elements = document.querySelectorAll(`.${DRAGGABLE_ELEMENT_CLASSNAME}`);
for (let el of elements) {
if (!el.dragging) continue;
const {
elementStart: {x: x0, y: y0},
pointerStart: {x: x1, y: y1},
pointerEnd: {x: x2, y: y2}
} = el.dragging;
const dx = x2 - x1;
const dy = y2 - y1;
const left = x0 + dx;
const top = y0 + dy;
el.style.left = `${left}px`;
el.style.top = `${top}px`;
}
}
} }

View File

@ -1,6 +1,5 @@
import { import {
DISPLAY_CURSOR_INFO, DISPLAY_CURSOR_INFO,
DRAGGABLE_ELEMENT_CLASSNAME,
MODE_MASS_GENERATION, MODE_MASS_GENERATION,
MODE_OBJECT_SELECT, MODE_OBJECT_SELECT,
MODE_PAN_VIEW, MODE_PAN_VIEW,
@ -13,7 +12,6 @@ export class Pointer {
sim = undefined; sim = undefined;
pointerHistory = []; pointerHistory = [];
draggingElement = undefined;
panning = undefined; // { velocity: {x and y in sim coordinates}, paused: boolean } panning = undefined; // { velocity: {x and y in sim coordinates}, paused: boolean }
panTouchStart = undefined; // {x: undefined, y: undefined, t: undefined}; panTouchStart = undefined; // {x: undefined, y: undefined, t: undefined};
panTouchLatest = undefined; // {x: undefined, y: undefined, t: undefined}; panTouchLatest = undefined; // {x: undefined, y: undefined, t: undefined};
@ -29,53 +27,20 @@ export class Pointer {
if (DISPLAY_CURSOR_INFO) { if (DISPLAY_CURSOR_INFO) {
this.sim.info['pointermove'] = [`${e.clientX}, `, `${e.clientY}`]; this.sim.info['pointermove'] = [`${e.clientX}, `, `${e.clientY}`];
} }
this.handlePointerMove({x: e.clientX, y: e.clientY});
if (this.draggingElement) {
this.draggingElement.dragging.pointerEnd = {
x: e.clientX,
y: e.clientY,
};
} else {
this.handlePointerMove({x: e.clientX, y: e.clientY});
}
}); });
el.addEventListener('pointerdown', e => { el.addEventListener('pointerdown', e => {
// If this is a child of a draggable element, handle dragging
let target = e.target; let target = e.target;
while (target && !target.classList.contains(DRAGGABLE_ELEMENT_CLASSNAME)) { if (target.nodeName.toLowerCase() === 'button') {
target = target.parentElement; return;
} }
if (target?.classList.contains(DRAGGABLE_ELEMENT_CLASSNAME)) { this.handlePointerDown({x: e.clientX, y: e.clientY});
this.draggingElement = target;
this.draggingElement.dragging = {
elementStart: {
x: parseInt(this.draggingElement.style.left),
y: parseInt(this.draggingElement.style.top),
},
pointerStart: {
x: e.clientX,
y: e.clientY,
},
pointerEnd: {
x: e.clientX,
y: e.clientY,
},
};
} else {
this.handlePointerDown({x: e.clientX, y: e.clientY});
}
}); });
el.addEventListener('pointerup', e => { el.addEventListener('pointerup', e => {
if (this.draggingElement) { this.handlePointerUp({x: e.clientX, y: e.clientY});
this.draggingElement.dragging = undefined;
this.draggingElement = undefined;
this.lastPosition = {x: undefined, y: undefined};
} else {
this.handlePointerUp({x: e.clientX, y: e.clientY});
}
}); });
// Monitor wheel events // Monitor wheel events

View File

@ -13,6 +13,7 @@ import {
DISPLAY_CURRENT_SCALE, DISPLAY_CURRENT_SCALE,
DISPLAY_CURRENT_MODE, DISPLAY_CURRENT_MODE,
MOTION_TIME_SCALE, MOTION_TIME_SCALE,
EVENT_ZOOM,
} from './config.js'; } from './config.js';
export class Sim { export class Sim {
@ -41,9 +42,10 @@ export class Sim {
this.div = div; this.div = div;
this.display = new Display(this); this.display = new Display(this);
this.overlay = new Overlay(this);
this.objects = new Objects(this); this.objects = new Objects(this);
this.toolbar = new Toolbar(this); this.toolbar = new Toolbar(this, 'Tools');
this.toolbar2 = new Toolbar(this, 'Options');
this.overlay = new Overlay(this);
// Set up toolbar // Set up toolbar
this.toolbar.addTool(new Zoom(this.toolbar)); this.toolbar.addTool(new Zoom(this.toolbar));
@ -51,7 +53,7 @@ export class Sim {
this.toolbar.addTool(new ModeSwitch(this.toolbar)); this.toolbar.addTool(new ModeSwitch(this.toolbar));
// Set up second toolbar // Set up second toolbar
this.toolbar2 = new Toolbar(this).topRight(); this.toolbar2.topRight();
this.toolbar2.addTool(new Options(this.toolbar)); this.toolbar2.addTool(new Options(this.toolbar));
this.pointer = new Pointer(this); this.pointer = new Pointer(this);
@ -92,6 +94,9 @@ export class Sim {
} }
}; };
} }
const e = new CustomEvent(EVENT_ZOOM);
this.div.dispatchEvent(e);
} }
// Transform display coordinates to simulator coordinates using scale and viewOrigin // Transform display coordinates to simulator coordinates using scale and viewOrigin
@ -107,6 +112,18 @@ export class Sim {
this.playing = false; this.playing = false;
} }
getScaleDisplay() {
const scale = 2 ** Math.abs(this.display.scalePower);
return this.display.scalePower >= 0 ? `${scale}` : `1/${scale}`;
}
// cb: () => undefined
onZoom(cb) {
this.div.addEventListener(EVENT_ZOOM, () => {
cb();
});
}
// Main loop // Main loop
loop(currentTime) { loop(currentTime) {
const elapsedTime = (currentTime - this.time) * MOTION_TIME_SCALE; const elapsedTime = (currentTime - this.time) * MOTION_TIME_SCALE;
@ -122,16 +139,12 @@ export class Sim {
} }
if (DISPLAY_CURRENT_SCALE) { if (DISPLAY_CURRENT_SCALE) {
const scale = 2 ** Math.abs(this.display.scalePower); this.info['Scale'] = this.getScaleDisplay();
this.info['Scale'] = this.display.scalePower >= 0 ? `${scale}` : `1/${scale}`;
} }
this.pointer.computeFrame(elapsedTime); this.pointer.computeFrame(elapsedTime);
this.objects.computeFrame(elapsedTime); this.objects.computeFrame(elapsedTime);
this.overlay.updateDraggable();
this.overlay.renderInfo(); this.overlay.renderInfo();
this.display.fillCanvas(); this.display.fillCanvas();
this.display.drawObjects(); this.display.drawObjects();

91
style.css Normal file
View File

@ -0,0 +1,91 @@
body {
background-color: #000;
color: #5f5;
font-family: monospace;
font-size: 10pt;
overflow: hidden;
user-select: none;
-webkit-user-select: none;
}
h1, h2, h3, h4, h5, h6 {
margin-block-start: 4pt;
margin-block-end: 4pt;
text-align: center;
font-size: 12pt;
font-weight: normal;
}
h1 {
font-size: 16pt;
}
div[id=simulator] {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
div.lhg-toolbar {
position: fixed;
z-index: 2;
width: fit-content;
border-radius: 0.5EM;
}
div.lhg-toolbar div.lhg-tool {
position: relative;
top: 0;
left: 0;
width: 12EM;
padding: 0.5EM;
margin: 0.5EM;
border-radius: 0.5EM;
border-width: 1px;
border-style: solid;
border-color: #282;
}
div.lhg-tool button {
font-family: monospace;
font-size: 10pt;
width: 6em;
background-color: #333;
color: #5f5;
border-radius: 0.5EM;
border-color: #000;
border-width: 4px;
border-style: solid;
padding-top: 0.5EM;
padding-bottom: 0.5EM;
padding-left: 0EM;
padding-right: 0EM;
text-align: center;
/* margin-top: 0.125EM;
margin-bottom: 0.125EM; */
margin-left: 0;
margin-right: 0;
}
div.lhg-tool button:hover {
background-color: #444;
}
div.lhg-tool button:active {
background-color: #252;
}
div.lhg-tool button.lhg-tool-info {
background-color: #111;
border-color: #282;
border-width: 0px;
}
div.lhg-tool button.lhg-wide {
width: 12em;
}
div.lhg-tool > div {
/* border: 2px red solid; */
}

14
sync
View File

@ -5,8 +5,18 @@ if [[ $(hostname) != "ladd76" ]]; then
exit 1 exit 1
fi fi
rsync -ru --exclude='.git' --exclude='node_modules' ~/code/gravity-dev/ lentilz:code/gravity-dev/ set -eo pipefail
rsync -ru --exclude='.git' --exclude='node_modules' lentilz:code/gravity-dev/ ~/code/gravity-dev/
do_rsync() {
rsync -ru \
--exclude='.git' \
--exclude='node_modules' \
--exclude='.*.sw*' \
"$@"
}
do_rsync ~/code/gravity-dev/ lentilz:code/gravity-dev/
do_rsync lentilz:code/gravity-dev/ ~/code/gravity-dev/
echo >&2 echo >&2
echo >&2 "Synced with https://laddhoffman.com/gravity-dev/" echo >&2 "Synced with https://laddhoffman.com/gravity-dev/"

View File

@ -3,7 +3,6 @@
// through toolbar can access sim // through toolbar can access sim
import { import {
DRAGGABLE_ELEMENT_CLASSNAME,
TOOL_CLASSNAME, TOOL_CLASSNAME,
} from './config.js'; } from './config.js';
@ -17,7 +16,6 @@ export class Tool {
const div = document.createElement('div'); const div = document.createElement('div');
this.div = div; this.div = div;
div.classList.add(TOOL_CLASSNAME) div.classList.add(TOOL_CLASSNAME)
div.classList.add(DRAGGABLE_ELEMENT_CLASSNAME);
} }
frame() {} frame() {}

49
tool/header.js Normal file
View File

@ -0,0 +1,49 @@
import { Tool } from '../tool.js';
export class Header extends Tool {
expanded = true;
constructor(toolbar, title = 'Tools') {
super(toolbar);
this.title = document.createElement('h1');
this.title.innerHTML = title;
this.toggleButton = document.createElement('button');
this.updateButton();
this.toggleButton.addEventListener('click', () => this.toggle());
this.div.appendChild(this.title);
this.div.appendChild(this.toggleButton);
this.title.style.verticalAlign = 'center';
this.toggleButton.style.width = '3EM';
this.div.style.display = 'flex';
this.div.style.justifyContent = 'space-around';
}
updateButton() {
this.toggleButton.innerHTML = this.expanded ? '˄' : '˅';
}
toggle() {
this.expanded = !this.expanded;
this.updateButton();
this.apply();
}
apply() {
for (const tool of this.toolbar.tools) {
if (tool === this) continue;
if (this.expanded) {
if (!this.toolbar.div.contains(tool.div)) {
this.toolbar.div.appendChild(tool.div);
}
} else {
if (this.toolbar.div.contains(tool.div)) {
this.toolbar.div.removeChild(tool.div);
}
}
}
}
}

View File

@ -6,6 +6,7 @@ import {
MODE_OBJECT_SELECT, MODE_OBJECT_SELECT,
EVENT_MODE_LEAVE, EVENT_MODE_LEAVE,
EVENT_MODE_ENTER, EVENT_MODE_ENTER,
WIDE_CLASSNAME,
} from '../config.js'; } from '../config.js';
export class ModeSwitch extends Tool { export class ModeSwitch extends Tool {
@ -21,7 +22,11 @@ export class ModeSwitch extends Tool {
super(toolbar); super(toolbar);
const modesDiv = document.createElement('div'); const modesDiv = document.createElement('div');
const heading = document.createElement('h2');
heading.innerHTML = 'Modes';
// this.div.appendChild(heading);
this.div.appendChild(modesDiv); this.div.appendChild(modesDiv);
modesDiv.style.display = 'flex'; modesDiv.style.display = 'flex';
@ -32,8 +37,8 @@ export class ModeSwitch extends Tool {
this.buttons.push(button); this.buttons.push(button);
button.modeID = modeID; button.modeID = modeID;
modesDiv.appendChild(button); modesDiv.appendChild(button);
button.innerHTML = `<h3>${modeTitle}</h3>`; button.innerHTML = modeTitle;
button.classList.add('wide'); button.classList.add(WIDE_CLASSNAME);
button.addEventListener('click', () => this.setCurrentMode(modeID)); button.addEventListener('click', () => this.setCurrentMode(modeID));
} }

View File

@ -6,21 +6,22 @@ import {
PATH_TRACES_DASHED, PATH_TRACES_DASHED,
PAUSE_DURING_CREATION, PAUSE_DURING_CREATION,
PAUSE_DURING_SELECTION, PAUSE_DURING_SELECTION,
WIDE_CLASSNAME,
} from '../config.js'; } from '../config.js';
import {Tool} from '../tool.js'; import {Tool} from '../tool.js';
export class Options extends Tool { export class Options extends Tool {
options = [{ options = [{
type: 'group', name: 'pauseDuring', title: 'Pause During Mass', type: 'group', name: 'pauseDuring', title: 'Pause During',
items: [ items: [
{type: 'boolean', name: 'creation', title: 'Creation', default: PAUSE_DURING_CREATION}, {type: 'boolean', name: 'creation', title: 'Create', default: PAUSE_DURING_CREATION},
{type: 'boolean', name: 'selection', title: 'Selection', default: PAUSE_DURING_SELECTION}, {type: 'boolean', name: 'selection', title: 'Select', default: PAUSE_DURING_SELECTION},
] ]
}, { }, {
type: 'group', name: 'display', title: 'Display', type: 'group', name: 'display', title: 'Display',
items: [ items: [
{type: 'boolean', name: 'velocity', title: 'Velocity', default: DISPLAY_VELOCITY_VECTORS}, {type: 'boolean', name: 'velocity', title: 'Velocity', default: DISPLAY_VELOCITY_VECTORS},
{type: 'boolean', name: 'acceleration', title: 'Acceleration', default: DISPLAY_ACCELERATION_VECTORS}, {type: 'boolean', name: 'acceleration', title: 'Accel', default: DISPLAY_ACCELERATION_VECTORS},
{type: 'boolean', name: 'traces', title: 'Traces', default: DISPLAY_ACCELERATION_VECTORS}, {type: 'boolean', name: 'traces', title: 'Traces', default: DISPLAY_ACCELERATION_VECTORS},
{type: 'boolean', name: 'dashedTraces', title: 'Dashed', default: PATH_TRACES_DASHED}, {type: 'boolean', name: 'dashedTraces', title: 'Dashed', default: PATH_TRACES_DASHED},
] ]
@ -29,8 +30,7 @@ export class Options extends Tool {
items: [ items: [
{type: 'boolean', name: 'merge', title: 'Merge Masses', default: MERGE_ON_COLLIDE, wide: true}, {type: 'boolean', name: 'merge', title: 'Merge Masses', default: MERGE_ON_COLLIDE, wide: true},
] ]
}, }];
];
values = {}; values = {};
@ -50,9 +50,9 @@ export class Options extends Tool {
} }
case 'boolean': { case 'boolean': {
const button = document.createElement('button'); const button = document.createElement('button');
button.innerHTML = `<h4>${item.title}</h4>`; button.innerHTML = item.title;
if (item.wide === true) { if (item.wide === true) {
button.classList.add('wide'); button.classList.add(WIDE_CLASSNAME);
} }
this.setOption(path, item.default); this.setOption(path, item.default);
button.style.opacity = this.values[path] ? '100%' : '50%'; button.style.opacity = this.values[path] ? '100%' : '50%';
@ -71,7 +71,7 @@ export class Options extends Tool {
super(toolbar); super(toolbar);
const heading = document.createElement('h2'); const heading = document.createElement('h2');
heading.innerHTML = 'Options'; heading.innerHTML = 'Options';
this.div.appendChild(heading); // this.div.appendChild(heading);
for (const item of this.options) { for (const item of this.options) {
const child = this.visitItem(item); const child = this.visitItem(item);
this.div.appendChild(child); this.div.appendChild(child);

View File

@ -1,8 +1,8 @@
import { Tool } from '../tool.js'; import { Tool } from '../tool.js';
export class PlayPause extends Tool { export class PlayPause extends Tool {
playHTML = '<h2>Play</h2>'; playHTML = 'Play';
pauseHTML = '<h2>Pause</h2>'; pauseHTML = 'Pause';
constructor(toolbar) { constructor(toolbar) {
super(toolbar); super(toolbar);

View File

@ -1,5 +1,7 @@
import { Tool } from '../tool.js'; import { Tool } from '../tool.js';
import { import {
TOOL_INFO_CLASSNAME,
WIDE_CLASSNAME,
ZOOM_IN_FACTOR, ZOOM_IN_FACTOR,
ZOOM_OUT_FACTOR, ZOOM_OUT_FACTOR,
} from '../config.js'; } from '../config.js';
@ -8,21 +10,28 @@ export class Zoom extends Tool {
constructor(toolbar) { constructor(toolbar) {
super(toolbar); super(toolbar);
const currentScale = document.createElement('button')
const zoomOut = document.createElement('button'); const zoomOut = document.createElement('button');
const zoomIn = document.createElement('button'); const zoomIn = document.createElement('button');
const zoomAll = document.createElement('button'); const zoomAll = document.createElement('button');
this.div.appendChild(currentScale);
this.div.appendChild(zoomOut); this.div.appendChild(zoomOut);
this.div.appendChild(zoomIn); this.div.appendChild(zoomIn);
this.div.appendChild(document.createElement('br'));
this.div.appendChild(zoomAll); this.div.appendChild(zoomAll);
this.div.appendChild(document.createElement('br'));
zoomAll.classList.add('wide'); zoomAll.classList.add(WIDE_CLASSNAME);
currentScale.classList.add(WIDE_CLASSNAME);
currentScale.classList.add(TOOL_INFO_CLASSNAME);
zoomOut.innerHTML = '<h2>Zoom<br>Out</h2>'; zoomOut.innerHTML = 'Zoom<br>Out';
zoomIn.innerHTML = '<h2>Zoom<br>In</h2>'; zoomIn.innerHTML = 'Zoom<br>In';
zoomAll.innerHTML = '<h2>Zoom to Fit</h2>'; zoomAll.innerHTML = 'Zoom to Fit';
currentScale.innerHTML = `Scale: ${this.sim.getScaleDisplay()}`;
this.sim.onZoom(() => {
currentScale.innerHTML = `Scale: ${this.sim.getScaleDisplay()}`;
});
zoomOut.addEventListener('click', () => { zoomOut.addEventListener('click', () => {
// Aim at center of view // Aim at center of view

View File

@ -1,12 +1,13 @@
import { import {
TOOLBAR_CLASSNAME, TOOLBAR_CLASSNAME,
} from './config.js'; } from './config.js';
import {Header} from './tool/header.js';
export class Toolbar { export class Toolbar {
sim = undefined; sim = undefined;
tools = []; tools = [];
constructor(sim) { constructor(sim, title) {
this.sim = sim; this.sim = sim;
// Create ourselves a div, as child of sim's div // Create ourselves a div, as child of sim's div
@ -14,6 +15,10 @@ export class Toolbar {
this.div = div; this.div = div;
this.sim.div.appendChild(div); this.sim.div.appendChild(div);
div.classList.add(TOOLBAR_CLASSNAME); div.classList.add(TOOLBAR_CLASSNAME);
// Create a collapse/expand tool
const header = new Header(this, title);
this.addTool(header);
} }
topRight() { topRight() {