Feature: Select; Object Tool
This commit is contained in:
parent
eac659822b
commit
4eec03dd1f
@ -53,3 +53,4 @@ TODO
|
|||||||
- [ ] Feature: Lossy Rescaling To Widen Zoom (Handling overflow/underflow)
|
- [ ] Feature: Lossy Rescaling To Widen Zoom (Handling overflow/underflow)
|
||||||
- [ ] Enhancement: Track farthest reaches, min/max in each dimension (x, y)
|
- [ ] Enhancement: Track farthest reaches, min/max in each dimension (x, y)
|
||||||
- [x] Task: Verify stationary pointer leads to zero pointer velocity
|
- [x] Task: Verify stationary pointer leads to zero pointer velocity
|
||||||
|
- [ ] Fix: Unpause panning when initiated while sim is paused
|
||||||
|
|||||||
@ -8,6 +8,11 @@ export const ACCELERATION_VECTOR_COLOR = 'rgba(0, 128, 0, 0.8)'; // optionally s
|
|||||||
export const ACCELERATION_VECTOR_WIDTH = 1.5;
|
export const ACCELERATION_VECTOR_WIDTH = 1.5;
|
||||||
export const ACCELERATION_VECTOR_ARROWHEAD = true;
|
export const ACCELERATION_VECTOR_ARROWHEAD = true;
|
||||||
|
|
||||||
|
// SELECTION
|
||||||
|
export const SELECTED_GROUP_COLOR = 'rgba(0, 255, 0, 0.50)';
|
||||||
|
export const SELECTED_SINGLE_COLOR = 'rgba(0, 255, 0, 1.0)';
|
||||||
|
export const SELECTED_MARGIN = 5;
|
||||||
|
|
||||||
// PATH TRACES
|
// PATH TRACES
|
||||||
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;
|
||||||
|
|||||||
18
display.js
18
display.js
@ -4,22 +4,20 @@ import {
|
|||||||
} from './config.js';
|
} from './config.js';
|
||||||
|
|
||||||
export class Display {
|
export class Display {
|
||||||
sim = undefined;
|
|
||||||
scalePower = 0;
|
scalePower = 0;
|
||||||
viewOrigin = {x: 0, y: 0};
|
viewOrigin = {x: 0, y: 0};
|
||||||
|
|
||||||
constructor(sim) {
|
constructor(sim) {
|
||||||
this.sim = sim;
|
|
||||||
// Create canvas that fills the window
|
|
||||||
// If the window resizes, also resize the canvas
|
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
this.canvas = canvas;
|
this.canvas = canvas;
|
||||||
this.sim.div.appendChild(canvas);
|
|
||||||
canvas.style.position = 'absolute';
|
if (sim) {
|
||||||
canvas.style.top = 0;
|
// Create canvas that fills the window
|
||||||
canvas.style.left = 0;
|
// If the window resizes, also resize the canvas
|
||||||
this.fullscreen();
|
sim.div.appendChild(canvas);
|
||||||
window.addEventListener('resize', () => this.fullscreen());
|
this.fullscreen();
|
||||||
|
window.addEventListener('resize', () => this.fullscreen());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
frame() {
|
frame() {
|
||||||
|
|||||||
249
object.js
249
object.js
@ -9,11 +9,14 @@ import {
|
|||||||
PATH_TRACES_DASHED_OPACITY,
|
PATH_TRACES_DASHED_OPACITY,
|
||||||
PATH_TRACES_OPACITY,
|
PATH_TRACES_OPACITY,
|
||||||
PATH_TRACES_WIDTH,
|
PATH_TRACES_WIDTH,
|
||||||
|
SELECTED_GROUP_COLOR,
|
||||||
|
SELECTED_MARGIN,
|
||||||
|
SELECTED_SINGLE_COLOR,
|
||||||
VELOCITY_VECTOR_ARROWHEAD,
|
VELOCITY_VECTOR_ARROWHEAD,
|
||||||
VELOCITY_VECTOR_COLOR,
|
VELOCITY_VECTOR_COLOR,
|
||||||
VELOCITY_VECTOR_WIDTH,
|
VELOCITY_VECTOR_WIDTH,
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
import {add, components, direction, div, magnitude} from './vector.js';
|
import {add, components, direction, div, magnitude, zero} from './vector.js';
|
||||||
|
|
||||||
export class MassObject {
|
export class MassObject {
|
||||||
sim = undefined;
|
sim = undefined;
|
||||||
@ -21,8 +24,8 @@ export class MassObject {
|
|||||||
mass = 0;
|
mass = 0;
|
||||||
density = 1;
|
density = 1;
|
||||||
position = {x: undefined, y: undefined};
|
position = {x: undefined, y: undefined};
|
||||||
velocity = {x: 0, y: 0};
|
velocity = zero;
|
||||||
acceleration = {x: 0, y: 0};
|
acceleration = zero;
|
||||||
color = {r: undefined, g: undefined, b: undefined};
|
color = {r: undefined, g: undefined, b: undefined};
|
||||||
created = undefined;
|
created = undefined;
|
||||||
forces = []; // [{x, y}]
|
forces = []; // [{x, y}]
|
||||||
@ -58,46 +61,21 @@ export class MassObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAcceleration() {
|
getAcceleration() {
|
||||||
let ax = 0;
|
const netForce = this.forces.reduce((t, f) => add(t, f), zero);
|
||||||
let ay = 0;
|
return div(netForce, this.mass);
|
||||||
for (let {x, y} of this.forces) {
|
|
||||||
ax += x;
|
|
||||||
ay += y;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
x: ax / this.mass,
|
|
||||||
y: ay / this.mass,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
drawObject(sim) {
|
drawPath(display) {
|
||||||
const {
|
const {ctx} = display;
|
||||||
color: {r, g, b},
|
const {color: {r, g, b}} = this;
|
||||||
position: {x, y},
|
|
||||||
velocity: {x: vx, y: vy},
|
|
||||||
acceleration,
|
|
||||||
radius,
|
|
||||||
} = this;
|
|
||||||
|
|
||||||
const {
|
|
||||||
display: {
|
|
||||||
ctx,
|
|
||||||
height: H,
|
|
||||||
width: W,
|
|
||||||
viewOrigin: {x: ox, y: oy},
|
|
||||||
}
|
|
||||||
} = sim;
|
|
||||||
|
|
||||||
const cx = ox + W / 2;
|
|
||||||
const cy = oy + H / 2;
|
|
||||||
|
|
||||||
// Draw path traces
|
// Draw path traces
|
||||||
if (sim.getOption('display.traces') && this.history?.length) {
|
if (this.sim.getOption('display.traces') && this.history?.length) {
|
||||||
const dashedTraces = sim.getOption('display.dashedTraces');
|
const dashedTraces = this.sim.getOption('display.dashedTraces');
|
||||||
const opacity = dashedTraces ? PATH_TRACES_DASHED_OPACITY : PATH_TRACES_OPACITY;
|
const opacity = dashedTraces ? PATH_TRACES_DASHED_OPACITY : PATH_TRACES_OPACITY;
|
||||||
ctx.strokeStyle = PATH_TRACES_COLOR === 'object color' ?
|
ctx.strokeStyle = PATH_TRACES_COLOR === 'object color' ?
|
||||||
`rgba(${r}, ${g}, ${b}, ${opacity})` : PATH_TRACES_COLOR;
|
`rgba(${r}, ${g}, ${b}, ${opacity})` : PATH_TRACES_COLOR;
|
||||||
ctx.lineWidth = PATH_TRACES_WIDTH / this.sim.display.scale;
|
ctx.lineWidth = PATH_TRACES_WIDTH / display.scale;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
let dash = false;
|
let dash = false;
|
||||||
for (let i = 0; i < this.history.length; i++) {
|
for (let i = 0; i < this.history.length; i++) {
|
||||||
@ -116,6 +94,114 @@ export class MassObject {
|
|||||||
}
|
}
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawArrows(display) {
|
||||||
|
const {
|
||||||
|
color: {r, g, b},
|
||||||
|
position: {x, y},
|
||||||
|
velocity: {x: vx, y: vy},
|
||||||
|
acceleration,
|
||||||
|
radius,
|
||||||
|
} = this;
|
||||||
|
|
||||||
|
const {
|
||||||
|
height: H,
|
||||||
|
width: W,
|
||||||
|
viewOrigin: {x: ox, y: oy},
|
||||||
|
} = display;
|
||||||
|
|
||||||
|
const cx = ox + W / 2;
|
||||||
|
const cy = oy + H / 2;
|
||||||
|
|
||||||
|
if (!this.alive) return;
|
||||||
|
|
||||||
|
// If the object is outside the display area, skip drawing arrows
|
||||||
|
if (Math.abs(x - cx) - radius >= W / 2 ||
|
||||||
|
Math.abs(y - cy) - radius >= H / 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw arrow for the velocity
|
||||||
|
if (this.sim.getOption('display.velocity')) {
|
||||||
|
const vecScale = this.sim.getOption('display.velocityScale');
|
||||||
|
const selected = this.sim.system.getSelectedOrCreating();
|
||||||
|
const isSelected = selected?.id === this.id;
|
||||||
|
let velocity = {x: vx, y: vy};
|
||||||
|
if (isSelected) {
|
||||||
|
// If this object is being dragged by the user,
|
||||||
|
// show the pointer velocity instead of object velocity
|
||||||
|
let pointerV = this.sim.pointer.latestVelocity;
|
||||||
|
if (this.sim.getOption('compensate.timeScale')) {
|
||||||
|
pointerV = div(pointerV, this.sim.timeScale);
|
||||||
|
}
|
||||||
|
velocity = add(velocity, pointerV);
|
||||||
|
}
|
||||||
|
const speed = magnitude(velocity);
|
||||||
|
const arrowDirection = direction(velocity);
|
||||||
|
// Prevent negative numbers by adding 1
|
||||||
|
// Make arrow lengths appear consistent regardless of display scale
|
||||||
|
let arrowLength = Math.log10(speed + 1) * vecScale;
|
||||||
|
if (!this.sim.getOption('display.zoomVectors')) {
|
||||||
|
arrowLength /= display.scale;
|
||||||
|
}
|
||||||
|
// const offset = mult({x: Math.cos(
|
||||||
|
const offset = components(radius, arrowDirection)
|
||||||
|
const start = add({x, y}, offset);
|
||||||
|
const end = add(start, components(arrowLength, arrowDirection));
|
||||||
|
const style = VELOCITY_VECTOR_COLOR === 'object color' ?
|
||||||
|
`rgb(${r}, ${g}, ${b})` : VELOCITY_VECTOR_COLOR;
|
||||||
|
display.drawArrow(start.x, start.y, end.x, end.y, {
|
||||||
|
style,
|
||||||
|
width: VELOCITY_VECTOR_WIDTH,
|
||||||
|
arrowhead: VELOCITY_VECTOR_ARROWHEAD,
|
||||||
|
fill: false,
|
||||||
|
ifShort: 'head'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw arrow for acceleration
|
||||||
|
if (this.sim.getOption('display.acceleration')) {
|
||||||
|
const vecScale = this.sim.getOption('display.accelerationScale');
|
||||||
|
const accel = magnitude(acceleration);
|
||||||
|
const arrowDirection = direction(acceleration);
|
||||||
|
// Prevent negative numbers by adding 1
|
||||||
|
// Make arrow lengths appear consistent regardless of display scale
|
||||||
|
let arrowLength = Math.log10(accel + 1) * vecScale;
|
||||||
|
if (!this.sim.getOption('display.zoomVectors')) {
|
||||||
|
arrowLength /= this.sim.display.scale;
|
||||||
|
}
|
||||||
|
const offset = components(radius, arrowDirection)
|
||||||
|
const start = add({x, y}, offset);
|
||||||
|
const end = add(start, components(arrowLength, arrowDirection));
|
||||||
|
const style = ACCELERATION_VECTOR_COLOR === 'object color' ?
|
||||||
|
`rgb(${r}, ${g}, ${b})` : ACCELERATION_VECTOR_COLOR;
|
||||||
|
display.drawArrow(start.x, start.y, end.x, end.y, {
|
||||||
|
style,
|
||||||
|
width: ACCELERATION_VECTOR_WIDTH,
|
||||||
|
arrowhead: ACCELERATION_VECTOR_ARROWHEAD,
|
||||||
|
fill: false,
|
||||||
|
ifShort: 'tail'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawObject(display) {
|
||||||
|
const {
|
||||||
|
color: {r, g, b},
|
||||||
|
position: {x, y},
|
||||||
|
radius,
|
||||||
|
} = this;
|
||||||
|
|
||||||
|
const {
|
||||||
|
ctx,
|
||||||
|
height: H,
|
||||||
|
width: W,
|
||||||
|
viewOrigin: {x: ox, y: oy},
|
||||||
|
} = display;
|
||||||
|
|
||||||
|
const cx = ox + W / 2;
|
||||||
|
const cy = oy + H / 2;
|
||||||
|
|
||||||
if (!this.alive) return;
|
if (!this.alive) return;
|
||||||
|
|
||||||
@ -153,11 +239,11 @@ export class MassObject {
|
|||||||
|
|
||||||
const arrowDirection = Math.atan2(py - cy, px - cx);
|
const arrowDirection = Math.atan2(py - cy, px - cx);
|
||||||
// Length of arrow based on distance (logarithmic scale)
|
// Length of arrow based on distance (logarithmic scale)
|
||||||
const distance = Math.sqrt((x - px) ** 2, (y - py) ** 2);
|
const d = Math.sqrt((x - px) ** 2, (y - py) ** 2);
|
||||||
const arrowLength = Math.log(distance + 1) * OFFSCREEN_OBJECT_LINE_SCALE / this.sim.display.scale;
|
const arrowLength = Math.log(d + 1) * OFFSCREEN_OBJECT_LINE_SCALE / display.scale;
|
||||||
const startAx = px - arrowLength * Math.cos(arrowDirection);
|
const startAx = px - arrowLength * Math.cos(arrowDirection);
|
||||||
const startAy = py - arrowLength * Math.sin(arrowDirection);
|
const startAy = py - arrowLength * Math.sin(arrowDirection);
|
||||||
sim.display.drawArrow(startAx, startAy, px, py, {
|
display.drawArrow(startAx, startAy, px, py, {
|
||||||
style: `rgb(${r}, ${g}, ${b})`,
|
style: `rgb(${r}, ${g}, ${b})`,
|
||||||
width: OFFSCREEN_OBJECT_LINE_WIDTH,
|
width: OFFSCREEN_OBJECT_LINE_WIDTH,
|
||||||
arrowheadLength: OFFSCREEN_OBJECT_ARROWHEAD_LENGTH,
|
arrowheadLength: OFFSCREEN_OBJECT_ARROWHEAD_LENGTH,
|
||||||
@ -173,73 +259,26 @@ export class MassObject {
|
|||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(x, y, radius, 0, 2 * Math.PI);
|
ctx.arc(x, y, radius, 0, 2 * Math.PI);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
// Draw arrow for the velocity
|
|
||||||
if (sim.getOption('display.velocity')) {
|
|
||||||
const vecScale = this.sim.getOption('display.velocityScale');
|
|
||||||
const selected = this.sim.system.getSelectedOrCreating();
|
|
||||||
const isSelected = selected?.id === this.id;
|
|
||||||
let velocity = {x: vx, y: vy};
|
|
||||||
if (isSelected) {
|
|
||||||
// If this object is being dragged by the user,
|
|
||||||
// show the pointer velocity instead of object velocity
|
|
||||||
let pointerV = this.sim.pointer.latestVelocity;
|
|
||||||
if (this.sim.getOption('compensate.timeScale')) {
|
|
||||||
pointerV = div(pointerV, this.sim.timeScale);
|
|
||||||
}
|
|
||||||
velocity = add(velocity, pointerV);
|
|
||||||
}
|
|
||||||
const speed = magnitude(velocity);
|
|
||||||
const arrowDirection = direction(velocity);
|
|
||||||
// Prevent negative numbers by adding 1
|
|
||||||
// TODO: Make logarithmic vector length scale optional
|
|
||||||
// Make arrow lengths appear consistent regardless of display scale
|
|
||||||
let arrowLength = Math.log10(speed + 1) * vecScale;
|
|
||||||
if (!this.sim.getOption('display.zoomVectors')) {
|
|
||||||
arrowLength /= this.sim.display.scale;
|
|
||||||
}
|
|
||||||
// const offset = mult({x: Math.cos(
|
|
||||||
const offset = components(radius, arrowDirection)
|
|
||||||
const start = add({x, y}, offset);
|
|
||||||
const end = add(start, components(arrowLength, arrowDirection));
|
|
||||||
const style = VELOCITY_VECTOR_COLOR === 'object color' ?
|
|
||||||
`rgb(${r}, ${g}, ${b})` : VELOCITY_VECTOR_COLOR;
|
|
||||||
sim.display.drawArrow(start.x, start.y, end.x, end.y, {
|
|
||||||
style,
|
|
||||||
width: VELOCITY_VECTOR_WIDTH,
|
|
||||||
arrowhead: VELOCITY_VECTOR_ARROWHEAD,
|
|
||||||
fill: false,
|
|
||||||
ifShort: 'head'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw arrow for acceleration
|
|
||||||
if (sim.getOption('display.acceleration')) {
|
|
||||||
const vecScale = this.sim.getOption('display.accelerationScale');
|
|
||||||
const accel = magnitude(acceleration);
|
|
||||||
const arrowDirection = direction(acceleration);
|
|
||||||
// Prevent negative numbers by adding 1
|
|
||||||
// TODO: Make logarithmic vector length scale optional
|
|
||||||
// Make arrow lengths appear consistent regardless of display scale
|
|
||||||
let arrowLength = Math.log10(accel + 1) * vecScale;
|
|
||||||
if (!this.sim.getOption('display.zoomVectors')) {
|
|
||||||
arrowLength /= this.sim.display.scale;
|
|
||||||
}
|
|
||||||
const offset = components(radius, arrowDirection)
|
|
||||||
const start = add({x, y}, offset);
|
|
||||||
const end = add(start, components(arrowLength, arrowDirection));
|
|
||||||
const style = ACCELERATION_VECTOR_COLOR === 'object color' ?
|
|
||||||
`rgb(${r}, ${g}, ${b})` : ACCELERATION_VECTOR_COLOR;
|
|
||||||
sim.display.drawArrow(start.x, start.y, end.x, end.y, {
|
|
||||||
style,
|
|
||||||
width: ACCELERATION_VECTOR_WIDTH,
|
|
||||||
arrowhead: ACCELERATION_VECTOR_ARROWHEAD,
|
|
||||||
fill: false,
|
|
||||||
ifShort: 'tail'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drawSelection(display) {
|
||||||
|
const {
|
||||||
|
position: {x, y},
|
||||||
|
radius,
|
||||||
|
} = this;
|
||||||
|
const {ctx} = display;
|
||||||
|
|
||||||
|
if (!this.alive) return;
|
||||||
|
|
||||||
|
// If object is currently selected, draw a circle around it
|
||||||
|
const isInSelectedGroup = this.sim.select.selectedGroup?.some(({id}) => id === this.id);
|
||||||
|
const isSelectedSingle = this.sim.select.selectedSingle?.id === this.id;
|
||||||
|
if (isSelectedSingle || isInSelectedGroup) {
|
||||||
|
ctx.strokeStyle = isSelectedSingle ? SELECTED_SINGLE_COLOR : SELECTED_GROUP_COLOR;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, radius + SELECTED_MARGIN / display.scale, 0, 2 * Math.PI);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
45
select.js
45
select.js
@ -1,33 +1,74 @@
|
|||||||
|
import {copy} from './vector.js';
|
||||||
|
|
||||||
export class Select {
|
export class Select {
|
||||||
sim = undefined;
|
sim = undefined;
|
||||||
box = {
|
box = {
|
||||||
start: undefined,
|
start: undefined,
|
||||||
end: undefined,
|
end: undefined,
|
||||||
};
|
};
|
||||||
|
selectedSingle = undefined;
|
||||||
|
selectedGroup = [];
|
||||||
|
|
||||||
constructor(sim) {
|
constructor(sim) {
|
||||||
this.sim = sim;
|
this.sim = sim;
|
||||||
|
|
||||||
|
// TODO: Move this to a new Keyboard class singleton
|
||||||
|
window.addEventListener('keydown', (e) => {
|
||||||
|
switch (e.key) {
|
||||||
|
case 'Tab': {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!this.selectedGroup.length) return;
|
||||||
|
const currentIdx = this.selectedGroup.indexOf(this.selectedSingle);
|
||||||
|
const newIdx = (currentIdx + 1) % this.selectedGroup.length;
|
||||||
|
this.selectedSingle = this.selectedGroup[newIdx];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePointerDown({x: clientX, y: clientY}) {
|
handlePointerDown({x: clientX, y: clientY}) {
|
||||||
this.box.start = this.sim.screenToSim(clientX, clientY);
|
this.box.start = this.sim.screenToSim(clientX, clientY);
|
||||||
|
this.box.end = this.box.start;
|
||||||
|
this.getSelectedObjects();
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePointerMove({x: clientX, y: clientY}) {
|
handlePointerMove({x: clientX, y: clientY}) {
|
||||||
|
if (!this.box.start) return;
|
||||||
this.box.end = this.sim.screenToSim(clientX, clientY);
|
this.box.end = this.sim.screenToSim(clientX, clientY);
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePointerUp() {
|
handlePointerUp() {
|
||||||
console.log('selection box', {...this.box});
|
if (!this.box.start) return;
|
||||||
|
const start = copy(this.box.start);
|
||||||
|
const end = copy(this.box.end);
|
||||||
|
this.box.start = {
|
||||||
|
x: Math.min(start.x, end.x),
|
||||||
|
y: Math.min(start.y, end.y),
|
||||||
|
};
|
||||||
|
this.box.end = {
|
||||||
|
x: Math.max(start.x, end.x),
|
||||||
|
y: Math.max(start.y, end.y),
|
||||||
|
};
|
||||||
|
this.getSelectedObjects();
|
||||||
this.box = {
|
this.box = {
|
||||||
start: undefined,
|
start: undefined,
|
||||||
end: undefined,
|
end: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSelectedObjects() {
|
||||||
|
const {start, end} = this.box;
|
||||||
|
if (!start) return;
|
||||||
|
this.selectedGroup = this.sim.system.filter(({position: {x, y}}) => {
|
||||||
|
return x >= start.x && x <= end.x && y >= start.y && y <= end.y;
|
||||||
|
});
|
||||||
|
// For now, first object in group is selected single
|
||||||
|
this.selectedSingle = this.selectedGroup[0] ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
frame() {
|
frame() {
|
||||||
if (!this.box.start) return;
|
if (!this.box.start) return;
|
||||||
|
|
||||||
this.sim.display.drawBox(this.box)
|
this.sim.display.drawBox(this.box)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
65
sim-tools.js
65
sim-tools.js
@ -1,4 +1,6 @@
|
|||||||
import {ModeSwitch} from './tool/modes.js';
|
import {ModeSwitch} from './tool/modes.js';
|
||||||
|
import {ObjectTool} from './tool/object.js';
|
||||||
|
import {ObjectsTool} from './tool/objects.js';
|
||||||
import {OptionsTool} from './tool/options.js';
|
import {OptionsTool} from './tool/options.js';
|
||||||
import {PlayPause} from './tool/play-pause.js';
|
import {PlayPause} from './tool/play-pause.js';
|
||||||
import {UtilityTool} from './tool/utility.js';
|
import {UtilityTool} from './tool/utility.js';
|
||||||
@ -7,43 +9,40 @@ import {ToolbarGroup} from './toolbar-group.js';
|
|||||||
import {Toolbar} from './toolbar.js';
|
import {Toolbar} from './toolbar.js';
|
||||||
|
|
||||||
export function initializeTools(sim) {
|
export function initializeTools(sim) {
|
||||||
sim.toolbars = {
|
|
||||||
tools: new Toolbar(sim, 'Tools')
|
|
||||||
.addTool(new Zoom())
|
|
||||||
.addTool(new PlayPause()),
|
|
||||||
modes: new Toolbar(sim, 'Modes')
|
|
||||||
.addTool(new ModeSwitch()),
|
|
||||||
utils: new Toolbar(sim, 'Utils', {expanded: false})
|
|
||||||
.addTool(new UtilityTool()),
|
|
||||||
options: new Toolbar(sim, 'Options')
|
|
||||||
.addTool(new OptionsTool([
|
|
||||||
'pauseDuring',
|
|
||||||
'display',
|
|
||||||
'compensate',
|
|
||||||
])),
|
|
||||||
params: new Toolbar(sim, 'Params')
|
|
||||||
.addTool(new OptionsTool([
|
|
||||||
'param'
|
|
||||||
])),
|
|
||||||
debug: new Toolbar(sim, 'Debug', {expanded: false})
|
|
||||||
.addTool(new OptionsTool([
|
|
||||||
'debug'
|
|
||||||
])),
|
|
||||||
};
|
|
||||||
const {tools, modes, options, params, debug, utils} = sim.toolbars;
|
|
||||||
sim.toolbarGroups = {
|
sim.toolbarGroups = {
|
||||||
left: new ToolbarGroup(sim)
|
left: new ToolbarGroup(sim)
|
||||||
.addToolbar(tools)
|
.addToolbar(new Toolbar(sim, 'Tools')
|
||||||
.addToolbar(modes)
|
.addTool(new Zoom())
|
||||||
.addToolbar(utils),
|
.addTool(new PlayPause())
|
||||||
|
)
|
||||||
|
.addToolbar(new Toolbar(sim, 'Modes')
|
||||||
|
.addTool(new ModeSwitch())
|
||||||
|
)
|
||||||
|
.addToolbar(new Toolbar(sim, 'Utils', {expanded: false})
|
||||||
|
.addTool(new UtilityTool())
|
||||||
|
)
|
||||||
|
.addToolbar(new Toolbar(sim, 'Objects')
|
||||||
|
.addTool(new ObjectsTool()),
|
||||||
|
),
|
||||||
right: new ToolbarGroup(sim).topRight()
|
right: new ToolbarGroup(sim).topRight()
|
||||||
.addToolbar(options)
|
.addToolbar(new Toolbar(sim, 'Options')
|
||||||
.addToolbar(params)
|
.addTool(new OptionsTool(['pauseDuring', 'display', 'compensate']))
|
||||||
.addToolbar(debug),
|
)
|
||||||
|
.addToolbar(new Toolbar(sim, 'Params')
|
||||||
|
.addTool(new OptionsTool([
|
||||||
|
'param'
|
||||||
|
])),
|
||||||
|
)
|
||||||
|
.addToolbar(new Toolbar(sim, 'Debug', {expanded: false})
|
||||||
|
.addTool(new OptionsTool(['debug'])),
|
||||||
|
)
|
||||||
|
.addToolbar(new Toolbar(sim, 'Object')
|
||||||
|
.addTool(new ObjectTool()),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const id in sim.toolbars) {
|
for (const groupName in sim.toolbarGroups) {
|
||||||
const toolbar = sim.toolbars[id];
|
const group = sim.toolbarGroups[groupName];
|
||||||
toolbar.applyExpanded();
|
group.applyExpanded();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
style.css
13
style.css
@ -27,6 +27,12 @@ div[id=simulator] {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div[id=simulator] > canvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* normal toolbar group */
|
/* normal toolbar group */
|
||||||
div.lhg-toolbar-group div.lhg-tool {
|
div.lhg-toolbar-group div.lhg-tool {
|
||||||
width: 12em;
|
width: 12em;
|
||||||
@ -138,10 +144,3 @@ div.lhg-option-group {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
div.lhg-option-group > * {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|||||||
18
system.js
18
system.js
@ -317,7 +317,11 @@ export class System {
|
|||||||
}
|
}
|
||||||
|
|
||||||
drawObjects() {
|
drawObjects() {
|
||||||
this.forEachObject(obj => obj.drawObject(this.sim), {alive: null});
|
// Draw all paths, all objects, and then all arrows
|
||||||
|
this.forEachObject(obj => obj.drawPath(this.sim.display), {alive: null});
|
||||||
|
this.forEachObject(obj => obj.drawObject(this.sim.display), {alive: true});
|
||||||
|
this.forEachObject(obj => obj.drawSelection(this.sim.display), {alive: true});
|
||||||
|
this.forEachObject(obj => obj.drawArrows(this.sim.display), {alive: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
// cb: (acc, obj, idx) => {}
|
// cb: (acc, obj, idx) => {}
|
||||||
@ -332,6 +336,18 @@ export class System {
|
|||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cb: (obj, idx) => boolean
|
||||||
|
filter(cb, opts) {
|
||||||
|
let objects = [];
|
||||||
|
this.forEachObject((obj, idx) => {
|
||||||
|
const ret = cb(obj, idx);
|
||||||
|
if (ret) {
|
||||||
|
objects.push(obj);
|
||||||
|
}
|
||||||
|
}, opts);
|
||||||
|
return objects;
|
||||||
|
}
|
||||||
|
|
||||||
computeForces() {
|
computeForces() {
|
||||||
const gravity = this.sim.getOption('param.gravity');
|
const gravity = this.sim.getOption('param.gravity');
|
||||||
if (this.objects.length < 2) return;
|
if (this.objects.length < 2) return;
|
||||||
|
|||||||
49
tool/object.js
Normal file
49
tool/object.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import {Display} from '../display.js';
|
||||||
|
import {Tool} from '../tool.js';
|
||||||
|
import {div, sub} from '../vector.js';
|
||||||
|
|
||||||
|
export class ObjectTool extends Tool {
|
||||||
|
selected = undefined;
|
||||||
|
display = undefined;
|
||||||
|
textEl = undefined;
|
||||||
|
|
||||||
|
setContainer(container) {
|
||||||
|
super.setContainer(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.display = new Display();
|
||||||
|
this.display.canvas.width = 150;
|
||||||
|
this.display.canvas.height = 150;
|
||||||
|
|
||||||
|
this.textEl = document.createElement('div');
|
||||||
|
|
||||||
|
this.div.appendChild(this.textEl);
|
||||||
|
this.div.appendChild(this.display.canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
frame() {
|
||||||
|
this.display.frame();
|
||||||
|
const obj = this.sim.select.selectedSingle;
|
||||||
|
this.textEl.innerHTML = `obj id: ${obj?.id}`;
|
||||||
|
if (!obj) return;
|
||||||
|
|
||||||
|
// Show object details
|
||||||
|
// Distance from center of screen
|
||||||
|
// Render the object
|
||||||
|
|
||||||
|
// Set up this.display.viewOrigin
|
||||||
|
|
||||||
|
const {radius, position} = obj;
|
||||||
|
const widthRatio = Math.abs(2 * radius) / this.display.canvas.width;
|
||||||
|
const heightRatio = Math.abs(2 * radius) / this.display.canvas.height;
|
||||||
|
const ratio = Math.max(widthRatio, heightRatio) * 2;
|
||||||
|
this.display.scalePower = Math.log2(1 / ratio);
|
||||||
|
this.display.viewOrigin = sub(position,
|
||||||
|
div({x: this.display.width, y: this.display.height}, 2));
|
||||||
|
obj.drawObject(this.display);
|
||||||
|
obj.drawArrows(this.display);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
tool/objects.js
Normal file
11
tool/objects.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import {Tool} from '../tool.js';
|
||||||
|
|
||||||
|
export class ObjectsTool extends Tool {
|
||||||
|
setContainer(container) {
|
||||||
|
super.setContainer(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -24,8 +24,8 @@ export class Zoom extends Tool {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(container) {
|
constructor() {
|
||||||
super(container);
|
super();
|
||||||
|
|
||||||
const currentScale = document.createElement('button')
|
const currentScale = document.createElement('button')
|
||||||
const zoomOut = document.createElement('button');
|
const zoomOut = document.createElement('button');
|
||||||
|
|||||||
@ -28,8 +28,14 @@ export class ToolbarGroup {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyExpanded() {
|
||||||
|
for (const toolbar of this.toolbars) {
|
||||||
|
toolbar.applyExpanded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
frame() {
|
frame() {
|
||||||
for (let toolbar of this.toolbars) {
|
for (const toolbar of this.toolbars) {
|
||||||
toolbar.frame();
|
toolbar.frame();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user