1package graphics
2
3import (
4 "fmt"
5 "math/bits"
6 "sort"
7 "sync"
8)
9
10type assetRecord struct {
11 id AssetID
12 encoded []byte
13 format AssetFormat
14 width int64
15 height int64
16 pixels uint64
17}
18
19type placementRecord struct {
20 id PlacementID
21 asset AssetID
22 source PixelRect
23 destination PixelRect
24 cells CellRect
25 layer int64
26 order uint64
27}
28
29type sceneState struct {
30 generation uint64
31 nextAsset uint64
32 nextPlace uint64
33 nextOrder uint64
34 assets map[AssetID]assetRecord
35 placements map[PlacementID]placementRecord
36 usage Usage
37}
38
39// Scene owns a bounded set of immutable assets and sparse placements. Scene
40// methods are safe for concurrent callers; snapshots never share mutable maps
41// with the scene. The scene itself contains no text or renderer state.
42type Scene struct {
43 mu sync.RWMutex
44 limits Limits
45 state *sceneState
46}
47
48// NewScene creates an empty bounded graphics scene. Zero-valued limits use
49// package defaults. Limits are copied and may be safely reused by the caller.
50func NewScene(limits Limits) *Scene {
51 return &Scene{limits: normalizeLimits(limits), state: emptyState()}
52}
53
54// New is a concise constructor alias.
55func New(limits Limits) *Scene { return NewScene(limits) }
56
57// NewSceneWithLimits is a descriptive constructor alias.
58func NewSceneWithLimits(limits Limits) *Scene { return NewScene(limits) }
59
60func emptyState() *sceneState {
61 return &sceneState{
62 nextAsset: 1,
63 nextPlace: 1,
64 nextOrder: 1,
65 assets: make(map[AssetID]assetRecord),
66 placements: make(map[PlacementID]placementRecord),
67 }
68}
69
70func normalizeLimits(limits Limits) Limits {
71 if limits.MaxAssets == 0 {
72 limits.MaxAssets = DefaultMaxAssets
73 }
74 if limits.MaxPlacements == 0 {
75 limits.MaxPlacements = DefaultMaxPlacements
76 }
77 if limits.MaxEncodedBytes == 0 {
78 limits.MaxEncodedBytes = DefaultMaxEncodedBytes
79 }
80 if limits.MaxDecodedPixels == 0 {
81 limits.MaxDecodedPixels = DefaultMaxDecodedPixels
82 }
83 if limits.MaxEncodedBytesPerAsset == 0 {
84 limits.MaxEncodedBytesPerAsset = DefaultMaxEncodedBytesPerAsset
85 }
86 if limits.MaxDecodedPixelsPerAsset == 0 {
87 limits.MaxDecodedPixelsPerAsset = DefaultMaxDecodedPixelsPerAsset
88 }
89 return limits
90}
91
92// Limits returns the immutable limits configured for the scene.
93func (s *Scene) Limits() Limits {
94 if s == nil {
95 return Limits{}
96 }
97 s.mu.RLock()
98 defer s.mu.RUnlock()
99 return s.limits
100}
101
102// AddAsset copies and registers an encoded asset, returning an opaque ID.
103func (s *Scene) AddAsset(blob AssetBlob) (AssetID, error) {
104 if s == nil {
105 return AssetID{}, fmt.Errorf("add asset: %w", ErrInvalidAsset)
106 }
107 s.mu.Lock()
108 defer s.mu.Unlock()
109 candidate := cloneState(s.state)
110 record, err := s.prepareAsset(blob, candidate)
111 if err != nil {
112 return AssetID{}, err
113 }
114 id, ok := takeAssetID(candidate)
115 if !ok {
116 return AssetID{}, fmt.Errorf("add asset: %w", ErrIdentifierOverflow)
117 }
118 record.id = id
119 candidate.assets[id] = record
120 candidate.usage.Assets++
121 candidate.usage.EncodedBytes += uint64(len(record.encoded))
122 candidate.usage.DecodedPixels += record.pixels
123 if err := advanceGeneration(candidate); err != nil {
124 return AssetID{}, err
125 }
126 s.state = candidate
127 return id, nil
128}
129
130// RegisterAsset is an alias for AddAsset.
131func (s *Scene) RegisterAsset(blob AssetBlob) (AssetID, error) { return s.AddAsset(blob) }
132
133// ReplaceAsset atomically replaces an asset while removing all placements that
134// reference it. If validation or resource limits reject the new asset, the
135// existing asset and placements remain unchanged.
136func (s *Scene) ReplaceAsset(id AssetID, blob AssetBlob) (AssetID, error) {
137 if s == nil {
138 return AssetID{}, fmt.Errorf("replace asset: %w", ErrAssetNotFound)
139 }
140 s.mu.Lock()
141 defer s.mu.Unlock()
142 candidate := cloneState(s.state)
143 old, ok := candidate.assets[id]
144 if !ok {
145 return AssetID{}, fmt.Errorf("replace asset: %w", ErrAssetNotFound)
146 }
147 for placementID, placement := range candidate.placements {
148 if placement.asset != id {
149 continue
150 }
151 delete(candidate.placements, placementID)
152 candidate.usage.Placements--
153 }
154 delete(candidate.assets, id)
155 candidate.usage.Assets--
156 candidate.usage.EncodedBytes -= uint64(len(old.encoded))
157 candidate.usage.DecodedPixels -= old.pixels
158 replacement, err := s.prepareAsset(blob, candidate)
159 if err != nil {
160 return AssetID{}, err
161 }
162 replacementID, ok := takeAssetID(candidate)
163 if !ok {
164 return AssetID{}, fmt.Errorf("replace asset: %w", ErrIdentifierOverflow)
165 }
166 replacement.id = replacementID
167 candidate.assets[replacementID] = replacement
168 candidate.usage.Assets++
169 candidate.usage.EncodedBytes += uint64(len(replacement.encoded))
170 candidate.usage.DecodedPixels += replacement.pixels
171 if err := advanceGeneration(candidate); err != nil {
172 return AssetID{}, fmt.Errorf("replace asset: %w", err)
173 }
174 s.state = candidate
175 return replacementID, nil
176}
177
178// AddEncodedAsset registers an asset from its encoded bytes and decoded pixel
179// dimensions. The input bytes are copied before this method returns.
180func (s *Scene) AddEncodedAsset(encoded []byte, width, height int64) (AssetID, error) {
181 return s.AddAsset(AssetBlob{Encoded: encoded, Width: width, Height: height})
182}
183
184// RemoveAsset removes an unreferenced asset. Use RemoveAssetCascade when the
185// asset's placements should be removed as one transaction.
186func (s *Scene) RemoveAsset(id AssetID) error {
187 if s == nil {
188 return fmt.Errorf("remove asset: %w", ErrAssetNotFound)
189 }
190 s.mu.Lock()
191 defer s.mu.Unlock()
192 candidate := cloneState(s.state)
193 if err := removeAsset(candidate, id, false); err != nil {
194 return err
195 }
196 if err := advanceGeneration(candidate); err != nil {
197 return fmt.Errorf("remove asset: %w", err)
198 }
199 s.state = candidate
200 return nil
201}
202
203// RemoveAssetCascade removes an asset and all placements that reference it in
204// one generation.
205func (s *Scene) RemoveAssetCascade(id AssetID) error {
206 if s == nil {
207 return fmt.Errorf("remove asset: %w", ErrAssetNotFound)
208 }
209 s.mu.Lock()
210 defer s.mu.Unlock()
211 candidate := cloneState(s.state)
212 if err := removeAsset(candidate, id, true); err != nil {
213 return err
214 }
215 if err := advanceGeneration(candidate); err != nil {
216 return fmt.Errorf("remove asset: %w", err)
217 }
218 s.state = candidate
219 return nil
220}
221
222// Place adds one sparse placement and returns its opaque ID.
223func (s *Scene) Place(spec PlacementSpec) (PlacementID, error) {
224 if s == nil {
225 return PlacementID{}, fmt.Errorf("place: %w", ErrInvalidPlacement)
226 }
227 s.mu.Lock()
228 defer s.mu.Unlock()
229 candidate := cloneState(s.state)
230 record, err := s.preparePlacement(candidate, spec, nil)
231 if err != nil {
232 return PlacementID{}, err
233 }
234 if uint64(len(candidate.placements)) >= s.limits.MaxPlacements {
235 return PlacementID{}, fmt.Errorf("place: %w", ErrTooManyPlacements)
236 }
237 id, ok := takePlacementID(candidate)
238 if !ok {
239 return PlacementID{}, fmt.Errorf("place: %w", ErrIdentifierOverflow)
240 }
241 record.id = id
242 record.order = takeOrder(candidate)
243 candidate.placements[id] = record
244 candidate.usage.Placements++
245 if err := advanceGeneration(candidate); err != nil {
246 return PlacementID{}, fmt.Errorf("place: %w", err)
247 }
248 s.state = candidate
249 return id, nil
250}
251
252// AddPlacement is an alias for Place.
253func (s *Scene) AddPlacement(spec PlacementSpec) (PlacementID, error) { return s.Place(spec) }
254
255// PlaceAsset creates a full-source placement of an asset.
256func (s *Scene) PlaceAsset(asset AssetID, destination PixelRect) (PlacementID, error) {
257 return s.Place(PlacementSpec{Asset: asset, Destination: destination})
258}
259
260// UpdatePlacement replaces the geometry and optional metadata of a placement.
261// If spec.Asset and spec.AssetID are both invalid, the existing asset is kept.
262func (s *Scene) UpdatePlacement(id PlacementID, spec PlacementSpec) error {
263 if s == nil {
264 return fmt.Errorf("update placement: %w", ErrPlacementNotFound)
265 }
266 s.mu.Lock()
267 defer s.mu.Unlock()
268 candidate := cloneState(s.state)
269 existing, ok := candidate.placements[id]
270 if !ok {
271 return fmt.Errorf("update placement: %w", ErrPlacementNotFound)
272 }
273 record, err := s.preparePlacement(candidate, spec, &existing)
274 if err != nil {
275 return err
276 }
277 record.id = id
278 record.order = existing.order
279 candidate.placements[id] = record
280 if err := advanceGeneration(candidate); err != nil {
281 return fmt.Errorf("update placement: %w", err)
282 }
283 s.state = candidate
284 return nil
285}
286
287// MovePlacement changes only the destination rectangle of a placement.
288func (s *Scene) MovePlacement(id PlacementID, destination PixelRect) error {
289 return s.UpdatePlacement(id, PlacementSpec{Destination: destination})
290}
291
292// RemovePlacement removes one sparse placement.
293func (s *Scene) RemovePlacement(id PlacementID) error {
294 if s == nil {
295 return fmt.Errorf("remove placement: %w", ErrPlacementNotFound)
296 }
297 s.mu.Lock()
298 defer s.mu.Unlock()
299 candidate := cloneState(s.state)
300 if _, ok := candidate.placements[id]; !ok {
301 return fmt.Errorf("remove placement: %w", ErrPlacementNotFound)
302 }
303 delete(candidate.placements, id)
304 candidate.usage.Placements--
305 if err := advanceGeneration(candidate); err != nil {
306 return fmt.Errorf("remove placement: %w", err)
307 }
308 s.state = candidate
309 return nil
310}
311
312// Clear removes every asset and placement while preserving the monotonic ID
313// counters. It publishes a new generation only when the scene is non-empty.
314func (s *Scene) Clear() error {
315 if s == nil {
316 return nil
317 }
318 s.mu.Lock()
319 defer s.mu.Unlock()
320 if len(s.state.assets) == 0 && len(s.state.placements) == 0 {
321 return nil
322 }
323 candidate := cloneState(s.state)
324 candidate.assets = make(map[AssetID]assetRecord)
325 candidate.placements = make(map[PlacementID]placementRecord)
326 candidate.usage = Usage{}
327 if err := advanceGeneration(candidate); err != nil {
328 return fmt.Errorf("clear scene: %w", err)
329 }
330 s.state = candidate
331 return nil
332}
333
334// Apply commits sparse operations transactionally. If any operation fails,
335// neither the state nor the generation changes.
336func (s *Scene) Apply(operations ...Operation) error {
337 if s == nil {
338 return fmt.Errorf("apply operations: %w", ErrInvalidOperation)
339 }
340 if len(operations) == 0 {
341 return nil
342 }
343 s.mu.Lock()
344 defer s.mu.Unlock()
345 candidate := cloneState(s.state)
346 for i, operation := range operations {
347 if err := s.applyOperation(candidate, operation); err != nil {
348 return fmt.Errorf("apply operation %d: %w", i, err)
349 }
350 }
351 if err := advanceGeneration(candidate); err != nil {
352 return fmt.Errorf("apply operations: %w", err)
353 }
354 s.state = candidate
355 return nil
356}
357
358// ApplyOperations is the slice-taking form of Apply.
359func (s *Scene) ApplyOperations(operations []Operation) error { return s.Apply(operations...) }
360
361// Snapshot returns an immutable reference to the current scene state. Future
362// scene mutations use copy-on-write maps and cannot alter this snapshot.
363func (s *Scene) Snapshot() *Snapshot {
364 if s == nil {
365 return &Snapshot{state: emptyState()}
366 }
367 s.mu.RLock()
368 defer s.mu.RUnlock()
369 return &Snapshot{state: s.state}
370}
371
372// Generation returns the current scene generation.
373func (s *Scene) Generation() Generation {
374 if s == nil {
375 return 0
376 }
377 s.mu.RLock()
378 defer s.mu.RUnlock()
379 return s.state.generation
380}
381
382// Usage returns the current live resource usage.
383func (s *Scene) Usage() Usage {
384 if s == nil {
385 return Usage{}
386 }
387 s.mu.RLock()
388 defer s.mu.RUnlock()
389 return s.state.usage
390}
391
392func (s *Scene) prepareAsset(blob AssetBlob, state *sceneState) (assetRecord, error) {
393 data := blob.Encoded
394 if blob.Width <= 0 || blob.Height <= 0 {
395 return assetRecord{}, fmt.Errorf("add asset: %w", ErrInvalidAsset)
396 }
397 pixels, ok := decodedPixels(blob.Width, blob.Height, blob.DecodedPixels)
398 if !ok {
399 return assetRecord{}, fmt.Errorf("add asset: %w", ErrInvalidAsset)
400 }
401 encoded := uint64(len(data))
402 if encoded > s.limits.MaxEncodedBytesPerAsset {
403 return assetRecord{}, fmt.Errorf("add asset: %w", ErrEncodedBudget)
404 }
405 if pixels > s.limits.MaxDecodedPixelsPerAsset {
406 return assetRecord{}, fmt.Errorf("add asset: %w", ErrDecodedPixelBudget)
407 }
408 if uint64(len(state.assets)) >= s.limits.MaxAssets {
409 return assetRecord{}, fmt.Errorf("add asset: %w", ErrTooManyAssets)
410 }
411 if !within(state.usage.EncodedBytes, encoded, s.limits.MaxEncodedBytes) {
412 return assetRecord{}, fmt.Errorf("add asset: %w", ErrEncodedBudget)
413 }
414 if !within(state.usage.DecodedPixels, pixels, s.limits.MaxDecodedPixels) {
415 return assetRecord{}, fmt.Errorf("add asset: %w", ErrDecodedPixelBudget)
416 }
417 return assetRecord{encoded: append([]byte(nil), data...), format: blob.Format, width: blob.Width, height: blob.Height, pixels: pixels}, nil
418}
419
420func (s *Scene) preparePlacement(state *sceneState, spec PlacementSpec, existing *placementRecord) (placementRecord, error) {
421 asset := spec.Asset
422 if !asset.Valid() && existing != nil {
423 asset = existing.asset
424 }
425 recordAsset, ok := state.assets[asset]
426 if !ok {
427 return placementRecord{}, fmt.Errorf("place: %w", ErrAssetNotFound)
428 }
429 destination := spec.Destination
430 if existing != nil && destination == (PixelRect{}) {
431 destination = existing.destination
432 }
433 if !destination.Valid() || destination.Empty() {
434 return placementRecord{}, fmt.Errorf("place: %w", ErrInvalidPlacement)
435 }
436 source := spec.Source
437 if source == (PixelRect{}) {
438 if existing != nil && spec.Source == (PixelRect{}) {
439 source = existing.source
440 } else {
441 source = PixelRect{Width: recordAsset.width, Height: recordAsset.height}
442 }
443 }
444 if !source.Valid() || source.Empty() {
445 return placementRecord{}, fmt.Errorf("place: %w", ErrInvalidPlacement)
446 }
447 assetBounds := PixelRect{Width: recordAsset.width, Height: recordAsset.height}
448 if _, ok := assetBounds.Intersect(source); !ok || source != assetBounds && !containsRect(assetBounds, source) {
449 return placementRecord{}, fmt.Errorf("place: %w", ErrInvalidPlacement)
450 }
451 cells := spec.Cells
452 if existing != nil && !spec.HasCells && cells == (CellRect{}) {
453 cells = existing.cells
454 }
455 if cells != (CellRect{}) && (!cells.Valid() || cells.Empty()) {
456 return placementRecord{}, fmt.Errorf("place: %w", ErrInvalidPlacement)
457 }
458 layer := spec.Layer
459 if existing != nil && !spec.HasLayer {
460 layer = existing.layer
461 }
462 return placementRecord{asset: asset, source: source, destination: destination, cells: cells, layer: layer}, nil
463}
464
465func containsRect(outer, inner PixelRect) bool {
466 right, okRight := outer.Right()
467 innerRight, okInnerRight := inner.Right()
468 bottom, okBottom := outer.Bottom()
469 innerBottom, okInnerBottom := inner.Bottom()
470 return okRight && okInnerRight && okBottom && okInnerBottom && inner.X >= outer.X && innerRight <= right && inner.Y >= outer.Y && innerBottom <= bottom
471}
472
473func removeAsset(state *sceneState, id AssetID, cascade bool) error {
474 record, ok := state.assets[id]
475 if !ok {
476 return fmt.Errorf("remove asset: %w", ErrAssetNotFound)
477 }
478 for placementID, placement := range state.placements {
479 if placement.asset != id {
480 continue
481 }
482 if !cascade {
483 return fmt.Errorf("remove asset %s: %w", id, ErrAssetInUse)
484 }
485 delete(state.placements, placementID)
486 state.usage.Placements--
487 }
488 delete(state.assets, id)
489 state.usage.Assets--
490 state.usage.EncodedBytes -= uint64(len(record.encoded))
491 state.usage.DecodedPixels -= record.pixels
492 return nil
493}
494
495func (s *Scene) applyOperation(state *sceneState, operation Operation) error {
496 switch operation.Kind {
497 case OperationAddAsset:
498 record, err := s.prepareAsset(operation.Blob, state)
499 if err != nil {
500 return err
501 }
502 id, ok := takeAssetID(state)
503 if !ok {
504 return fmt.Errorf("add asset: %w", ErrIdentifierOverflow)
505 }
506 record.id = id
507 state.assets[id] = record
508 state.usage.Assets++
509 state.usage.EncodedBytes += uint64(len(record.encoded))
510 state.usage.DecodedPixels += record.pixels
511 case OperationRemoveAsset:
512 if err := removeAsset(state, operation.Asset, false); err != nil {
513 return err
514 }
515 case OperationPlace:
516 record, err := s.preparePlacement(state, operation.Placement, nil)
517 if err != nil {
518 return err
519 }
520 if uint64(len(state.placements)) >= s.limits.MaxPlacements {
521 return fmt.Errorf("place: %w", ErrTooManyPlacements)
522 }
523 id, ok := takePlacementID(state)
524 if !ok {
525 return fmt.Errorf("place: %w", ErrIdentifierOverflow)
526 }
527 record.id = id
528 record.order = takeOrder(state)
529 state.placements[id] = record
530 state.usage.Placements++
531 case OperationUpdatePlacement:
532 existing, ok := state.placements[operation.PlacementID]
533 if !ok {
534 return fmt.Errorf("update placement: %w", ErrPlacementNotFound)
535 }
536 record, err := s.preparePlacement(state, operation.Placement, &existing)
537 if err != nil {
538 return err
539 }
540 record.id = existing.id
541 record.order = existing.order
542 state.placements[existing.id] = record
543 case OperationRemovePlacement:
544 if _, ok := state.placements[operation.PlacementID]; !ok {
545 return fmt.Errorf("remove placement: %w", ErrPlacementNotFound)
546 }
547 delete(state.placements, operation.PlacementID)
548 state.usage.Placements--
549 default:
550 return fmt.Errorf("operation kind %d: %w", operation.Kind, ErrInvalidOperation)
551 }
552 return nil
553}
554
555func cloneState(state *sceneState) *sceneState {
556 if state == nil {
557 return emptyState()
558 }
559 clone := *state
560 clone.assets = make(map[AssetID]assetRecord, len(state.assets))
561 for id, record := range state.assets {
562 clone.assets[id] = record
563 }
564 clone.placements = make(map[PlacementID]placementRecord, len(state.placements))
565 for id, record := range state.placements {
566 clone.placements[id] = record
567 }
568 return &clone
569}
570
571func advanceGeneration(state *sceneState) error {
572 if state.generation == ^uint64(0) {
573 return ErrGenerationOverflow
574 }
575 state.generation++
576 return nil
577}
578
579func takeAssetID(state *sceneState) (AssetID, bool) {
580 if state.nextAsset == 0 {
581 return AssetID{}, false
582 }
583 id := AssetID{value: state.nextAsset}
584 if state.nextAsset == ^uint64(0) {
585 state.nextAsset = 0
586 } else {
587 state.nextAsset++
588 }
589 return id, true
590}
591
592func takePlacementID(state *sceneState) (PlacementID, bool) {
593 if state.nextPlace == 0 {
594 return PlacementID{}, false
595 }
596 id := PlacementID{value: state.nextPlace}
597 if state.nextPlace == ^uint64(0) {
598 state.nextPlace = 0
599 } else {
600 state.nextPlace++
601 }
602 return id, true
603}
604
605func takeOrder(state *sceneState) uint64 {
606 order := state.nextOrder
607 if state.nextOrder != ^uint64(0) {
608 state.nextOrder++
609 } else {
610 state.nextOrder = 0
611 }
612 return order
613}
614
615func within(current, additional, limit uint64) bool {
616 if current > limit {
617 return false
618 }
619 return additional <= limit-current
620}
621
622func decodedPixels(width, height int64, declared uint64) (uint64, bool) {
623 if width <= 0 || height <= 0 {
624 return 0, false
625 }
626 w, h := uint64(width), uint64(height)
627 if h != 0 && w > ^uint64(0)/h {
628 return 0, false
629 }
630 computed := w * h
631 // DecodedPixels is retained as an optional compatibility field, but it is
632 // only a valid declaration when it agrees with the dimensions. Resource
633 // accounting must never trust a caller-supplied number that can undercount
634 // the pixels represented by the asset.
635 if declared != 0 && declared != computed {
636 return 0, false
637 }
638 return computed, true
639}
640
641// AssetView is an immutable view of a registered asset. Encoded returns a new
642// byte slice on every call; no caller can mutate scene or snapshot storage.
643type AssetView struct {
644 id AssetID
645 encoded []byte
646 format AssetFormat
647 width int64
648 height int64
649 pixels uint64
650}
651
652// Asset is an alias for the immutable snapshot view.
653type Asset = AssetView
654
655func (a AssetView) ID() AssetID { return a.id }
656func (a AssetView) Format() AssetFormat { return a.format }
657func (a AssetView) Width() int64 { return a.width }
658func (a AssetView) Height() int64 { return a.height }
659func (a AssetView) DecodedPixels() uint64 { return a.pixels }
660func (a AssetView) EncodedSize() uint64 { return uint64(len(a.encoded)) }
661func (a AssetView) Encoded() []byte { return append([]byte(nil), a.encoded...) }
662func (a AssetView) Bytes() []byte { return a.Encoded() }
663func (a AssetView) Blob() AssetBlob {
664 return AssetBlob{Encoded: a.Encoded(), Format: a.format, Width: a.width, Height: a.height, DecodedPixels: a.pixels}
665}
666
667// PlacementView is an immutable view of a sparse placement.
668type PlacementView struct {
669 id PlacementID
670 asset AssetID
671 source PixelRect
672 destination PixelRect
673 cells CellRect
674 layer int64
675}
676
677// Placement is an alias for the immutable snapshot view.
678type Placement = PlacementView
679
680func (p PlacementView) ID() PlacementID { return p.id }
681func (p PlacementView) AssetID() AssetID { return p.asset }
682func (p PlacementView) Source() PixelRect { return p.source }
683func (p PlacementView) Destination() PixelRect { return p.destination }
684func (p PlacementView) Cells() CellRect { return p.cells }
685func (p PlacementView) Layer() int64 { return p.layer }
686func (p PlacementView) Spec() PlacementSpec {
687 return PlacementSpec{
688 Asset: p.asset,
689 Source: p.source,
690 Destination: p.destination,
691 Cells: p.cells,
692 HasCells: p.cells != (CellRect{}),
693 Layer: p.layer,
694 HasLayer: true,
695 }
696}
697
698// Snapshot is an immutable reference to a scene state.
699type Snapshot struct{ state *sceneState }
700
701// Generation is a monotonically increasing scene generation.
702func (s *Snapshot) Generation() uint64 {
703 if s == nil || s.state == nil {
704 return 0
705 }
706 return s.state.generation
707}
708
709// Usage reports the resources retained by this snapshot's state.
710func (s *Snapshot) Usage() Usage {
711 if s == nil || s.state == nil {
712 return Usage{}
713 }
714 return s.state.usage
715}
716
717// Asset returns an immutable asset view by ID.
718func (s *Snapshot) Asset(id AssetID) (AssetView, bool) {
719 if s == nil || s.state == nil {
720 return AssetView{}, false
721 }
722 record, ok := s.state.assets[id]
723 if !ok {
724 return AssetView{}, false
725 }
726 return AssetView(record), true
727}
728
729// GetAsset is an alias for Asset.
730func (s *Snapshot) GetAsset(id AssetID) (AssetView, bool) { return s.Asset(id) }
731
732// Assets returns immutable asset views in ID order.
733func (s *Snapshot) Assets() []AssetView {
734 if s == nil || s.state == nil || len(s.state.assets) == 0 {
735 return nil
736 }
737 ids := make([]AssetID, 0, len(s.state.assets))
738 for id := range s.state.assets {
739 ids = append(ids, id)
740 }
741 sort.Slice(ids, func(i, j int) bool { return ids[i].value < ids[j].value })
742 assets := make([]AssetView, 0, len(ids))
743 for _, id := range ids {
744 asset, _ := s.Asset(id)
745 assets = append(assets, asset)
746 }
747 return assets
748}
749
750// Placement returns an immutable placement view by ID.
751func (s *Snapshot) Placement(id PlacementID) (PlacementView, bool) {
752 if s == nil || s.state == nil {
753 return PlacementView{}, false
754 }
755 record, ok := s.state.placements[id]
756 if !ok {
757 return PlacementView{}, false
758 }
759 return PlacementView{id: record.id, asset: record.asset, source: record.source, destination: record.destination, cells: record.cells, layer: record.layer}, true
760}
761
762// GetPlacement is an alias for Placement.
763func (s *Snapshot) GetPlacement(id PlacementID) (PlacementView, bool) { return s.Placement(id) }
764
765// Placements returns immutable placement views in layer and insertion order.
766func (s *Snapshot) Placements() []PlacementView {
767 if s == nil || s.state == nil || len(s.state.placements) == 0 {
768 return nil
769 }
770 placements := make([]placementRecord, 0, len(s.state.placements))
771 for _, placement := range s.state.placements {
772 placements = append(placements, placement)
773 }
774 sort.SliceStable(placements, func(i, j int) bool {
775 if placements[i].layer != placements[j].layer {
776 return placements[i].layer < placements[j].layer
777 }
778 return placements[i].order < placements[j].order
779 })
780 views := make([]PlacementView, 0, len(placements))
781 for _, placement := range placements {
782 views = append(views, PlacementView{id: placement.id, asset: placement.asset, source: placement.source, destination: placement.destination, cells: placement.cells, layer: placement.layer})
783 }
784 return views
785}
786
787// VisibleFragments returns clipped placement fragments. viewport may be a
788// PixelRect, CellRect, or Viewport. Unsupported or invalid viewport values
789// produce no fragments rather than wrapping coordinates.
790func (s *Snapshot) VisibleFragments(viewport any) []VisibleFragment {
791 if s == nil || s.state == nil {
792 return nil
793 }
794 pixels, cells, hasPixels, hasCells := viewportParts(viewport)
795 if !hasPixels && !hasCells {
796 return nil
797 }
798 placements := s.Placements()
799 fragments := make([]VisibleFragment, 0, len(placements))
800 for _, placement := range placements {
801 visible, ok := visiblePlacement(placement, pixels, cells, hasPixels, hasCells)
802 if ok {
803 fragments = append(fragments, visible)
804 }
805 }
806 return fragments
807}
808
809// VisiblePixelFragments is the typed pixel-space form of VisibleFragments.
810func (s *Snapshot) VisiblePixelFragments(viewport PixelRect) []VisibleFragment {
811 return s.VisibleFragments(viewport)
812}
813
814// VisibleCellFragments is the typed cell-space form of VisibleFragments.
815func (s *Snapshot) VisibleCellFragments(viewport CellRect) []VisibleFragment {
816 return s.VisibleFragments(viewport)
817}
818
819func viewportParts(viewport any) (PixelRect, CellRect, bool, bool) {
820 switch value := viewport.(type) {
821 case PixelRect:
822 return value, CellRect{}, value.Valid() && !value.Empty(), false
823 case CellRect:
824 return PixelRect{}, value, false, value.Valid() && !value.Empty()
825 case Viewport:
826 hasPixels := value.Pixels != (PixelRect{})
827 hasCells := value.Cells != (CellRect{})
828 if hasPixels && (!value.Pixels.Valid() || value.Pixels.Empty()) {
829 return PixelRect{}, CellRect{}, false, false
830 }
831 if hasCells && (!value.Cells.Valid() || value.Cells.Empty()) {
832 return PixelRect{}, CellRect{}, false, false
833 }
834 return value.Pixels, value.Cells, hasPixels, hasCells
835 case *Viewport:
836 if value == nil {
837 return PixelRect{}, CellRect{}, false, false
838 }
839 return viewportParts(*value)
840 default:
841 return PixelRect{}, CellRect{}, false, false
842 }
843}
844
845func visiblePlacement(placement PlacementView, pixels PixelRect, cells CellRect, hasPixels, hasCells bool) (VisibleFragment, bool) {
846 destination := placement.destination
847 if hasPixels {
848 var ok bool
849 destination, ok = destination.Intersect(pixels)
850 if !ok {
851 return VisibleFragment{}, false
852 }
853 }
854 var visibleCells CellRect
855 if placement.cells.Valid() && !placement.cells.Empty() {
856 visibleCells = placement.cells
857 if hasCells {
858 var ok bool
859 visibleCells, ok = visibleCells.Intersect(cells)
860 if !ok {
861 return VisibleFragment{}, false
862 }
863 cellPixels, ok := mapSubRectCellToPixel(placement.cells, placement.destination, visibleCells)
864 if !ok {
865 return VisibleFragment{}, false
866 }
867 var pixelOK bool
868 destination, pixelOK = destination.Intersect(cellPixels)
869 if !pixelOK {
870 return VisibleFragment{}, false
871 }
872 }
873 if hasPixels {
874 var ok bool
875 visibleCells, ok = mapSubRectPixelToCell(placement.destination, placement.cells, destination)
876 if !ok {
877 return VisibleFragment{}, false
878 }
879 if hasCells {
880 visibleCells, ok = visibleCells.Intersect(cells)
881 if !ok {
882 return VisibleFragment{}, false
883 }
884 }
885 }
886 } else if hasCells {
887 return VisibleFragment{}, false
888 }
889 source, ok := mapSubRect(placement.destination, placement.source, destination)
890 if !ok {
891 return VisibleFragment{}, false
892 }
893 return VisibleFragment{PlacementID: placement.id, AssetID: placement.asset, Source: source, Destination: destination, Cells: visibleCells}, true
894}
895
896// mapSubRect maps a non-empty sub-rectangle from one rectangle's coordinate
897// space into another rectangle's coordinate space. Its range mapping uses
898// exact 128-bit arithmetic so products cannot wrap before division.
899func mapSubRect(from, to, sub PixelRect) (PixelRect, bool) {
900 if !from.Valid() || !to.Valid() || !sub.Valid() || from.Empty() || to.Empty() || sub.Empty() || !containsRect(from, sub) {
901 return PixelRect{}, false
902 }
903 x, okX := mapRange(from.X, from.Width, to.X, to.Width, sub.X, mustRight(sub.X, sub.Width))
904 y, okY := mapRange(from.Y, from.Height, to.Y, to.Height, sub.Y, mustRight(sub.Y, sub.Height))
905 if !okX || !okY || x[1] <= x[0] || y[1] <= y[0] {
906 return PixelRect{}, false
907 }
908 return PixelRect{X: x[0], Y: y[0], Width: x[1] - x[0], Height: y[1] - y[0]}, true
909}
910
911func mapSubRectCellToPixel(from CellRect, to PixelRect, sub CellRect) (PixelRect, bool) {
912 if !from.Valid() || !to.Valid() || !sub.Valid() || from.Empty() || to.Empty() || sub.Empty() || !containsCellRect(from, sub) {
913 return PixelRect{}, false
914 }
915 x, okX := mapRange(from.X, from.Width, to.X, to.Width, sub.X, mustRight(sub.X, sub.Width))
916 y, okY := mapRange(from.Y, from.Height, to.Y, to.Height, sub.Y, mustRight(sub.Y, sub.Height))
917 if !okX || !okY || x[1] <= x[0] || y[1] <= y[0] {
918 return PixelRect{}, false
919 }
920 return PixelRect{X: x[0], Y: y[0], Width: x[1] - x[0], Height: y[1] - y[0]}, true
921}
922
923func mapSubRectPixelToCell(from PixelRect, to CellRect, sub PixelRect) (CellRect, bool) {
924 if !from.Valid() || !to.Valid() || !sub.Valid() || from.Empty() || to.Empty() || sub.Empty() || !containsRect(from, sub) {
925 return CellRect{}, false
926 }
927 x, okX := mapRange(from.X, from.Width, to.X, to.Width, sub.X, mustRight(sub.X, sub.Width))
928 y, okY := mapRange(from.Y, from.Height, to.Y, to.Height, sub.Y, mustRight(sub.Y, sub.Height))
929 if !okX || !okY || x[1] <= x[0] || y[1] <= y[0] {
930 return CellRect{}, false
931 }
932 return CellRect{X: x[0], Y: y[0], Width: x[1] - x[0], Height: y[1] - y[0]}, true
933}
934
935func containsCellRect(outer, inner CellRect) bool {
936 right, okRight := outer.Right()
937 innerRight, okInnerRight := inner.Right()
938 bottom, okBottom := outer.Bottom()
939 innerBottom, okInnerBottom := inner.Bottom()
940 return okRight && okInnerRight && okBottom && okInnerBottom && inner.X >= outer.X && innerRight <= right && inner.Y >= outer.Y && innerBottom <= bottom
941}
942
943func mustRight(start, width int64) int64 {
944 right, _ := checkedEdge(start, width)
945 return right
946}
947
948func mapRange(fromStart, fromSize, toStart, toSize, clipStart, clipEnd int64) ([2]int64, bool) {
949 if fromSize <= 0 || toSize <= 0 || clipStart < fromStart || clipEnd < clipStart {
950 return [2]int64{}, false
951 }
952 fromEnd, ok := checkedEdge(fromStart, fromSize)
953 if !ok || clipEnd > fromEnd {
954 return [2]int64{}, false
955 }
956 startOffset := clipStart - fromStart
957 endOffset := clipEnd - fromStart
958 startDelta, ok := ratio(startOffset, toSize, fromSize, false)
959 if !ok {
960 return [2]int64{}, false
961 }
962 endDelta, ok := ratio(endOffset, toSize, fromSize, true)
963 if !ok {
964 return [2]int64{}, false
965 }
966 start, ok := checkedAdd(toStart, startDelta)
967 if !ok {
968 return [2]int64{}, false
969 }
970 end, ok := checkedAdd(toStart, endDelta)
971 if !ok || end <= start {
972 return [2]int64{}, false
973 }
974 return [2]int64{start, end}, true
975}
976
977func ratio(offset, numerator, denominator int64, ceil bool) (int64, bool) {
978 if offset < 0 || numerator <= 0 || denominator <= 0 {
979 return 0, false
980 }
981 hi, lo := bits.Mul64(uint64(offset), uint64(numerator))
982 divisor := uint64(denominator)
983 // bits.Div64 requires hi < divisor. Otherwise the quotient cannot fit in
984 // uint64 and consequently cannot fit in the int64 result type.
985 if hi >= divisor {
986 return 0, false
987 }
988 quotient, remainder := bits.Div64(hi, lo, divisor)
989 if ceil && remainder != 0 {
990 if quotient == ^uint64(0) {
991 return 0, false
992 }
993 quotient++
994 }
995 if quotient > uint64(maxInt64) {
996 return 0, false
997 }
998 return int64(quotient), true
999}
1000
1001func checkedAdd(a, b int64) (int64, bool) {
1002 if b > 0 && a > maxInt64-b {
1003 return 0, false
1004 }
1005 if b < 0 && a < minInt64-b {
1006 return 0, false
1007 }
1008 return a + b, true
1009}