Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -2130,6 +2130,10 @@ Logic Interface CSS
background-image: url('../svg/motionStudy/table-view.svg');
}

.analytics-timeline-snapping-button {
background-image: url('../svg/motionStudy/magnet.svg');
}

.analytics-timeline-selection-row {
height: var(--mst-timeline-selection-row-height);
font-size: var(--mst-timeline-selection-row-font-size);
Expand Down
4 changes: 3 additions & 1 deletion src/humanPose/WalkingLens.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ function groundDistMeters(a, b) {
return Math.hypot(dx, dz);
}

export const WALKING_LENS_NAME = 'Walking vs Standing';

/**
* Step Number lens is a lens that stores the current step number (or 0) at the current timestamp
* This assessement is done by a analyst and is marked manually.
Expand All @@ -36,7 +38,7 @@ export class WalkingLens extends MotionStudyLens {
* @param {MotionStudy} motionStudy
*/
constructor(motionStudy) {
super('Walking vs Standing');
super(WALKING_LENS_NAME);
this.motionStudy = motionStudy;

/* sliding window of the latest poses: [{pos: THREE.Vector3, t: ms}, …] */
Expand Down
21 changes: 20 additions & 1 deletion src/motionStudy/TrackSidebar.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { intents, selectors, store, subscribeSelector } from '../store/index.js';
import { WALKING_LENS_NAME } from '../humanPose/WalkingLens.js';

export class TrackSidebar {
/**
Expand Down Expand Up @@ -93,9 +94,27 @@ export class TrackSidebar {
tableButton.classList.toggle('selected', newValue);
}, undefined, { fireImmediately: true });

// 4. --- snapping button --- toggles magnetic snapping on/off

let snappingButton = document.createElement('div');
snappingButton.classList.add('analytics-timeline-header-button');
let snappingButtonIcon = document.createElement('div');
snappingButtonIcon.classList.add('analytics-timeline-snapping-button', 'analytics-timeline-header-button-icon');
snappingButton.appendChild(snappingButtonIcon);

snappingButton.addEventListener('click', () => {
let isSnappingToggled = selectors.motionStudy.isTimelineSnappingToggled(store.getState());
store.dispatch(intents.motionStudy.toggleTimelineSnapping(!isSnappingToggled));
});

subscribeSelector(store, selectors.motionStudy.isTimelineSnappingToggled, (newValue) => {
snappingButton.classList.toggle('selected', newValue);
}, undefined, { fireImmediately: true });

shortcutRow.append(settingsButton);
shortcutRow.append(mapButton);
shortcutRow.append(tableButton);
shortcutRow.append(snappingButton);
return shortcutRow;
}

Expand Down Expand Up @@ -194,7 +213,7 @@ export class TrackSidebar {
ergonomicsRow.setAttribute(LENS_NAME_ATTR, 'Muri Ergonomics');
stepsRow.setAttribute(LENS_NAME_ATTR, 'Step Number');
tagRow.setAttribute(LENS_NAME_ATTR, 'Value Add/Waste Time');
walkingRow.setAttribute(LENS_NAME_ATTR, 'Walking vs Standing');
walkingRow.setAttribute(LENS_NAME_ATTR, WALKING_LENS_NAME);

const rows = [ergonomicsRow, stepsRow, tagRow, walkingRow];
rows[0].classList.add('analytics-timeline-first-track-row');
Expand Down
102 changes: 96 additions & 6 deletions src/motionStudy/timeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from './ValueAddWasteTimeManager.js';
import { intents, selectors, store } from '../store/index.js';
import { TrackSidebar } from './TrackSidebar.js';
import { WALKING_LENS_NAME } from '../humanPose/WalkingLens.js';

// Note: Some of these values propagate into the css variables via the `setCSSVariables` function.

Expand Down Expand Up @@ -141,6 +142,7 @@ export class Timeline {
this.highlightStartTime = -1;
this.regionCard = null;
this.lastRegionCardCacheKey = '';
this.lastBreakTimeStampsPerLens = {};

this.dragMode = DragMode.NONE;
this.mouseX = -1;
Expand Down Expand Up @@ -312,7 +314,7 @@ export class Timeline {
this.drawPinnedRegionCards();
this.drawPatches();
this.drawValueAddWasteTime();
this.drawPoses('Walking vs Standing', {insetRect: true});
this.drawPoses(WALKING_LENS_NAME, { insetRect: true, calculateBreakPoints: true });
this.drawSensors();

this.drawHighlightRegion();
Expand Down Expand Up @@ -622,7 +624,7 @@ export class Timeline {
}
}

drawPoses(lensName, options = {insetRect: false}) {
drawPoses(lensName, options = { insetRect: false, calculateBreakPoints: false }) {
let hpa = realityEditor.motionStudy.getActiveHumanPoseAnalyzer();
if (!lensName) {
lensName = hpa.activeLens.name;
Expand All @@ -633,7 +635,7 @@ export class Timeline {
return;
}
for (let spaghetti of Object.values(historyLines)) {
this.drawSpaghettiPoses(spaghetti.points, options);
this.drawSpaghettiPoses(spaghetti.points, lensName, options);
}
this.rowIndex += 1;
}
Expand Down Expand Up @@ -662,7 +664,7 @@ export class Timeline {
return [rgba[0] * dim, rgba[1] * dim, rgba[2] * dim, rgba[3] * dim];
}

drawSpaghettiPoses(poses, options) {
drawSpaghettiPoses(poses, lensName, options) {
let lastPose = poses[0];
let lastPoseTime = lastPose.timestamp;
let startSectionTime = lastPoseTime;
Expand All @@ -673,6 +675,10 @@ export class Timeline {

const timeMax = this.timeMin + this.widthMs;

if (options.calculateBreakPoints) {
this.lastBreakTimeStampsPerLens[lensName] = [];
}

for (const pose of poses) {
if (pose.timestamp < this.timeMin) {
startSectionTime = pose.timestamp;
Expand All @@ -687,6 +693,21 @@ export class Timeline {
const poseColor = this.recolorPoseForHighlight(pose.timestamp, pose.originalTimelineColor);
const lastPoseColor = this.recolorPoseForHighlight(lastPose.timestamp, lastPose.originalTimelineColor);
const isColorSwap = !rgbaEquals(poseColor, lastPoseColor);

// A gap or a color swap is a "snappable" moment
const isColorSwapIgnoringHighlight = !rgbaEquals(pose.originalTimelineColor, lastPose.originalTimelineColor);
if (options.calculateBreakPoints && (isGap || isColorSwapIgnoringHighlight)) {
if (isGap) {
// this will add the last point before a gap
this.lastBreakTimeStampsPerLens[lensName].push(lastPoseTime);
// this will add the first point after a gap
this.lastBreakTimeStampsPerLens[lensName].push(pose.timestamp);
} else {
// this adds the color swap point
this.lastBreakTimeStampsPerLens[lensName].push(lastPoseTime);
}
}

if (!isGap && !isColorSwap) {
lastPose = pose;
lastPoseTime = lastPose.timestamp;
Expand Down Expand Up @@ -1681,8 +1702,74 @@ export class Timeline {
}
}

getSnappedX(mouseX, options = { excludeSelectedStep: true }) {
if (!selectors.motionStudy.isTimelineSnappingToggled(store.getState())) {
return mouseX; // don't snap if the magnet mode is off
}

// get a list of all the key points and check if the mouseX is within a few pixels of any
const startEndTimes = this.getStartEndSnappableTimes();
const ergonomicsKeyTimes = this.getErgonomicsSnappableTimes();
const stepKeyTimes = this.getStepSnappableTimes(options);
const valueWasteKeyTimes = this.getValueWasteSnappableTimes();
const walkStandKeyTimes = this.getWalkStandSnappableTimes();
const allKeyTimes = [
startEndTimes,
ergonomicsKeyTimes,
stepKeyTimes,
valueWasteKeyTimes,
walkStandKeyTimes
].flat();
if (allKeyTimes.length === 0) return mouseX;

const allKeyXCoords = allKeyTimes.map(t => this.timeToX(t));
const closestKeyXCoord = allKeyXCoords.reduce((prev, curr) => Math.abs(curr - mouseX) < Math.abs(prev - mouseX) ? curr : prev);

// within this number of pixels, snap to the closest key moment - can be tuned if needed
const SNAP_THRESHOLD = 10;
if (Math.abs(closestKeyXCoord - mouseX) < SNAP_THRESHOLD) {
return closestKeyXCoord;
}

return mouseX;
}

getStartEndSnappableTimes() {
// the start and end of the timeline are always snappable
return [this.minTimeMin, this.maxTimeMax];
}

getErgonomicsSnappableTimes() {
return []; // TODO: detect significant peaks/valleys to snap to
}

getStepSnappableTimes(options = { excludeSelectedStep: true }) {
// add the start and end times of all step cards
const snappableTimes = [];
for (const prc of this.motionStudy.pinnedRegionCards) {
// use this option to prevent snapping to a step's own endpoints when resizing it
if (prc.displayActive && options.excludeSelectedStep) continue;
snappableTimes.push(prc.startTime);
snappableTimes.push(prc.endTime);
}
return snappableTimes; // might have some duplicates but not a big issue
}

getWalkStandSnappableTimes() {
return this.lastBreakTimeStampsPerLens[WALKING_LENS_NAME] ?? [];
}

getValueWasteSnappableTimes() {
const snappableTimes = [];
this.motionStudy.valueAddWasteTimeManager.regions.forEach((region) => {
snappableTimes.push(region.startTime);
snappableTimes.push(region.endTime);
});
return snappableTimes;
}

onPointerMoveDragModeMoveCursor(_event) {
let newCursorTime = this.xToTime(this.mouseX);
let newCursorTime = this.xToTime(this.getSnappedX(this.mouseX), { excludeSelectedStep: false });
if (newCursorTime < this.timeMin) {
newCursorTime = this.timeMin;
}
Expand All @@ -1707,9 +1794,12 @@ export class Timeline {
}

this.canvas.style.cursor = 'col-resize';
let highlightEndTime = this.xToTime(this.mouseX);
let highlightEndTime = this.xToTime(this.getSnappedX(this.mouseX), { excludeSelectedStep: true });

let startTime = Math.min(this.highlightStartTime, highlightEndTime);
if (selectors.motionStudy.isTimelineSnappingToggled(store.getState())) {
startTime += 1; // fix rounding error when snapping start needle, to render correctly on timeline
}
let endTime = Math.max(this.highlightStartTime, highlightEndTime);
this.motionStudy.setHighlightRegion({
startTime,
Expand Down
13 changes: 13 additions & 0 deletions src/store/slices/motionStudy.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const types = {
// various check-box settings from the settings menu:
TOGGLE_PATH_MAP: 'motionStudy/TOGGLE_PATH_MAP',
TOGGLE_TABLE_VIEW: 'motionStudy/TOGGLE_TABLE_VIEW',
TOGGLE_TIMELINE_SNAPPING: 'motionStudy/TOGGLE_TIMELINE_SNAPPING',
TOGGLE_CHILD_HUMAN_OBJECTS: 'motionStudy/TOGGLE_CHILD_HUMAN_OBJECTS',
TOGGLE_LIVE_HISTORY_LINES: 'motionStudy/TOGGLE_LIVE_HISTORY_LINES',
TOGGLE_FILTER_UNRELIABLE_JOINTS: 'motionStudy/TOGGLE_FILTER_UNRELIABLE_JOINTS',
Expand All @@ -38,6 +39,7 @@ export const motionStudyInitial = {
// various check-box settings from the settings menu:
isPathMapToggled: false, // doesn't say for which tool - derive that from the focused tool
isTableViewToggled: false, // doesn't say for which tool - derive that from the focused tool
isTimelineSnappingToggled: false, // if true, dragging the timeline will snap to significant points
areChildHumanObjectsVisible: false, // auxiliary human objects supporting fused human objects
areLiveHistoryLinesVisible: false,
jointConfidenceThreshold: JOINT_CONFIDENCE_THRESHOLD, // toggles to 0 if "Filter unreliable joints" turned off
Expand All @@ -61,6 +63,7 @@ export const actions = {
// various check-box settings from the settings menu:
togglePathMap: (newIsToggled) => ({ type: types.TOGGLE_PATH_MAP, payload: { value: newIsToggled } }),
toggleTableView: (newIsToggled) => ({ type: types.TOGGLE_TABLE_VIEW, payload: { value: newIsToggled } }),
toggleTimelineSnapping: (newIsToggled) => ({ type: types.TOGGLE_TIMELINE_SNAPPING, payload: { value: newIsToggled } }),
toggleChildHumanObjectsVisible: (newIsToggled) => ({ type: types.TOGGLE_CHILD_HUMAN_OBJECTS, payload: { value: newIsToggled } }),
toggleLiveHistoryLinesVisible: (newIsToggled) => ({ type: types.TOGGLE_LIVE_HISTORY_LINES, payload: { value: newIsToggled } }),
toggleFilterUnreliableJoints: (newIsToggled) => ({ type: types.TOGGLE_FILTER_UNRELIABLE_JOINTS, payload: { value: newIsToggled } }),
Expand Down Expand Up @@ -89,6 +92,8 @@ export function motionStudyReducer(state = motionStudyInitial, action) {
return { ...state, isPathMapToggled: action.payload.value };
case types.TOGGLE_TABLE_VIEW:
return { ...state, isTableViewToggled: action.payload.value };
case types.TOGGLE_TIMELINE_SNAPPING:
return { ...state, isTimelineSnappingToggled: action.payload.value };
case types.EXPAND_SETTINGS_MENU:
return { ...state, isSettingsMenuExpanded: action.payload.value };
case types.ALWAYS_SHOW_ANALYTICS_MENU:
Expand Down Expand Up @@ -142,6 +147,7 @@ function makeSelectors(_sliceName, { slice }) {
const currentLensName = (s) => root(s)?.currentLensName ?? null;
const isPathMapToggled = (s) => root(s)?.isPathMapToggled ?? false;
const isTableViewToggled = (s) => root(s)?.isTableViewToggled ?? false;
const isTimelineSnappingToggled = (s) => root(s)?.isTimelineSnappingToggled ?? false;
const liveHistoryLastResetTimestamp = (s) => root(s)?.liveHistoryLastResetTimestamp ?? null;
const isSettingsMenuExpanded = (s) => root(s)?.isSettingsMenuExpanded ?? false;
const isSettingsMenuAlwaysInBar = (s) => root(s)?.isSettingsMenuAlwaysInBar ?? false;
Expand Down Expand Up @@ -171,6 +177,7 @@ function makeSelectors(_sliceName, { slice }) {
currentLensName,
isPathMapToggled,
isTableViewToggled,
isTimelineSnappingToggled,
liveHistoryLastResetTimestamp,
isSettingsMenuExpanded,
isSettingsMenuAlwaysInBar,
Expand Down Expand Up @@ -240,6 +247,12 @@ function makeIntents(_sliceName, /* { actions } */) {
if (newIsToggled === prevToggled) return;
dispatch(actions.toggleTableView(newIsToggled));
},
toggleTimelineSnapping: (newIsToggled) => (dispatch, getState) => {
const s = getState();
const prevToggled = s.motionStudy.isTimelineSnappingToggled;
if (newIsToggled === prevToggled) return;
dispatch(actions.toggleTimelineSnapping(newIsToggled));
},
toggleChildHumanObjectsVisible: (newIsToggled) => (dispatch, getState) => {
const s = getState();
const prevToggled = s.motionStudy.areChildHumanObjectsVisible;
Expand Down
4 changes: 4 additions & 0 deletions src/store/tests/motionStudy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ describe('motionStudy reducer: core ops', () => {
s = motionStudyReducer(s, motionStudyActions.toggleTableView(true));
expect(s.isTableViewToggled).toBe(true);

s = motionStudyReducer(s, motionStudyActions.toggleTimelineSnapping(true));
expect(s.isTimelineSnappingToggled).toBe(true);

s = motionStudyReducer(s, motionStudyActions.toggleChildHumanObjectsVisible(true));
expect(s.areChildHumanObjectsVisible).toBe(true);

Expand Down Expand Up @@ -121,6 +124,7 @@ describe('motionStudy selectors', () => {
expect(selectors.currentLensName(base)).toBe('Muri Ergonomics');
expect(selectors.isPathMapToggled(base)).toBe(false);
expect(selectors.isTableViewToggled(base)).toBe(false);
expect(selectors.isTimelineSnappingToggled(base)).toBe(false);
expect(selectors.isSettingsMenuExpanded(base)).toBe(false);
expect(selectors.isSettingsMenuAlwaysInBar(base)).toBe(false);
expect(selectors.areChildHumanObjectsVisible(base)).toBe(false);
Expand Down
9 changes: 9 additions & 0 deletions svg/motionStudy/magnet.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.