Feature: Objects Tool
This commit is contained in:
parent
71854d2a95
commit
f05d1ed399
15
Readme.md
15
Readme.md
@ -16,13 +16,13 @@ TODO
|
||||
----
|
||||
|
||||
- [x] Selection Box
|
||||
- [ ] Feature: Object List
|
||||
- [ ] Feature: Object Detail
|
||||
- [ ] Feature: Zoom to Object
|
||||
- [x] Feature: Object List
|
||||
- [x] Feature: Object Detail
|
||||
- [x] Feature: Zoom to Object
|
||||
- [ ] Feature: Teleport Object
|
||||
- [ ] Enhancement: Create Time class and refactor to use
|
||||
- [ ] Enhancement: Create Vector class and refactor to use
|
||||
- [ ] Enhancement: Create Panning class and refactor to use
|
||||
- [x] Enhancement: Create Vector class and refactor to use
|
||||
- [x] Enhancement: Create Panning class and refactor to use
|
||||
- [ ] Enhancement: Handle pointerleave or other mechanism when window loses focus
|
||||
- [ ] Enhancement: Calculate Work as FxD as measure of energy flux
|
||||
- [ ] Feature: Automatically slow time when energy flux is greater
|
||||
@ -32,7 +32,7 @@ TODO
|
||||
- [ ] Feature: Polar Coordinates
|
||||
- [ ] Feature: Cylindrical Coordinates
|
||||
- [ ] Feature: Spherical Coordinates
|
||||
- [ ] Feature: Camera Velocity Display
|
||||
- [x] Feature: Camera Velocity Display
|
||||
- [ ] Enhancement: World State Snapshots
|
||||
- [ ] Feature: List / Save / Load World States
|
||||
- [ ] Feature: Left Button Panning
|
||||
@ -52,5 +52,4 @@ TODO
|
||||
- [ ] Feature: Time Control: Reverse Time
|
||||
- [ ] Feature: Lossy Rescaling To Widen Zoom (Handling overflow/underflow)
|
||||
- [ ] Enhancement: Track farthest reaches, min/max in each dimension (x, y)
|
||||
- [x] Task: Verify stationary pointer leads to zero pointer velocity
|
||||
- [ ] Fix: Unpause panning when initiated while sim is paused
|
||||
- [x] Fix: Unpause panning when initiated while sim is paused
|
||||
|
||||
@ -54,6 +54,9 @@ export const EVENT_MODE_ENTER = 'lhg-mode-enter';
|
||||
export const EVENT_ZOOM = 'lhg-zoom-event';
|
||||
export const EVENT_OPTION_SET = 'lhg-option-set';
|
||||
export const EVENT_PLAY_PAUSE = 'lhg-play-pause';
|
||||
export const EVENT_SELECT = 'lhg-select';
|
||||
export const EVENT_OBJECT_CREATE = 'lhg-object-create';
|
||||
export const EVENT_OBJECT_MERGE = 'lhg-object-merge';
|
||||
|
||||
// MODES
|
||||
export const MODE_MASS_GENERATION = 'mass-gen';
|
||||
|
||||
@ -36,7 +36,8 @@ export function show({items, item, parentEl, itemEl}) {
|
||||
item.hidden = false;
|
||||
}
|
||||
|
||||
export function hide({item, parentEl, itemEl}) {
|
||||
export function hide({items, item, parentEl, itemEl}) {
|
||||
if (items.indexOf(item) < 0) return;
|
||||
parentEl.removeChild(itemEl);
|
||||
item.hidden = true;
|
||||
}
|
||||
|
||||
20
pointer.js
20
pointer.js
@ -17,38 +17,38 @@ export class Pointer {
|
||||
this.sim = sim;
|
||||
|
||||
// Monitor mouse movements
|
||||
const el = this.sim.display.canvas;
|
||||
const {canvas} = this.sim.display;
|
||||
|
||||
el.addEventListener('pointermove', e => {
|
||||
window.addEventListener('pointermove', e => {
|
||||
if (this.sim.getOption('debug.cursorInfo')) {
|
||||
this.sim.info['pointermove'] = [`${e.clientX.toPrecision(6)}, `, `${e.clientY.toPrecision(6)}`];
|
||||
}
|
||||
this.handlePointerMove({x: e.clientX, y: e.clientY});
|
||||
});
|
||||
|
||||
el.addEventListener('pointerdown', e => {
|
||||
canvas.addEventListener('pointerdown', e => {
|
||||
this.handlePointerDown({x: e.clientX, y: e.clientY});
|
||||
});
|
||||
|
||||
el.addEventListener('pointerup', e => {
|
||||
window.addEventListener('pointerup', e => {
|
||||
this.handlePointerUp({x: e.clientX, y: e.clientY});
|
||||
});
|
||||
|
||||
el.addEventListener('pointerleave', e => {
|
||||
this.handlePointerUp({x: e.clientX, y: e.clientY});
|
||||
});
|
||||
// window.addEventListener('pointerleave', e => {
|
||||
// this.handlePointerUp({x: e.clientX, y: e.clientY});
|
||||
// });
|
||||
|
||||
// Monitor wheel events
|
||||
el.addEventListener('wheel', e => {
|
||||
canvas.addEventListener('wheel', e => {
|
||||
const factor = e.deltaY < 0 ? ZOOM_IN_FACTOR : ZOOM_OUT_FACTOR;
|
||||
const {x, y} = this.sim.screenToSim(e.clientX, e.clientY);
|
||||
this.sim.scheduleZoom({x, y}, factor);
|
||||
});
|
||||
|
||||
el.addEventListener('focus', () => {
|
||||
window.addEventListener('focus', () => {
|
||||
console.log('window focus');
|
||||
});
|
||||
el.addEventListener('blur', () => {
|
||||
window.addEventListener('blur', () => {
|
||||
console.log('window blur');
|
||||
});
|
||||
}
|
||||
|
||||
32
select.js
32
select.js
@ -1,3 +1,4 @@
|
||||
import {EVENT_SELECT} from './config.js';
|
||||
import {add, copy, mult} from './vector.js';
|
||||
|
||||
export class Select {
|
||||
@ -30,7 +31,7 @@ export class Select {
|
||||
handlePointerDown({x: clientX, y: clientY}) {
|
||||
this.box.start = this.sim.screenToSim(clientX, clientY);
|
||||
this.box.end = this.box.start;
|
||||
this.getSelectedObjects();
|
||||
// this.getSelectedObjects();
|
||||
}
|
||||
|
||||
handlePointerMove({x: clientX, y: clientY}) {
|
||||
@ -51,22 +52,13 @@ export class Select {
|
||||
y: Math.max(start.y, end.y),
|
||||
};
|
||||
this.getSelectedObjects();
|
||||
this.sim.div.dispatchEvent(new CustomEvent(EVENT_SELECT));
|
||||
this.box = {
|
||||
start: 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(elapsedTime) {
|
||||
if (!this.box.start) return;
|
||||
// If panning, let's update the position of our box so it doesn't drift away
|
||||
@ -77,4 +69,22 @@ export class Select {
|
||||
// Display the box
|
||||
this.sim.display.drawBox(this.box.start, this.box.end);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// cb: ({selectedGroup, selectedSingle}) => undefined
|
||||
onSelect(cb) {
|
||||
this.sim.div.addEventListener(EVENT_SELECT, () => {
|
||||
const {selectedGroup, selectedSingle} = this;
|
||||
cb({selectedGroup, selectedSingle});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
46
system.js
46
system.js
@ -1,4 +1,4 @@
|
||||
import {OBJECT_HISTORY_SIZE} from './config.js';
|
||||
import {EVENT_OBJECT_CREATE, EVENT_OBJECT_MERGE, OBJECT_HISTORY_SIZE} from './config.js';
|
||||
import {MassObject} from './object.js';
|
||||
import {
|
||||
add, copy, cross, degrees,
|
||||
@ -134,6 +134,8 @@ export class System {
|
||||
};
|
||||
T.alive = false;
|
||||
T.forces = [];
|
||||
const e = new CustomEvent(EVENT_OBJECT_MERGE, {detail: {surviving: S, merged: T}});
|
||||
this.sim.div.dispatchEvent(e);
|
||||
}
|
||||
}, {alive: true, startWith: i + 1});
|
||||
});
|
||||
@ -212,8 +214,9 @@ export class System {
|
||||
if (this.sim.getOption('pauseDuring.creation')) {
|
||||
this.pause();
|
||||
}
|
||||
|
||||
obj.velocity = copy(this.sim.panning.velocity);
|
||||
const e = new CustomEvent(EVENT_OBJECT_CREATE, {detail: {obj}});
|
||||
this.sim.div.dispatchEvent(e);
|
||||
}
|
||||
|
||||
doneCreatingObject() {
|
||||
@ -223,6 +226,20 @@ export class System {
|
||||
}
|
||||
}
|
||||
|
||||
// cb: (obj) => undefined
|
||||
onCreate(cb) {
|
||||
this.sim.div.addEventListener(EVENT_OBJECT_CREATE, ({detail: {obj}}) => {
|
||||
cb(obj);
|
||||
});
|
||||
}
|
||||
|
||||
// cb: ({surviving, merged}) => undefined
|
||||
onMerge(cb) {
|
||||
this.sim.div.addEventListener(EVENT_OBJECT_MERGE, ({detail: {surviving, merged}}) => {
|
||||
cb({surviving, merged});
|
||||
});
|
||||
}
|
||||
|
||||
object(i) {
|
||||
return this.objects[i];
|
||||
}
|
||||
@ -253,21 +270,20 @@ export class System {
|
||||
return this.objects.length;
|
||||
}
|
||||
|
||||
get boundingBox() {
|
||||
getBoundingBox(objects = []) {
|
||||
const box = this.reduce(({start, end}, obj) => {
|
||||
if (objects.length && !objects.includes(obj)) return {start, end};
|
||||
const lx = obj.position.x - obj.radius;
|
||||
const gx = obj.position.x + obj.radius;
|
||||
const ly = obj.position.y - obj.radius;
|
||||
const gy = obj.position.y + obj.radius;
|
||||
let ret;
|
||||
if (start.x === undefined) {
|
||||
ret = {
|
||||
return {
|
||||
start: {x: lx, y: ly},
|
||||
end: {x: gx, y: gy},
|
||||
};
|
||||
return ret;
|
||||
}
|
||||
ret = {
|
||||
return {
|
||||
start: {
|
||||
x: Math.min(start.x, lx),
|
||||
y: Math.min(start.y, ly),
|
||||
@ -277,7 +293,6 @@ export class System {
|
||||
y: Math.max(end.y, gy),
|
||||
}
|
||||
};
|
||||
return ret;
|
||||
}, {
|
||||
start: {x: undefined, y: undefined},
|
||||
end: {x: undefined, y: undefined},
|
||||
@ -373,15 +388,18 @@ export class System {
|
||||
});
|
||||
}
|
||||
|
||||
computeSystemCenter() {
|
||||
computeSystemCenter(objects = []) {
|
||||
// Determine center of mass
|
||||
const {totalMass, count, totalMassLocation} =
|
||||
this.reduce((acc, obj) => ({
|
||||
this.reduce((acc, obj) => {
|
||||
if (objects.length && !objects.includes(obj)) return acc;
|
||||
return {
|
||||
count: acc.count + 1,
|
||||
totalMass: acc.totalMass + obj.mass,
|
||||
totalMassLocation: add(acc.totalMassLocation,
|
||||
mult(obj.position, obj.mass)),
|
||||
}), {
|
||||
};
|
||||
}, {
|
||||
totalMassLocation: {x: 0, y: 0},
|
||||
totalMass: 0,
|
||||
count: 0,
|
||||
@ -390,8 +408,10 @@ export class System {
|
||||
const centerOfMass = count ? div(totalMassLocation, totalMass) : zero;
|
||||
|
||||
// Determine average momentum
|
||||
const netMomentum = this.reduce((acc, obj) =>
|
||||
add(acc, mult(obj.velocity, obj.mass)), zero);
|
||||
const netMomentum = this.reduce((acc, obj) => {
|
||||
if (objects.length && !objects.includes(obj)) return acc;
|
||||
return add(acc, mult(obj.velocity, obj.mass));
|
||||
}, zero);
|
||||
|
||||
return {totalMass, count, totalMassLocation, centerOfMass, netMomentum};
|
||||
}
|
||||
|
||||
@ -1,11 +1,82 @@
|
||||
import {hide, show} from '../helper.js';
|
||||
import {Tool} from '../tool.js';
|
||||
import {add, magnitude, sub} from '../vector.js';
|
||||
|
||||
export class ObjectsTool extends Tool {
|
||||
objects = [];
|
||||
|
||||
setContainer(container) {
|
||||
super.setContainer(container);
|
||||
|
||||
// Display a list of the currently selected objects,
|
||||
// or all objects if none are currently selected.
|
||||
if (this.sim.select.selectedGroup.length) {
|
||||
this.objects = this.sim.select.selectedGroup;
|
||||
} else {
|
||||
this.objects = this.sim.system.filter(obj => obj.alive);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.populate();
|
||||
|
||||
this.sim.select.onSelect(({selectedGroup}) => {
|
||||
this.objects = selectedGroup;
|
||||
this.depopulate();
|
||||
this.populate();
|
||||
});
|
||||
|
||||
this.sim.system.onCreate(obj => {
|
||||
if (!this.sim.select.selectedGroup.length) {
|
||||
this.objects.push(obj);
|
||||
this.populate();
|
||||
}
|
||||
});
|
||||
|
||||
this.sim.system.onMerge(({merged}) => {
|
||||
if (!merged.objectsToolEl) return;
|
||||
hide({
|
||||
items: this.objects,
|
||||
item: merged,
|
||||
parentEl: this.div,
|
||||
itemEl: merged.objectsToolEl,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
frame() {
|
||||
this.populate();
|
||||
}
|
||||
|
||||
depopulate() {
|
||||
while (this.div.firstChild) {
|
||||
this.div.removeChild(this.div.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
populate() {
|
||||
for (const obj of this.objects) {
|
||||
const objectEl = obj.objectsToolEl ?? document.createElement('div');
|
||||
obj.objectsToolEl = objectEl;
|
||||
const {r, g, b} = obj.color;
|
||||
// Distance from center of screen
|
||||
const distance = magnitude(sub(obj.position, add(this.sim.display.viewOrigin, {
|
||||
x: this.sim.display.width / 2,
|
||||
y: this.sim.display.height / 2,
|
||||
})));
|
||||
objectEl.innerHTML = `
|
||||
<span style="background-color: rgb(${r},${g},${b});">` +
|
||||
' </span>' +
|
||||
`${obj.mass.toPrecision(3)} ` +
|
||||
`${distance.toPrecision(3)}`;
|
||||
// `${magnitude(obj.velocity).toExponential(0)} ` +
|
||||
// `${-degrees(direction(obj.velocity)).toFixed(0)}°`;
|
||||
if (!obj.hidden) {
|
||||
show({
|
||||
items: this.objects,
|
||||
item: obj,
|
||||
parentEl: this.div,
|
||||
itemEl: objectEl,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,6 +76,7 @@ export class OptionsTool extends Tool {
|
||||
});
|
||||
} else {
|
||||
hide({
|
||||
items: group.items,
|
||||
item: next,
|
||||
parentEl: groupEl,
|
||||
itemEl: optionEl,
|
||||
|
||||
@ -7,35 +7,6 @@ import {
|
||||
export class UtilityTool extends Tool {
|
||||
currentTimeEl = undefined;
|
||||
|
||||
get timeText() {
|
||||
let time = this.sim.time;
|
||||
// Time in milliseconds
|
||||
const ms = Math.floor(time % 1000);
|
||||
time = (time - ms) / 1000;
|
||||
const s = Math.floor(time % 60);
|
||||
time = (time - s) / 60;
|
||||
const m = Math.floor(time % 60);
|
||||
time = (time - m) / 60;
|
||||
const h = Math.floor(time % 24);
|
||||
time = (time - h) / 24;
|
||||
const d = Math.floor(time);
|
||||
return [
|
||||
d || undefined,
|
||||
h.toString().padStart(2, '0'),
|
||||
m.toString().padStart(2, '0'),
|
||||
[
|
||||
s.toString().padStart(2, '0'),
|
||||
ms.toString().padStart(3, '0'),
|
||||
].join('.')
|
||||
].filter(x => x !== undefined).join(':');
|
||||
}
|
||||
|
||||
frame() {
|
||||
if (this.currentTimeEl) {
|
||||
this.currentTimeEl.innerHTML = this.timeText;
|
||||
}
|
||||
}
|
||||
|
||||
setContainer(container) {
|
||||
super.setContainer(container);
|
||||
this.currentTimeEl.innerHTML = this.timeText;
|
||||
@ -73,4 +44,34 @@ export class UtilityTool extends Tool {
|
||||
this.sim.info = {};
|
||||
});
|
||||
}
|
||||
|
||||
frame() {
|
||||
if (this.currentTimeEl) {
|
||||
this.currentTimeEl.innerHTML = this.timeText;
|
||||
}
|
||||
}
|
||||
|
||||
get timeText() {
|
||||
let time = this.sim.time;
|
||||
// Time in milliseconds
|
||||
const ms = Math.floor(time % 1000);
|
||||
time = (time - ms) / 1000;
|
||||
const s = Math.floor(time % 60);
|
||||
time = (time - s) / 60;
|
||||
const m = Math.floor(time % 60);
|
||||
time = (time - m) / 60;
|
||||
const h = Math.floor(time % 24);
|
||||
time = (time - h) / 24;
|
||||
const d = Math.floor(time);
|
||||
return [
|
||||
d || undefined,
|
||||
h.toString().padStart(2, '0'),
|
||||
m.toString().padStart(2, '0'),
|
||||
[
|
||||
s.toString().padStart(2, '0'),
|
||||
ms.toString().padStart(3, '0'),
|
||||
].join('.')
|
||||
].filter(x => x !== undefined).join(':');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -65,7 +65,8 @@ export class Zoom extends Tool {
|
||||
|
||||
zoomAll.addEventListener('click', () => {
|
||||
// Determine bounding box
|
||||
const box = this.sim.system.boundingBox;
|
||||
const objects = this.sim.select.selectedGroup;
|
||||
const box = this.sim.system.getBoundingBox(objects);
|
||||
const x = (box.start.x + box.end.x) / 2;
|
||||
const y = (box.start.y + box.end.y) / 2;
|
||||
const widthRatio = Math.abs(box.start.x - box.end.x) / this.sim.display.width;
|
||||
@ -74,7 +75,7 @@ export class Zoom extends Tool {
|
||||
const factor = Math.ceil(Math.log2(1 / ratio));
|
||||
|
||||
// Determine average momentum and set panning velocity to match
|
||||
const {netMomentum, totalMass} = this.sim.system.computeSystemCenter();
|
||||
const {netMomentum, totalMass} = this.sim.system.computeSystemCenter(objects);
|
||||
const netVelocity = {
|
||||
x: netMomentum.x / totalMass,
|
||||
y: netMomentum.y / totalMass,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user