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
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:
branches: [main]
pull_request:
branches: [main, initiative/repository-modernisation]
branches: [main, remove-future, initiative/repository-modernisation]

permissions:
contents: read
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:
branches: [main]
pull_request:
branches: [main, initiative/repository-modernisation]
branches: [main, remove-future, initiative/repository-modernisation]

permissions:
contents: read
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:
branches: [main]
pull_request:
branches: [main, initiative/repository-modernisation]
branches: [main, remove-future, initiative/repository-modernisation]

permissions:
contents: read
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: verify pull request
on:
pull_request:
types: [assigned, opened, synchronize, reopened, labeled, unlabeled]
branches: [main, initiative/repository-modernisation]
branches: [main, remove-future, initiative/repository-modernisation]

permissions:
contents: read
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

* Added durable, reference-only scene objects with declarative persistent-state
fields, explicit data rebinding, inspectable unresolved references, and
hierarchy-only scene nodes.
* Added stable semantic scene objects for shapes, surfaces, frames, planes, and
vectors, with backend-neutral discretization and display state.
* Added MRO-composed editable scene-state fields, narrowly scoped non-state
declarations, and data-only mutation callbacks for contextual object editors.
* Added a verified Linux Python 3.12 candidate wheel artifact for framework-wide pre-release integration.
* Added the public `compas.typing` structural vocabulary with NumPy-aware `PointLike`, `VectorLike`, collection, and neutral `VerticesFaces` aliases.
* Added the PEP 561 `py.typed` marker and installed-wheel static-consumer verification.
Expand All @@ -23,6 +30,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

* Made scene objects stable backend-neutral data records and replaced class
substitution with deterministic context- and MRO-keyed `draw` and `clear`
operations. Runtime resources remain outside durable scene objects.
* Migrated core geometry constructors and plugin contracts from private overlapping type aliases to semantic structural inputs, including copied NumPy coordinates and integral face indices.
* Added a committed uv development lock, documented direct quality commands while retaining Invoke convenience tasks, and migrated CI, release, artifact, and documentation mechanics to immutably pinned shared actions.
* Standardized numerical triangle-mesh plugin inputs and outputs on structural `(vertices, triangles)` buffers. `Tessellation` remains an ephemeral discrete representation for geometry producers and scene handoff, but is not required at numerical backend boundaries.
Expand Down
169 changes: 154 additions & 15 deletions docs/getting-started/basics.visualisation.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,48 @@ COMPAS (data) objects can be visualised by placing them into a "scene".
>>> scene.draw()
```

When a COMPAS object is added, the scene creates the corresponding scene object
for the active visualisation context. Visualisation backends are provided by
separate COMPAS packages.
When a COMPAS object is added, the scene creates a stable, backend-neutral scene
object for its semantic type. Visualisation backends are provided by separate
COMPAS packages and register operations for these stable objects.

## Scene Objects

When a COMPAS object is added to a scene, a scene object is created automatically, corresponding to the type of COMPAS object.

```pycon
>>> sceneobj = scene.add(box)
>>> sceneobj
BoxObject
>>> type(sceneobj).__name__
'ShapeObject'
```

The scene object holds a reference to the COMPAS data object, and stores its visualisation settings and transformation matrix.
Multiple scene objects can be created for the same COMPAS data object, each with different visualisation settings and transformations.

Core provides stable semantic scene-object types for data whose visualisation
needs additional backend-neutral settings:

- `ShapeObject` and `SurfaceObject` store the `u` and `v` discretization
counts used to display analytic shapes and parametric surfaces.
- `FrameObject` and `PlaneObject` store their display `size`.
- `VectorObject` stores the vector's display `anchor`.
- `MeshObject` stores `hide_coplanaredges` and `use_vertexcolors` in addition
to its vertex, edge, and face settings.

These settings describe durable presentation intent. A visualisation backend
decides how to draw them; changing the backend does not change the scene-object
type or its stored settings.

For a `FrameObject`, the inherited `geometry` property is the represented
[`Frame`][compas.geometry.Frame]. Its inherited `frame` property has the same
meaning as on every `SceneObject`: it is the world-placement frame derived from
the scene hierarchy. The two frames therefore serve different purposes.

```pycon
>>> sceneobj1 = scene.add(box)
>>> sceneobj2 = scene.add(box)
>>> sceneobj1 is sceneobj2
False
>>> sceneobj1.data is sceneobj2.data
>>> sceneobj1.item is sceneobj2.item
True
```

Expand All @@ -45,7 +64,7 @@ All scene objects have a color attribute.
```pycon
>>> sceneobj = scene.add(box)
>>> sceneobj.color
Color(0.0, 0.0, 0.0, alpha=1.0)
Color(1.0, 1.0, 1.0, alpha=1.0)
```

Color attributes can be set using a COMPAS Color object, or a tuple of RGB values, with the color components specified as floats in the range 0.0-1.0 or as integers in the range 0-255.
Expand Down Expand Up @@ -73,12 +92,106 @@ For example, meshes can have different colors for the vertices, the edges, and t
And the colors of vertices, edges, and faces can be specified individually, per element.
See the section about mesh visualisation for more information.

### Editable Attributes

Stable scene-object classes declare which of their attributes may be exposed by
an object editor. The resolved mapping contains only attribute names and
optional data-side update callbacks. Visualisation packages decide how to
construct controls and refresh their views.

```pycon
>>> attributes = type(sceneobj).editable_attributes()
>>> "show_surfaces" in attributes
True
>>> sceneobj.update_editable_attribute("opacity", 0.5)
>>> sceneobj.opacity
0.5
```

The declarations compose through the scene-object class hierarchy. A `None`
callback uses ordinary attribute assignment. A declaration may instead provide
an atomic, synchronous data-update callback when assignment alone is not
sufficient. Update callbacks never draw, rebuild render data, or refresh a user
interface.

Durable editable attributes are declared once as scene state:

```python
from compas.scene import SceneObject
from compas.scene import SceneStateField


class CustomObject(SceneObject):
__scene_state_namespace__ = "example.CustomObject"
__scene_state_fields__ = (SceneStateField("quality", int, required=False, editable=True),)
```

The separate `__editable_attributes__` mapping is reserved for editable
attributes that are not durable scene state. Derived callback overrides retain
the inherited attribute position, while new attributes append in declaration
order. Use
[`SceneObject.update_editable_attribute`][compas.scene.SceneObject.update_editable_attribute]
to apply the default assignment or the declared data-update callback.

Editability does not add presentation metadata to
[`SceneStateField`][compas.scene.SceneStateField] and is not serialized. A
durable scene-state field is not editable unless it explicitly sets
`editable=True`.

### Data, Scene State, and Extensions

Every scene object is a [`Data`][compas.data.Data] object. COMPAS serialization
wraps it in an envelope containing its data type, GUID, optional name, and the
payload returned by `SceneObject.__data__`. The represented geometry or
datastructure is another independent `Data` object; the scene object stores
only its GUID in the `item` field.

The scene-object payload has two state layers:

- Common state such as `transformation`, `color`, `opacity`, and `show` is
stored directly in the payload.
- Type-specific state declared by `SceneStateField` is stored under the
declaring class's namespace in the `extensions` mapping.

For example, a shape scene object may serialize geometry display settings and
shape resolution separately:

```python
from compas.colors import Color


payload = {
"item": "<represented-data-guid>",
"transformation": None,
"color": Color.white(),
"opacity": 1.0,
"show": True,
"extensions": {
"compas.scene.GeometryObject": {
"show_points": False,
"show_lines": True,
},
"compas.scene.ShapeObject": {
"u": 16,
"v": 16,
},
},
}
```

Namespaces let subclasses and external packages add durable state without
widening the common scene-object payload. Unknown namespaced values are
preserved during a round trip for forward compatibility. Runtime item
bindings, editable-attribute callbacks, Viewer controls, selection, and render
resources are not stored in `extensions` or elsewhere in the serialized scene.

## Scene Hierarchy

Scene objects are organised in a hierarchy under a private root group.
The scene stores data and scene objects by GUID, while a COMPAS tree of
lightweight internal nodes records the hierarchy. Public hierarchy properties
on scene objects resolve these internal references.
Scene objects are organised in a hierarchy under an ordinary root group. A
[`SceneNode`][compas.scene.SceneNode] contains only an object GUID and child
nodes. Presentation state and the referenced data GUID belong to the stable
[`SceneObject`][compas.scene.SceneObject]. Each scene object has exactly one
node in a scene.

```pycon
>>> scene = Scene()
Expand All @@ -96,15 +209,39 @@ True
True
```

## Saving Scenes and Data

A serialized scene contains its hierarchy, presentation state, and references
to data objects by GUID. It does not embed or own those data objects. Serialize
the data separately when it should outlive the current Python process.

After loading a scene, its objects and hierarchy can be inspected immediately.
Bind the independently loaded data explicitly before accessing an object's
`item` or drawing it.

```pycon
>>> description = scene.to_jsonstring()
>>> loaded_scene = Scene.from_jsonstring(description)
>>> loaded_scene.find_by_name(sceneobj.name).item_guid == box.guid
True
>>> loaded_scene.bind(box)
>>> loaded_scene.find_by_name(sceneobj.name).item is box
True
```

Use [`Scene.bind_many`][compas.scene.Scene.bind_many] when restoring several
referenced data objects at once. Bindings are runtime references: binding does
not copy the object or add it to the serialized scene description.

To use a different scene object as the parent, pass it to `Scene.add`.

```pycon
>>> from compas.geometry import Point
>>> point = Point(1, 2, 3)
>>> pointobj = scene.add(point)
>>> boxobj = scene.add(box, parent=pointobj)
>>> boxobj.parent
PointObject
>>> type(boxobj.parent).__name__
'GeometryObject'
```

## Object Frame And Transformation
Expand Down Expand Up @@ -138,8 +275,10 @@ True

## Scene Context

Visualisation packages register their own scene contexts and scene object
implementations. Users can select a registered context explicitly:
Visualisation packages register `draw` and `clear` operations for stable scene
object types. Selecting another context changes operation lookup; it never
replaces the scene objects or changes their Python types. Users can select a
registered context explicitly:

```pycon
>>> scene = Scene(context="MyContextName")
Expand Down
26 changes: 5 additions & 21 deletions src/compas/files/gltf/gltf_conversions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import warnings
from collections.abc import Sequence
from contextlib import contextmanager
from typing import TYPE_CHECKING
from typing import Any
from typing import cast
Expand All @@ -28,29 +27,14 @@
from compas.scene import Group
from compas.scene import MeshObject
from compas.scene import Scene
from compas.scene.context import ITEM_SCENEOBJECT
from compas.scene.context import register
from compas.scene.context import register_sceneobject_type

if TYPE_CHECKING:
from compas.files.gltf.gltf_document import GLTFDocument
from compas.files.gltf.gltf_node import GLTFNode
from compas.scene import SceneObject # noqa: F401


@contextmanager
def _base_scene_context():
"""Provide base scene objects without triggering visualization plugin discovery."""
original = {context: registry.copy() for context, registry in ITEM_SCENEOBJECT.items()}
register(Geometry, GeometryObject, context=None)
register(Mesh, MeshObject, context=None)
try:
yield
finally:
ITEM_SCENEOBJECT.clear()
for context, registry in original.items():
ITEM_SCENEOBJECT[context].update(registry)


def _indexed_positions(
positions: Sequence[Sequence[float]],
indices: Sequence[int] | None,
Expand Down Expand Up @@ -308,10 +292,10 @@ def add_node(node: "GLTFNode", parent: "SceneObject") -> None:
for child_key in node.children:
add_node(document.nodes[child_key], group)

with _base_scene_context():
for root_key in source_scene.children:
assert scene.root is not None
add_node(document.nodes[root_key], scene.root)
register_sceneobject_type(Geometry, GeometryObject)
register_sceneobject_type(Mesh, MeshObject)
for root_key in source_scene.children:
add_node(document.nodes[root_key], scene.root)
return scene


Expand Down
Loading
Loading