diff --git a/editor.go b/editor.go index be72fd1..3e8fd96 100644 --- a/editor.go +++ b/editor.go @@ -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 } @@ -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 { diff --git a/record.go b/record.go index 9ca1c00..1674dd4 100644 --- a/record.go +++ b/record.go @@ -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, @@ -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 } diff --git a/record_test.go b/record_test.go index 256b0e3..570866a 100644 --- a/record_test.go +++ b/record_test.go @@ -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) @@ -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