Skip to content
Draft
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
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