Skip to content

Commit

Permalink
feat(frontend): paint voxel (#18)
Browse files Browse the repository at this point in the history
* feat(frontend): dispatch paint-voxel action

* feat(frontend): set voxel on paint

* feat(frontend): finish renaming `addVoxel` to `setVoxel`

* feat(frontend): save old/new values and undo/redo painting

* refactor(frontend): s/positions/coord

* refactor(frontend): fix prettier issue
  • Loading branch information
PhilippeMorier authored Jan 20, 2020
1 parent 03a4ee2 commit d7a15fa
Show file tree
Hide file tree
Showing 13 changed files with 149 additions and 89 deletions.
37 changes: 24 additions & 13 deletions apps/frontend/src/app/scene-viewer-container/grid.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,44 +24,54 @@ export class GridService {
};

/**
* Adds a new voxel via accessor to share access path.
* Sets a new voxel via accessor to share access path.
* @returns origin of `InternalNode1` of affected node (node containing added voxel).
*/
addVoxel(xyz: Coord, value: number): VoxelChange {
this.accessor.setValueOn(xyz, value);
setVoxel(xyz: Coord, newValue: number): VoxelChange {
const oldValue = this.accessor.getValue(xyz);

this.accessor.setValueOn(xyz, newValue);

return {
affectedNodeOrigin: this.accessor.internalNode1Origin,
value,
position: xyz,
newValue,
oldValue,
xyz,
};
}

addVoxels(coords: Coord[], values: number[]): VoxelChange[] {
setVoxels(coords: Coord[], newValues: number[]): VoxelChange[] {
const changes = new Map<string, VoxelChange>();

if (coords.length !== values.length) {
throw new Error(`Coordinates and values don't have the same length.`);
if (coords.length !== newValues.length) {
throw new Error(`Coordinates and new values don't have the same length.`);
}

coords.forEach((xyz, i) => {
const change = this.addVoxel(xyz, values[i]);
const change = this.setVoxel(xyz, newValues[i]);
changes.set(change.affectedNodeOrigin.toString(), change);
});

return Array.from(changes.values());
}

removeVoxel(xyz: Coord): VoxelChange {
const oldValue = this.accessor.getValue(xyz);

this.accessor.setActiveState(xyz, false);

return {
affectedNodeOrigin: this.accessor.internalNode1Origin,
value: this.accessor.getValue(xyz),
position: xyz,
oldValue,
newValue: oldValue,
xyz,
};
}

paintVoxel(xyz: Coord, newValue: number): VoxelChange {
return this.setVoxel(xyz, newValue);
}

computeInternalNode1Mesh(origin: Coord): MeshData | undefined {
const internal1 = this.accessor.probeInternalNode1(origin);

Expand All @@ -79,6 +89,7 @@ export class GridService {

export interface VoxelChange {
affectedNodeOrigin: Coord;
value: number;
position: Coord;
newValue: number;
oldValue: number;
xyz: Coord;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,37 @@ import { VoxelChange } from './grid.service';

const actionTypePrefix = `[sceneViewerContainer]`;

export const addVoxel = createAction(
`${actionTypePrefix} Add voxel`,
props<{ position: Coord; value: number }>(),
// Set voxel
export const setVoxel = createAction(
`${actionTypePrefix} Set voxel`,
props<{ xyz: Coord; newValue: number }>(),
);
export const addVoxelFailed = createAction(`${actionTypePrefix} Add voxel failed`);
export const voxelAdded = createAction(`${actionTypePrefix} Voxel added`, props<VoxelChange>());
export const setVoxelFailed = createAction(`${actionTypePrefix} Set voxel failed`);
export const voxelSet = createAction(`${actionTypePrefix} Voxel set`, props<VoxelChange>());

export const addVoxels = createAction(
`${actionTypePrefix} Add voxels`,
props<{ positions: Coord[]; values: number[] }>(),
// Set voxels
export const setVoxels = createAction(
`${actionTypePrefix} Set voxels`,
props<{ coords: Coord[]; newValues: number[] }>(),
);
export const addVoxelsFailed = createAction(`${actionTypePrefix} Add voxels failed`);
export const voxelsAdded = createAction(
`${actionTypePrefix} Voxels added`,
export const setVoxelsFailed = createAction(`${actionTypePrefix} Set voxels failed`);
export const voxelsSet = createAction(
`${actionTypePrefix} Voxels set`,
props<{ voxelChanges: VoxelChange[] }>(),
);

// Remove voxel
export const removeVoxel = createAction(
`${actionTypePrefix} Remove voxel`,
props<{ position: Coord }>(),
props<{ xyz: Coord }>(),
);
export const removeVoxelFailed = createAction(`${actionTypePrefix} Remove voxel failed`);
export const voxelRemoved = createAction(`${actionTypePrefix} Voxel removed`, props<VoxelChange>());

// Paint voxel
export const paintVoxel = createAction(
`${actionTypePrefix} Paint voxel`,
props<{ xyz: Coord; newValue: number }>(),
);
export const paintVoxelFailed = createAction(`${actionTypePrefix} Paint voxel failed`);
export const voxelPainted = createAction(`${actionTypePrefix} Voxel painted`, props<VoxelChange>());
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Coord } from '@talus/vdb';
import { Subject } from 'rxjs';
import * as fromApp from '../app.reducer';
import { Tool } from '../tools-panel/tool.model';
import { addVoxel, removeVoxel } from './scene-viewer-container.actions';
import { removeVoxel, setVoxel } from './scene-viewer-container.actions';
import { SceneViewerContainerComponent } from './scene-viewer-container.component';

@Component({
Expand Down Expand Up @@ -39,7 +39,7 @@ describe('SceneViewerContainerComponent', () => {

mockSelectedToolIdSelector = mockStore.overrideSelector(
fromApp.selectSelectedToolId,
Tool.AddVoxel,
Tool.SetVoxel,
);
}));

Expand Down Expand Up @@ -115,10 +115,10 @@ describe('SceneViewerContainerComponent', () => {
[0, 0, 1],
],
])(
'should dispatch `addVoxel` action for %j',
'should dispatch `setVoxel` action for %j',
(pickedPoint: Coord, position: Coord, normal: Coord) => {
const initialAction = addVoxel({ position: [0, 0, 0], value: 42 });
const action = addVoxel({ position, value: 1 });
const initialAction = setVoxel({ xyz: [0, 0, 0], newValue: 42 });
const action = setVoxel({ xyz: position, newValue: 1 });

stubComponent.pointerPick.next({
pickedPoint,
Expand Down Expand Up @@ -174,7 +174,7 @@ describe('SceneViewerContainerComponent', () => {
mockStore.refreshState();
fixture.detectChanges();

const action = removeVoxel({ position });
const action = removeVoxel({ xyz: position });

stubComponent.pointerPick.next({
pickedPoint,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Coord } from '@talus/vdb';
import { Observable } from 'rxjs';
import * as fromApp from '../app.reducer';
import { Tool } from '../tools-panel/tool.model';
import { addVoxel, removeVoxel } from './scene-viewer-container.actions';
import { paintVoxel, removeVoxel, setVoxel } from './scene-viewer-container.actions';

@Component({
selector: 'fe-scene-viewer-container',
Expand All @@ -29,7 +29,7 @@ export class SceneViewerContainerComponent implements AfterViewInit {
constructor(private store: Store<fromApp.State>) {}

ngAfterViewInit(): void {
this.store.dispatch(addVoxel({ position: [0, 0, 0], value: 42 }));
this.store.dispatch(setVoxel({ xyz: [0, 0, 0], newValue: 42 }));
}

onPointerPick(event: UiPointerPickInfo, selectedToolId: Tool): void {
Expand All @@ -47,13 +47,16 @@ export class SceneViewerContainerComponent implements AfterViewInit {
}

switch (selectedToolId) {
case Tool.AddVoxel:
this.store.dispatch(
addVoxel({ position: this.calcVoxelToAddPosition(pickInfo), value: 1 }),
);
case Tool.SetVoxel:
this.store.dispatch(setVoxel({ xyz: this.calcVoxelToAddPosition(pickInfo), newValue: 1 }));
break;
case Tool.RemoveVoxel:
this.store.dispatch(removeVoxel({ position: this.calcVoxelToRemovePosition(pickInfo) }));
this.store.dispatch(removeVoxel({ xyz: this.calcClickedVoxelPosition(pickInfo) }));
break;
case Tool.PaintVoxel:
this.store.dispatch(
paintVoxel({ xyz: this.calcClickedVoxelPosition(pickInfo), newValue: 4 }),
);
break;
}
}
Expand All @@ -80,7 +83,7 @@ export class SceneViewerContainerComponent implements AfterViewInit {
return newPoint;
}

private calcVoxelToRemovePosition(pickInfo: UiPointerPickInfo): Coord {
private calcClickedVoxelPosition(pickInfo: UiPointerPickInfo): Coord {
const pickedIntegerPoint = this.roundDimensionAlongNormal(pickInfo);

const newPoint: Coord = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ import { of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { GridService } from './grid.service';
import {
addVoxel,
addVoxelFailed,
addVoxels,
addVoxelsFailed,
paintVoxel,
paintVoxelFailed,
removeVoxel,
removeVoxelFailed,
voxelAdded,
setVoxel,
setVoxelFailed,
setVoxels,
setVoxelsFailed,
voxelPainted,
voxelRemoved,
voxelsAdded,
voxelSet,
voxelsSet,
} from './scene-viewer-container.actions';

@Injectable()
Expand All @@ -25,37 +28,46 @@ export class SceneViewerContainerEffects {
private sceneViewerService: UiSceneViewerService,
) {}

addVoxel$ = createEffect(() =>
setVoxel$ = createEffect(() =>
this.actions$.pipe(
ofType(addVoxel),
map(({ position, value }) => this.gridService.addVoxel(position, value)),
map(voxelAdded),
catchError(() => of(addVoxelFailed())),
ofType(setVoxel),
map(({ xyz, newValue }) => this.gridService.setVoxel(xyz, newValue)),
map(voxelSet),
catchError(() => of(setVoxelFailed())),
),
);

addVoxels$ = createEffect(() =>
setVoxels$ = createEffect(() =>
this.actions$.pipe(
ofType(addVoxels),
map(({ positions, values }) => this.gridService.addVoxels(positions, values)),
map(voxelChanges => voxelsAdded({ voxelChanges })),
catchError(() => of(addVoxelsFailed())),
ofType(setVoxels),
map(({ coords, newValues }) => this.gridService.setVoxels(coords, newValues)),
map(voxelChanges => voxelsSet({ voxelChanges })),
catchError(() => of(setVoxelsFailed())),
),
);

removeVoxel$ = createEffect(() =>
this.actions$.pipe(
ofType(removeVoxel),
map(({ position }) => this.gridService.removeVoxel(position)),
map(({ xyz }) => this.gridService.removeVoxel(xyz)),
map(voxelRemoved),
catchError(() => of(removeVoxelFailed())),
),
);

paintVoxel$ = createEffect(() =>
this.actions$.pipe(
ofType(paintVoxel),
map(({ xyz, newValue }) => this.gridService.paintVoxel(xyz, newValue)),
map(voxelPainted),
catchError(() => of(paintVoxelFailed())),
),
);

updateGridMesh$ = createEffect(
() =>
this.actions$.pipe(
ofType(voxelAdded, voxelRemoved),
ofType(voxelSet, voxelRemoved, voxelPainted),
map(({ affectedNodeOrigin }) => this.computeAndUpdateNodeMesh(affectedNodeOrigin)),
),
{ dispatch: false },
Expand All @@ -64,7 +76,7 @@ export class SceneViewerContainerEffects {
updateGridMeshMultiple$ = createEffect(
() =>
this.actions$.pipe(
ofType(voxelsAdded),
ofType(voxelsSet),
map(({ voxelChanges }) =>
voxelChanges.map(change => {
this.computeAndUpdateNodeMesh(change.affectedNodeOrigin);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
import { VoxelChange } from './grid.service';
import { voxelAdded, voxelRemoved } from './scene-viewer-container.actions';
import { voxelRemoved, voxelSet } from './scene-viewer-container.actions';
import { reducer, selectVoxelCount } from './scene-viewer-container.reducer';

describe('SceneViewerContainerReducer', () => {
const voxelChange: VoxelChange = {
position: [0, 0, 0],
xyz: [0, 0, 0],
affectedNodeOrigin: [0, 0, 0],
value: 42,
oldValue: 24,
newValue: 42,
};

it('should increment counter', () => {
const stateWithOneVoxel = reducer(undefined, voxelAdded(voxelChange));
const stateWithOneVoxel = reducer(undefined, voxelSet(voxelChange));

expect(stateWithOneVoxel.voxelCount).toEqual(1);
});

it('should decrement counter', () => {
const stateWithOneVoxel = reducer(undefined, voxelAdded(voxelChange));
const stateWithOneVoxel = reducer(undefined, voxelSet(voxelChange));
const stateWithNoVoxel = reducer(stateWithOneVoxel, voxelRemoved(voxelChange));

expect(stateWithNoVoxel.voxelCount).toEqual(0);
});

it('should select voxel count', () => {
const stateWithOneVoxel = reducer(undefined, voxelAdded(voxelChange));
const stateWithOneVoxel = reducer(undefined, voxelSet(voxelChange));

expect(selectVoxelCount(stateWithOneVoxel)).toEqual(1);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createReducer, on } from '@ngrx/store';
import { voxelAdded, voxelRemoved } from './scene-viewer-container.actions';
import { voxelRemoved, voxelSet } from './scene-viewer-container.actions';

export const featureKey = 'sceneViewerContainer';

Expand All @@ -13,7 +13,7 @@ export const initialState: State = {

export const reducer = createReducer(
initialState,
on(voxelAdded, state => {
on(voxelSet, state => {
return {
...state,
voxelCount: state.voxelCount + 1,
Expand Down
3 changes: 2 additions & 1 deletion apps/frontend/src/app/tools-panel/tool.model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export enum Tool {
AddVoxel = 'AddVoxel',
SetVoxel = 'SetVoxel',
RemoveVoxel = 'RemoveVoxel',
PaintVoxel = 'PaintVoxel',
}
9 changes: 7 additions & 2 deletions apps/frontend/src/app/tools-panel/tools-panel.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,19 @@ export class ToolsPanelComponent {
tools: UiToolbarToolConfig<Tool>[] = [
{
icon: 'add_circle_outline',
tooltip: '@@@ Add voxel',
value: Tool.AddVoxel,
tooltip: '@@@ Set voxel',
value: Tool.SetVoxel,
},
{
icon: 'remove_circle_outline',
tooltip: '@@@ Remove voxel',
value: Tool.RemoveVoxel,
},
{
icon: 'brush',
tooltip: '@@@ Paint voxel',
value: Tool.PaintVoxel,
},
];

constructor(private store: Store<fromApp.State>) {
Expand Down
Loading

0 comments on commit d7a15fa

Please sign in to comment.