Skip to content
53 changes: 44 additions & 9 deletions editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,27 +177,29 @@ func (e *editor) mutateLabels(fn func(i int, l uint32) (uint32, bool)) error {
}

func (e *editor) passThrough(fn func(int, mat.Vec3) bool) error {
pp, err := passThrough(e.pp, fn)
it, err := e.pp.Vec3Iterator()
if err != nil {
return err
}
if err := e.push(newUndoDataEntireCloud(e.pp)); err != nil {
pcNew, p := compactInPlace(e.pp, func(i int) bool {
return fn(i, it.Vec3At(i))
})
e.pp = pcNew
if err := e.push(p); err != nil {
return err
}
e.pp = pp
runtime.GC()
return nil
}

func (e *editor) passThroughByMask(sel []uint32, mask, val uint32) error {
pp, err := passThroughByMask(e.pp, sel, mask, val)
if err != nil {
return err
}
if err := e.push(newUndoDataEntireCloud(e.pp)); err != nil {
pcNew, p := compactInPlace(e.pp, func(i int) bool {
return sel[i]&mask == val
})
e.pp = pcNew
if err := e.push(p); err != nil {
return err
}
e.pp = pp
runtime.GC()
return nil
}
Expand All @@ -222,6 +224,39 @@ func (e *editor) unlabelPoints(labelsToKeep []uint32) error {
})
}

// Spare capacity of pp.Data is kept so that undo can re-expand without allocation
func compactInPlace(pp *pc.PointCloud, keep func(i int) bool) (*pc.PointCloud, *undoDataRemovedPoints) {
stride := pp.Stride()
p := &undoDataRemovedPoints{OldWidth: pp.Width, OldHeight: pp.Height}
n := pp.Points
j := 0
runStart := -1
flush := func(end int) {
if runStart < 0 {
return
}
cnt := end - runStart
if runStart != j {
copy(pp.Data[j*stride:(j+cnt)*stride], pp.Data[runStart*stride:end*stride])
}
j += cnt
runStart = -1
}
for i := 0; i < n; i++ {
if keep(i) {
if runStart < 0 {
runStart = i
}
continue
}
flush(i)
p.Indices = append(p.Indices, uint32(i))
p.points = append(p.points, pp.Data[i*stride:(i+1)*stride]...)
}
flush(n)
return newCloudView(pp, j, j, 1, pp.Data[:j*stride]), p
}

func passThrough(pp *pc.PointCloud, fn func(int, mat.Vec3) bool) (*pc.PointCloud, error) {
it, err := pp.Vec3Iterator()
if err != nil {
Expand Down
53 changes: 53 additions & 0 deletions record.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func init() {
gob.Register(&undoDataEntireCloud{})
gob.Register(&undoDataLabels{})
gob.Register(&undoDataSize{})
gob.Register(&undoDataRemovedPoints{})
}

// pcgol caches an unsafe float32 alias of Data keyed only by its base pointer,
Expand Down Expand Up @@ -111,6 +112,58 @@ func (p *undoDataLabels) payload() []byte {

func (p *undoDataLabels) setPayload([]byte) {}

type undoDataRemovedPoints struct {
OldWidth, OldHeight int
Indices []uint32 // ascending original positions of the removed points
points []byte
}

func (p *undoDataRemovedPoints) restore(pp *pc.PointCloud) (*pc.PointCloud, error) {
stride := pp.Stride()
if len(p.points) != len(p.Indices)*stride {
return nil, errBrokenRecord
}
oldN := pp.Points + len(p.Indices)
need := oldN * stride
var data []byte
if cap(pp.Data) >= need {
data = pp.Data[:need]
} else {
data = make([]byte, need)
copy(data, pp.Data)
}

// Walk backwards so that every move reads a not-yet-overwritten position
di := len(p.Indices) - 1
src := pp.Points - 1
for dst := oldN - 1; dst >= 0; dst-- {
if di >= 0 && int(p.Indices[di]) == dst {
copy(data[dst*stride:(dst+1)*stride], p.points[di*stride:(di+1)*stride])
di--
} else {
if src < 0 {
return nil, errBrokenRecord
}
if dst != src {
copy(data[dst*stride:(dst+1)*stride], data[src*stride:(src+1)*stride])
}
src--
}
}
if di >= 0 {
return nil, errBrokenRecord
}
return newCloudView(pp, oldN, p.OldWidth, p.OldHeight, data), nil
}

func (p *undoDataRemovedPoints) payload() []byte {
return p.points
}

func (p *undoDataRemovedPoints) setPayload(data []byte) {
p.points = data
}

type undoDataSize struct {
Points, Width, Height int
}
Expand Down
73 changes: 73 additions & 0 deletions record_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,74 @@ func TestUndoDataLabelsRestore(t *testing.T) {
assertCloudEqual(t, orig, out)
}

func deleteForTest(pp *pc.PointCloud, removed map[int]bool) *undoDataRemovedPoints {
stride := pp.Stride()
p := &undoDataRemovedPoints{
OldWidth: pp.Width,
OldHeight: pp.Height,
}
j := 0
for i := 0; i < pp.Points; i++ {
if removed[i] {
p.Indices = append(p.Indices, uint32(i))
p.points = append(p.points, pp.Data[i*stride:(i+1)*stride]...)
continue
}
if i != j {
copy(pp.Data[j*stride:(j+1)*stride], pp.Data[i*stride:(i+1)*stride])
}
j++
}
pp.Points = j
pp.Width = j
pp.Height = 1
pp.Data = pp.Data[:j*stride]
return p
}

func TestUndoDataRemovedPointsRestore(t *testing.T) {
for name, removed := range map[string]map[int]bool{
"Scattered": {1: true, 5: true, 6: true, 99: true},
"Head": {0: true, 1: true, 2: true},
"Tail": {97: true, 98: true, 99: true},
"All": allIndices(100),
"None": {},
} {
t.Run(name, func(t *testing.T) {
orig := makeTestCloud(t, 100, 10, 10)
t.Run("KeptCapacity", func(t *testing.T) {
pp := cloneCloud(orig)
p := deleteForTest(pp, removed)
out, err := p.restore(pp)
if err != nil {
t.Fatal(err)
}
assertCloudEqual(t, orig, out)
})
t.Run("NoSpareCapacity", func(t *testing.T) {
pp := cloneCloud(orig)
p := deleteForTest(pp, removed)
// Drop the spare capacity to exercise the reallocation
// path (a no-op for the None pattern).
pp.Data = append([]byte{}, pp.Data...)
out, err := p.restore(pp)
if err != nil {
t.Fatal(err)
}
assertCloudEqual(t, orig, out)
})
})
}
}

func allIndices(n int) map[int]bool {
m := map[int]bool{}
for i := 0; i < n; i++ {
m[i] = true
}
return m
}

func TestUndoDataSizeRestore(t *testing.T) {
orig := makeTestCloud(t, 100, 10, 10)
pp := cloneCloud(orig)
Expand Down Expand Up @@ -114,6 +182,11 @@ func TestRecordEncodeDecodeRoundTrip(t *testing.T) {
"EntireCloud": newUndoDataEntireCloud(orig),
"Labels": &undoDataLabels{Indices: []uint32{1, 2, 42}, OldLabels: []uint32{7, 8, 9}},
"Size": &undoDataSize{Points: 90, Width: 9, Height: 10},
"RemovedPoints": &undoDataRemovedPoints{
OldWidth: 10, OldHeight: 10,
Indices: []uint32{0, 50, 99},
points: bytes.Repeat([]byte{1, 2, 3, 4}, 3*4),
},
} {
t.Run(name, func(t *testing.T) {
var buf bytes.Buffer
Expand Down
Loading