Skip to content
Open
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
2 changes: 1 addition & 1 deletion command.go
Original file line number Diff line number Diff line change
Expand Up @@ -631,8 +631,8 @@ func (c *commandContext) VoxelFilter(resolution float32) error {

if selected {
c.editor.passThrough(c.baseFilter(false))
c.editor.pop()
c.editor.merge(pcFiltered)
c.editor.squashLatest()
} else {
if err := c.editor.SetPointCloud(pcFiltered, cloudMain); err != nil {
return err
Expand Down
52 changes: 41 additions & 11 deletions editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ func newEditor() *editor {
type history interface {
MaxHistory() int
SetMaxHistory(m int)
push(pp *pc.PointCloud) *pc.PointCloud
pop() *pc.PointCloud
undo() (*pc.PointCloud, bool)
push(p patch)
squashLatest()
undo(pp *pc.PointCloud) (*pc.PointCloud, bool)
clear()
}

func (e *editor) Undo() bool {
pp, ok := e.history.undo()
pp, ok := e.history.undo(e.pp)
if ok {
e.pp = pp
}
Expand Down Expand Up @@ -114,7 +114,13 @@ func (e *editor) SetPointCloud(pp *pc.PointCloud, id cloudID) error {
}
switch id {
case cloudMain:
e.pp = e.push(pcNew)
if e.pp != nil {
e.push(&replacePatch{
header: e.pp.PointCloudHeader.Clone(),
data: e.pp.Data,
})
}
e.pp = pcNew
case cloudSub:
e.ppSub = pcNew
it, err := pcNew.Vec3Iterator()
Expand Down Expand Up @@ -161,7 +167,11 @@ func (e *editor) label(fn func(int, mat.Vec3) (uint32, bool)) error {
itL.Incr()
i++
}
e.pp = e.push(pcNew)
e.push(&replacePatch{
header: e.pp.PointCloudHeader.Clone(),
data: e.pp.Data,
})
e.pp = pcNew
runtime.GC()
return nil
}
Expand All @@ -171,7 +181,11 @@ func (e *editor) passThrough(fn func(int, mat.Vec3) bool) error {
if err != nil {
return err
}
e.pp = e.push(pp)
e.push(&replacePatch{
header: e.pp.PointCloudHeader.Clone(),
data: e.pp.Data,
})
e.pp = pp
runtime.GC()
return nil
}
Expand All @@ -181,7 +195,11 @@ func (e *editor) passThroughByMask(sel []uint32, mask, val uint32) error {
if err != nil {
return err
}
e.pp = e.push(pp)
e.push(&replacePatch{
header: e.pp.PointCloudHeader.Clone(),
data: e.pp.Data,
})
e.pp = pp
runtime.GC()
return nil
}
Expand Down Expand Up @@ -214,7 +232,11 @@ func (e *editor) relabelPointsInLabelRange(minLabel, maxLabel, newLabel uint32)
lt.SetUint32(newLabel)
}

e.pp = e.push(pcNew)
e.push(&replacePatch{
header: e.pp.PointCloudHeader.Clone(),
data: e.pp.Data,
})
e.pp = pcNew
runtime.GC()
return nil
}
Expand Down Expand Up @@ -255,7 +277,11 @@ func (e *editor) unlabelPoints(labelsToKeep []uint32) error {
lt.SetUint32(0)
}

e.pp = e.push(pcNew)
e.push(&replacePatch{
header: e.pp.PointCloudHeader.Clone(),
data: e.pp.Data,
})
e.pp = pcNew
runtime.GC()
return nil
}
Expand Down Expand Up @@ -356,6 +382,10 @@ func (e *editor) merge(pp *pc.PointCloud) {
pcNew.Width = pcNew.Points
pcNew.Height = 1

e.pp = e.push(pcNew)
e.push(&replacePatch{
header: e.pp.PointCloudHeader.Clone(),
data: e.pp.Data,
})
e.pp = pcNew
runtime.GC()
}
124 changes: 124 additions & 0 deletions history_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package main

import (
"math/rand"
"reflect"
"testing"

"github.com/seqsense/pcgol/mat"
"github.com/seqsense/pcgol/pc"
)

func snapshotCloud(e *editor) *pc.PointCloud {
return cloneCloud(e.pp)
}

func applyRandomEdit(t *testing.T, e *editor, rnd *rand.Rand) {
t.Helper()
switch rnd.Intn(5) {
case 0: // label by position
if err := e.label(func(i int, _ mat.Vec3) (uint32, bool) {
return uint32(rnd.Intn(4)), rnd.Intn(2) == 0
}); err != nil {
t.Fatal(err)
}
case 1: // relabel range
if err := e.relabelPointsInLabelRange(0, uint32(rnd.Intn(3)), uint32(rnd.Intn(4))); err != nil {
t.Fatal(err)
}
case 2: // delete random points
if err := e.passThrough(func(i int, _ mat.Vec3) bool {
return rnd.Intn(4) != 0
}); err != nil {
t.Fatal(err)
}
case 3: // paste
n := 1 + rnd.Intn(20)
e.merge(makeTestCloud(t, n, n, 1))
case 4: // whole-cloud replacement
n := 50 + rnd.Intn(100)
if err := e.SetPointCloud(makeTestCloud(t, n, n, 1), cloudMain); err != nil {
t.Fatal(err)
}
}
}

func TestEditorUndoRoundTrip(t *testing.T) {
for trial := int64(0); trial < 10; trial++ {
rnd := rand.New(rand.NewSource(trial))

e := newEditor()
e.SetMaxHistory(100)
if err := e.SetPointCloud(makeTestCloud(t, 200, 20, 10), cloudMain); err != nil {
t.Fatal(err)
}

const nOps = 8
snapshots := []*pc.PointCloud{snapshotCloud(e)}
for k := 0; k < nOps; k++ {
applyRandomEdit(t, e, rnd)
snapshots = append(snapshots, snapshotCloud(e))
}

for k := nOps; k > 0; k-- {
assertCloudEqual(t, snapshots[k], e.pp)
if !e.Undo() {
t.Fatalf("trial %d: undo %d failed", trial, nOps-k)
}
}
assertCloudEqual(t, snapshots[0], e.pp)
if !reflect.DeepEqual(snapshots[0].PointCloudHeader, e.pp.PointCloudHeader) {
t.Fatalf("trial %d: header mismatch after undoing all edits", trial)
}

if e.Undo() {
t.Fatalf("trial %d: undo over the initial state must fail", trial)
}
}
}

func TestHistoryMaxDepth(t *testing.T) {
rnd := rand.New(rand.NewSource(1))

e := newEditor() // maxHistoryDefault = 4
if err := e.SetPointCloud(makeTestCloud(t, 100, 10, 10), cloudMain); err != nil {
t.Fatal(err)
}

for k := 0; k < 6; k++ {
applyRandomEdit(t, e, rnd)
}
for k := 0; k < maxHistoryDefault; k++ {
if !e.Undo() {
t.Fatalf("undo %d must succeed", k)
}
}
if e.Undo() {
t.Fatal("undo deeper than max_history must fail")
}

e.SetMaxHistory(0)
applyRandomEdit(t, e, rnd)
if e.Undo() {
t.Fatal("undo with max_history=0 must fail")
}
}

func TestHistorySquashLatest(t *testing.T) {
e := newEditor()
if err := e.SetPointCloud(makeTestCloud(t, 100, 10, 10), cloudMain); err != nil {
t.Fatal(err)
}
orig := snapshotCloud(e)

if err := e.passThrough(func(i int, _ mat.Vec3) bool { return i%2 == 0 }); err != nil {
t.Fatal(err)
}
e.merge(makeTestCloud(t, 10, 10, 1))
e.squashLatest()

if !e.Undo() {
t.Fatal("undo failed")
}
assertCloudEqual(t, orig, e.pp)
}
Loading
Loading