diff --git a/editor.go b/editor.go index 792a384..be72fd1 100644 --- a/editor.go +++ b/editor.go @@ -310,18 +310,16 @@ func passThroughImpl(pp *pc.PointCloud, core func(_, _ *pc.PointCloud) int) (*pc } func (e *editor) merge(pp *pc.PointCloud) error { - 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 - - if err := e.push(newUndoDataEntireCloud(e.pp)); err != nil { + if err := e.push(&undoDataSize{ + Points: e.pp.Points, + Width: e.pp.Width, + Height: e.pp.Height, + }); err != nil { return err } - 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() return nil } diff --git a/record.go b/record.go index cf5c040..9ca1c00 100644 --- a/record.go +++ b/record.go @@ -21,6 +21,20 @@ type undoData interface { func init() { gob.Register(&undoDataEntireCloud{}) gob.Register(&undoDataLabels{}) + gob.Register(&undoDataSize{}) +} + +// 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 } var ( @@ -97,6 +111,25 @@ func (p *undoDataLabels) payload() []byte { func (p *undoDataLabels) setPayload([]byte) {} +type undoDataSize struct { + Points, Width, Height int +} + +func (p *undoDataSize) restore(pp *pc.PointCloud) (*pc.PointCloud, error) { + stride := pp.Stride() + if stride <= 0 || p.Points < 0 || p.Width < 0 || p.Height < 0 || + p.Points > pp.Points || p.Points > len(pp.Data)/stride { + return nil, errBrokenRecord + } + return newCloudView(pp, p.Points, p.Width, p.Height, pp.Data[:p.Points*stride]), nil +} + +func (p *undoDataSize) payload() []byte { + return nil +} + +func (p *undoDataSize) setPayload([]byte) {} + // A record is this encoding followed by the raw payload. func encodeUndoData(w io.Writer, d undoData) error { return gob.NewEncoder(w).Encode(&d) diff --git a/record_test.go b/record_test.go index 2f7c1e0..256b0e3 100644 --- a/record_test.go +++ b/record_test.go @@ -74,6 +74,24 @@ func TestUndoDataLabelsRestore(t *testing.T) { assertCloudEqual(t, orig, out) } +func TestUndoDataSizeRestore(t *testing.T) { + orig := makeTestCloud(t, 100, 10, 10) + pp := cloneCloud(orig) + + p := &undoDataSize{Points: pp.Points, Width: pp.Width, Height: 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.restore(pp) + if err != nil { + t.Fatal(err) + } + assertCloudEqual(t, orig, out) +} + func TestUndoDataEntireCloudRestore(t *testing.T) { orig := makeTestCloud(t, 100, 10, 10) pp := makeTestCloud(t, 5, 5, 1) @@ -95,6 +113,7 @@ func TestRecordEncodeDecodeRoundTrip(t *testing.T) { for name, d := range map[string]undoData{ "EntireCloud": newUndoDataEntireCloud(orig), "Labels": &undoDataLabels{Indices: []uint32{1, 2, 42}, OldLabels: []uint32{7, 8, 9}}, + "Size": &undoDataSize{Points: 90, Width: 9, Height: 10}, } { t.Run(name, func(t *testing.T) { var buf bytes.Buffer