diff --git a/editor.go b/editor.go index 0c0442f..c273785 100644 --- a/editor.go +++ b/editor.go @@ -324,18 +324,13 @@ func passThroughImpl(pp *pc.PointCloud, core func(_, _ *pc.PointCloud) int) (*pc } func (e *editor) merge(pp *pc.PointCloud) { - pcNew := &pc.PointCloud{ - PointCloudHeader: e.pp.PointCloudHeader.Clone(), - Points: e.pp.Points + pp.Points, - Data: append(e.pp.Data[:e.pp.Stride()*e.pp.Points], pp.Data...), - } - pcNew.Width = pcNew.Points - pcNew.Height = 1 - - e.push(&replacePatch{ - header: e.pp.PointCloudHeader.Clone(), - data: e.pp.Data, + e.push(&appendPatch{ + oldPoints: e.pp.Points, + oldWidth: e.pp.Width, + oldHeight: e.pp.Height, }) - e.pp = pcNew + n := e.pp.Points + pp.Points + data := append(e.pp.Data[:e.pp.Stride()*e.pp.Points], pp.Data...) + e.pp = newCloudView(e.pp, n, n, 1, data) runtime.GC() } diff --git a/patch.go b/patch.go index 7032a60..17d88e6 100644 --- a/patch.go +++ b/patch.go @@ -30,6 +30,19 @@ var ( errNoLabelField = errors.New("point cloud has no label field") ) +// pcgol caches an unsafe float32 alias of Data keyed only by its base pointer, +// so a change of the Data length must be delivered in a fresh PointCloud. +func newCloudView(pp *pc.PointCloud, points, width, height int, data []byte) *pc.PointCloud { + out := &pc.PointCloud{ + PointCloudHeader: pp.PointCloudHeader.Clone(), + Points: points, + Data: data, + } + out.Width = width + out.Height = height + return out +} + func fieldByteOffset(h *pc.PointCloudHeader, name string) (int, bool) { offset := 0 for i, fn := range h.Fields { @@ -76,6 +89,30 @@ func (p *labelPatch) payload() []byte { return nil } +type appendPatch struct { + oldPoints, oldWidth, oldHeight int +} + +func (p *appendPatch) revert(pp *pc.PointCloud) (*pc.PointCloud, error) { + stride := pp.Stride() + if stride <= 0 || p.oldPoints < 0 || p.oldWidth < 0 || p.oldHeight < 0 || + p.oldPoints > pp.Points || p.oldPoints > len(pp.Data)/stride { + return nil, errBrokenPatch + } + return newCloudView(pp, p.oldPoints, p.oldWidth, p.oldHeight, pp.Data[:p.oldPoints*stride]), nil +} + +func (p *appendPatch) encodeHead(buf *bytes.Buffer) { + buf.WriteByte(patchTypeAppend) + writeUint32(buf, uint32(p.oldPoints)) + writeUint32(buf, uint32(p.oldWidth)) + writeUint32(buf, uint32(p.oldHeight)) +} + +func (p *appendPatch) payload() []byte { + return nil +} + type replacePatch struct { header pc.PointCloudHeader data []byte @@ -150,6 +187,16 @@ func decodePatch(b []byte) (patch, []byte, error) { return nil, nil, r.err } return p, r.b, nil + case patchTypeAppend: + p := &appendPatch{ + oldPoints: int(r.uint32()), + oldWidth: int(r.uint32()), + oldHeight: int(r.uint32()), + } + if r.err != nil { + return nil, nil, r.err + } + return p, r.b, nil case patchTypeReplace: p := &replacePatch{} p.header.Version = math.Float32frombits(r.uint32()) diff --git a/patch_test.go b/patch_test.go index 42abd88..3b96125 100644 --- a/patch_test.go +++ b/patch_test.go @@ -73,6 +73,24 @@ func TestLabelPatchRevert(t *testing.T) { assertCloudEqual(t, orig, out) } +func TestAppendPatchRevert(t *testing.T) { + orig := makeTestCloud(t, 100, 10, 10) + pp := cloneCloud(orig) + + p := &appendPatch{oldPoints: pp.Points, oldWidth: pp.Width, oldHeight: pp.Height} + added := makeTestCloud(t, 10, 10, 1) + pp.Data = append(pp.Data, added.Data...) + pp.Points += added.Points + pp.Width = pp.Points + pp.Height = 1 + + out, err := p.revert(pp) + if err != nil { + t.Fatal(err) + } + assertCloudEqual(t, orig, out) +} + func TestReplacePatchRevert(t *testing.T) { orig := makeTestCloud(t, 100, 10, 10) orig.Viewpoint = []float32{0, 0, 0, 1, 0, 0, 0} @@ -97,6 +115,7 @@ func TestPatchEncodeDecodeRoundTrip(t *testing.T) { orig.Viewpoint = []float32{1, 2, 3, 1, 0, 0, 0} patches := []patch{ &labelPatch{indices: []uint32{1, 2, 42}, oldLabels: []uint32{7, 8, 9}}, + &appendPatch{oldPoints: 90, oldWidth: 9, oldHeight: 10}, &replacePatch{header: orig.PointCloudHeader.Clone(), data: orig.Data}, }