/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use api::{ExternalScrollId, PropertyBinding, ReferenceFrameKind, TransformStyle, PropertyBindingId}; use api::{APZScrollGeneration, HasScrollLinkedEffect, PipelineId, SampledScrollOffset}; use api::units::*; use euclid::Transform3D; usecrate::transform::TransformPalette; usecrate::internal_types::{FastHashMap, FrameMemory}; usecrate::print_tree::{PrintableTree, PrintTree, PrintTreePrinter}; usecrate::scene::SceneProperties; usecrate::spatial_node::{ReferenceFrameInfo, SpatialNode, SpatialNodeDescriptor, SpatialNodeType, StickyFrameInfo}; usecrate::spatial_node::{ScrollFrameKind, SceneSpatialNode, SpatialNodeInfo}; use std::{ops, u32}; usecrate::util::{FastTransform, LayoutToWorldFastTransform, MatrixHelpers, ScaleOffset, scale_factors}; use smallvec::SmallVec; usecrate::util::TransformedRectKind; use peek_poke::PeekPoke;
/// An id that identifies coordinate systems in the SpatialTree. Each /// coordinate system has an id and those ids will be shared when the coordinates /// system are the same or are in the same axis-aligned space. This allows /// for optimizing mask generation. #[derive(Copy, Clone, PartialEq, PartialOrd)] #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] pubstruct CoordinateSystemId(pub u32);
/// May be set on a cluster / picture during scene building if the spatial /// node is not known at this time. It must be set to a valid value before /// scene building is complete (by `finalize_picture`). In future, we could /// make this type-safe with a wrapper type to ensure we know when a spatial /// node index may have an unknown value. pubconst UNKNOWN: SpatialNodeIndex = SpatialNodeIndex(u32::MAX - 1);
}
// In some cases, the conversion from CSS pixels to device pixels can result in small // rounding errors when calculating the scrollable distance of a scroll frame. Apply // a small epsilon so that we don't detect these frames as "real" scroll frames. const MIN_SCROLLABLE_AMOUNT: f32 = 0.01;
// The minimum size for a scroll frame for it to be considered for a scroll root. const MIN_SCROLL_ROOT_SIZE: f32 = 128.0;
impl SpatialNodeIndex { pubfn new(index: usize) -> Self {
debug_assert!(index < ::std::u32::MAX as usize);
SpatialNodeIndex(index as u32)
}
}
/// Allows functions and methods to retrieve common information about /// a spatial node, whether during scene or frame building pubtrait SpatialNodeContainer { /// Get the common information for a given spatial node fn get_node_info(&self, index: SpatialNodeIndex) -> SpatialNodeInfo;
}
/// The representation of the spatial tree during scene building, which is /// mostly write-only, with a small number of queries for snapping, /// picture cache building. /// /// Each `SceneBuilder::build` call calls `reset()` to start the tree fresh, /// then emits a complete list of `SpatialTreeUpdate::Insert` ops that the /// frame-side `SpatialTree::apply_updates` consumes verbatim. #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] pubstruct SceneSpatialTree { /// Nodes which determine the positions (offsets and transforms) for primitives /// and clips.
spatial_nodes: Vec<SceneSpatialNode>,
root_reference_frame_index: SpatialNodeIndex,
updates: SpatialTreeUpdates,
}
impl SpatialNodeContainer for SceneSpatialTree { fn get_node_info(&self, index: SpatialNodeIndex) -> SpatialNodeInfo { let node = &self.spatial_nodes[index.0as usize];
/// Reset the tree to an empty state with just the root reference frame. /// Called at the start of each scene build. pubfn reset(&mutself) { self.spatial_nodes.clear(); self.updates = SpatialTreeUpdates::new(); self.add_root_reference_frame();
}
/// Complete building this scene, return the updates to apply to the frame spatial tree pubfn end_frame_and_get_pending_updates(&mutself) -> SpatialTreeUpdates { self.updates.root_reference_frame_index = self.root_reference_frame_index;
std::mem::replace(&mutself.updates, SpatialTreeUpdates::new())
}
/// Check if a given spatial node is an ancestor of another spatial node. pubfn is_ancestor(
&self,
maybe_parent: SpatialNodeIndex,
maybe_child: SpatialNodeIndex,
) -> bool { // Early out if same node if maybe_parent == maybe_child { returnfalse;
}
letmut current_node = maybe_child;
while current_node != self.root_reference_frame_index { let node = self.get_node_info(current_node);
current_node = node.parent.expect("bug: no parent");
if current_node == maybe_parent { returntrue;
}
}
false
}
/// Find the spatial node that is the scroll root for a given spatial node. /// A scroll root is the first spatial node when found travelling up the /// spatial node tree that is an explicit scroll frame. pubfn find_scroll_root(
&self,
spatial_node_index: SpatialNodeIndex,
allow_sticky_frames: bool,
) -> SpatialNodeIndex { letmut real_scroll_root = self.root_reference_frame_index; letmut outermost_scroll_root = self.root_reference_frame_index; letmut current_scroll_root_is_sticky = false; letmut node_index = spatial_node_index;
while node_index != self.root_reference_frame_index { let node = self.get_node_info(node_index); match node.node_type {
SpatialNodeType::ReferenceFrame(ref info) => { match info.kind {
ReferenceFrameKind::Transform { is_2d_scale_translation: true, .. } => { // We can handle scroll nodes that pass through a 2d scale/translation node
}
ReferenceFrameKind::Transform { is_2d_scale_translation: false, .. } |
ReferenceFrameKind::Perspective { .. } => { // When a reference frame is encountered, forget any scroll roots // we have encountered, as they may end up with a non-axis-aligned transform.
real_scroll_root = self.root_reference_frame_index;
outermost_scroll_root = self.root_reference_frame_index;
current_scroll_root_is_sticky = false;
}
}
}
SpatialNodeType::StickyFrame(..) => { // Though not a scroll frame, we optionally treat sticky frames as scroll roots // to ensure they are given a separate picture cache slice. if allow_sticky_frames {
outermost_scroll_root = node_index;
real_scroll_root = node_index; // Set this true so that we don't select an ancestor scroll frame as the scroll root // on a subsequent iteration.
current_scroll_root_is_sticky = true;
}
}
SpatialNodeType::ScrollFrame(ref info) => { match info.frame_kind {
ScrollFrameKind::PipelineRoot { is_root_pipeline } => { // Once we encounter a pipeline root, there is no need to look further if is_root_pipeline { break;
}
}
ScrollFrameKind::Explicit => { // Store the closest scroll root we find to the root, for use // later on, even if it's not actually scrollable.
outermost_scroll_root = node_index;
// If the previously identified scroll root is sticky then we don't // want to choose an ancestor scroll root, as we want the sticky item // to have its own picture cache slice. if !current_scroll_root_is_sticky { // If the scroll root has no scrollable area, we don't want to // consider it. This helps pages that have a nested scroll root // within a redundant scroll root to avoid selecting the wrong // reference spatial node for a picture cache. if info.scrollable_size.width > MIN_SCROLLABLE_AMOUNT ||
info.scrollable_size.height > MIN_SCROLLABLE_AMOUNT { // Since we are skipping redundant scroll roots, we may end up // selecting inner scroll roots that are very small. There is // no performance benefit to creating a slice for these roots, // as they are cheap to rasterize. The size comparison is in // local-space, but makes for a reasonable estimate. The value // is arbitrary, but is generally small enough to ignore things // like scroll roots around text input elements. if info.viewport_rect.width() > MIN_SCROLL_ROOT_SIZE &&
info.viewport_rect.height() > MIN_SCROLL_ROOT_SIZE { // If we've found a root that is scrollable, and a reasonable // size, select that as the current root for this node
real_scroll_root = node_index;
}
}
}
}
}
}
}
node_index = node.parent.expect("unable to find parent node");
}
// If we didn't find any real (scrollable) frames, then return the outermost // redundant scroll frame. This is important so that we can correctly find // the clips defined on the content which should be handled when drawing the // picture cache tiles (by definition these clips are ancestors of the // scroll root selected for the picture cache). if real_scroll_root == self.root_reference_frame_index {
outermost_scroll_root
} else {
real_scroll_root
}
}
/// The root reference frame, which is the true root of the SpatialTree. pubfn root_reference_frame_index(&self) -> SpatialNodeIndex { self.root_reference_frame_index
}
fn add_spatial_node(
&mutself,
node: SceneSpatialNode,
) -> SpatialNodeIndex { let descriptor = node.descriptor.clone(); let parent = node.parent;
let index = self.spatial_nodes.len(); self.spatial_nodes.push(node);
pubfn add_reference_frame(
&mutself,
parent_index: SpatialNodeIndex,
transform_style: TransformStyle,
source_transform: PropertyBinding<LayoutTransform>,
kind: ReferenceFrameKind,
origin_in_parent_reference_frame: LayoutVector2D,
pipeline_id: PipelineId,
is_pipeline_root: bool,
) -> SpatialNodeIndex { // Determine if this reference frame creates a new static coordinate system let new_static_coord_system = match kind {
ReferenceFrameKind::Transform { is_2d_scale_translation: true, .. } => { // Client has guaranteed this transform will only be axis-aligned false
}
ReferenceFrameKind::Transform { is_2d_scale_translation: false, .. } | ReferenceFrameKind::Perspective { .. } => { // Even if client hasn't promised it's an axis-aligned transform, we can still // check this so long as the transform isn't animated (and thus could change to // anything by APZ during frame building) match source_transform {
PropertyBinding::Value(m) => {
!m.is_2d_scale_translation()
}
PropertyBinding::Binding(..) => { // Animated, so assume it may introduce a complex transform true
}
}
}
};
let is_root_coord_system = !new_static_coord_system && self.spatial_nodes[parent_index.0as usize].is_root_coord_system;
pubfn add_sticky_frame(
&mutself,
parent_index: SpatialNodeIndex,
sticky_frame_info: StickyFrameInfo,
pipeline_id: PipelineId,
) -> SpatialNodeIndex { // Sticky frames are only 2d translations - they can't introduce a new static coord system let is_root_coord_system = self.spatial_nodes[parent_index.0as usize].is_root_coord_system;
/// The full set of spatial nodes for the scene that just finished building. /// `apply_updates` consumes this by replacing the frame-side tree wholesale. #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] pubstruct SpatialTreeUpdates {
root_reference_frame_index: SpatialNodeIndex,
updates: Vec<SpatialTreeUpdate>,
}
/// Represents the spatial tree during frame building, which is mostly /// read-only, apart from the tree update at the start of the frame #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] pubstruct SpatialTree { /// Nodes which determine the positions (offsets and transforms) for primitives /// and clips.
spatial_nodes: Vec<SpatialNode>,
/// A list of transforms that establish new coordinate systems. /// Spatial nodes only establish a new coordinate system when /// they have a transform that is not a simple 2d translation.
coord_systems: Vec<CoordinateSystem>,
root_reference_frame_index: SpatialNodeIndex,
/// Stack of current state for each parent node while traversing and updating tree
update_state_stack: Vec<TransformUpdateState>,
}
/// An id for keeping track of the axis-aligned space of this node. This is used in /// order to to track what kinds of clip optimizations can be done for a particular /// display list item, since optimizations can usually only be done among /// coordinate systems which are relatively axis aligned. pub current_coordinate_system_id: CoordinateSystemId,
/// Scale and offset from the coordinate system that started this compatible coordinate system. pub coordinate_system_relative_scale_offset: ScaleOffset,
/// True if this node is transformed by an invertible transform. If not, display items /// transformed by this node will not be displayed and display items not transformed by this /// node will not be clipped by clips that are transformed by this node. pub invertible: bool,
/// True if this node is a part of Preserve3D hierarchy. pub preserves_3d: bool,
/// True if the any parent nodes are currently zooming pub is_ancestor_or_self_zooming: bool,
/// Set to true if this state represents a scroll node with external id pub external_id: Option<ExternalScrollId>,
/// The node scroll offset if this state is a scroll/sticky node. Zero if a reference frame. pub scroll_offset: LayoutVector2D,
}
/// Transformation between two nodes in the spatial tree that can sometimes be /// encoded more efficiently than with a full matrix. #[derive(Debug, Clone)] pubenum CoordinateSpaceMapping<Src, Dst> {
Local,
ScaleOffset(ScaleOffset),
Transform(Transform3D<f32, Src, Dst>),
}
let node = self.get_spatial_node_mut(index);
f(index, node);
child_indices.extend_from_slice(&node.children);
for child_index in child_indices { self.visit_node_impl_mut(child_index, f);
}
}
fn visit_node_impl<F>(
&self,
index: SpatialNodeIndex,
f: &mut F,
) where F: FnMut(SpatialNodeIndex, &SpatialNode) { let node = self.get_spatial_node(index);
f(index, node);
for child_index in &node.children { self.visit_node_impl(*child_index, f);
}
}
/// Visit all nodes from the root of the tree, invoking a closure on each one pubfn visit_nodes<F>(&self, mut f: F) where F: FnMut(SpatialNodeIndex, &SpatialNode) { ifself.root_reference_frame_index == SpatialNodeIndex::INVALID { return;
}
/// Visit all nodes from the root of the tree, invoking a closure on each one pubfn visit_nodes_mut<F>(&mutself, mut f: F) where F: FnMut(SpatialNodeIndex, &mut SpatialNode) { ifself.root_reference_frame_index == SpatialNodeIndex::INVALID { return;
}
/// Replace this tree with the contents of a freshly-built scene. pubfn apply_updates(
&mutself,
updates: SpatialTreeUpdates,
) { self.root_reference_frame_index = updates.root_reference_frame_index; self.spatial_nodes.clear();
for SpatialTreeUpdate { index, parent, descriptor } in updates.updates {
debug_assert_eq!(index, self.spatial_nodes.len());
iflet Some(parent) = parent { self.get_spatial_node_mut(parent).add_child(SpatialNodeIndex(index as u32));
}
self.visit_nodes(|index, node| { if node.is_transform_bound_to_property(id) {
debug_assert!(node_index.is_none()); // Multiple nodes with same anim id
node_index = Some(index);
}
});
node_index
}
/// Calculate the relative transform from `child_index` to `parent_index`. /// This method will panic if the nodes are not connected! pubfn get_relative_transform(
&self,
child_index: SpatialNodeIndex,
parent_index: SpatialNodeIndex,
) -> CoordinateSpaceMapping<LayoutPixel, LayoutPixel> { self.get_relative_transform_with_face(child_index, parent_index, None)
}
/// Calculate the relative transform from `child_index` to `parent_index`. /// This method will panic if the nodes are not connected! /// Also, switch the visible face to `Back` if at any stage where the /// combined transform is flattened, we see the back face. pubfn get_relative_transform_with_face(
&self,
child_index: SpatialNodeIndex,
parent_index: SpatialNodeIndex, mut visible_face: Option<&mut VisibleFace>,
) -> CoordinateSpaceMapping<LayoutPixel, LayoutPixel> { if child_index == parent_index { return CoordinateSpaceMapping::Local;
}
let child = self.get_spatial_node(child_index); let parent = self.get_spatial_node(parent_index);
// TODO(gw): We expect this never to fail, but it's possible that it might due to // either (a) a bug in WR / Gecko, or (b) some obscure real-world content // that we're unaware of. If we ever hit this, please open a bug with any // repro steps!
assert!(
child.coordinate_system_id.0 >= parent.coordinate_system_id.0, "bug: this is an unexpected case - please open a bug and talk to #gfx team!",
);
if child.coordinate_system_id == parent.coordinate_system_id { let scale_offset = child.content_transform.then(&parent.content_transform.inverse());
// Optimization - detect identity scale-offsets and treat them as // local to skip following math if scale_offset.is_identity() { return CoordinateSpaceMapping::Local;
}
// we need to update the associated parameters of a transform in two cases: // 1) when the flattening happens, so that we don't lose that original 3D aspects // 2) when we reach the end of iteration, so that our result is up to date
while coordinate_system_id != parent.coordinate_system_id { let coord_system = &self.coord_systems[coordinate_system_id.0as usize];
if coord_system.should_flatten { iflet Some(refmut face) = visible_face { if transform.is_backface_visible() {
**face = VisibleFace::Back;
}
}
transform.flatten_z_output();
}
/// Returns true if both supplied spatial nodes are in the same coordinate system /// (implies the relative transform produce axis-aligned rects). pubfn is_matching_coord_system(
&self,
index0: SpatialNodeIndex,
index1: SpatialNodeIndex,
) -> bool { let node0 = self.get_spatial_node(index0); let node1 = self.get_spatial_node(index1);
if child.coordinate_system_id.0 == 0 { if index == self.root_reference_frame_index {
CoordinateSpaceMapping::Local
} else { match scroll {
TransformScroll::Scrolled => CoordinateSpaceMapping::ScaleOffset(child.content_transform),
TransformScroll::Unscrolled => CoordinateSpaceMapping::ScaleOffset(child.viewport_transform),
}
}
} else { let system = &self.coord_systems[child.coordinate_system_id.0as usize]; let scale_offset = match scroll {
TransformScroll::Scrolled => &child.content_transform,
TransformScroll::Unscrolled => &child.viewport_transform,
}; let transform = scale_offset
.to_transform()
.then(&system.world_transform);
CoordinateSpaceMapping::Transform(transform)
}
}
/// Calculate the relative transform from `index` to the root. pubfn get_world_transform(
&self,
index: SpatialNodeIndex,
) -> CoordinateSpaceMapping<LayoutPixel, WorldPixel> { self.get_world_transform_impl(index, TransformScroll::Scrolled)
}
/// Calculate the relative transform from `index` to the root. /// Unlike `get_world_transform`, this variant doesn't account for the local scroll offset. pubfn get_world_viewport_transform(
&self,
index: SpatialNodeIndex,
) -> CoordinateSpaceMapping<LayoutPixel, WorldPixel> { self.get_world_transform_impl(index, TransformScroll::Unscrolled)
}
/// The root reference frame, which is the true root of the SpatialTree. pubfn root_reference_frame_index(&self) -> SpatialNodeIndex { self.root_reference_frame_index
}
for child_index in &node.children { self.print_node(*child_index, pt);
}
pt.end_level();
}
/// Get the visible face of the transfrom from the specified node to its parent. pubfn get_local_visible_face(&self, node_index: SpatialNodeIndex) -> VisibleFace { let node = self.get_spatial_node(node_index); letmut face = VisibleFace::Front; iflet Some(mut parent_index) = node.parent { // Check if the parent is perspective. In CSS, a stacking context may // have both perspective and a regular transformation. Gecko translates the // perspective into a different `nsDisplayPerspective` and `nsDisplayTransform` items. // On WebRender side, we end up with 2 different reference frames: // one has kind of "transform", and it's parented to another of "perspective": // https://searchfox.org/mozilla-central/rev/72c7cef167829b6f1e24cae216fa261934c455fc/layout/generic/nsIFrame.cpp#3716 iflet SpatialNodeType::ReferenceFrame(ReferenceFrameInfo { kind: ReferenceFrameKind::Transform {
paired_with_perspective: true,
..
}, .. }) = node.node_type { let parent = self.get_spatial_node(parent_index); match parent.node_type {
SpatialNodeType::ReferenceFrame(ReferenceFrameInfo {
kind: ReferenceFrameKind::Perspective { .. },
..
}) => {
parent_index = parent.parent.unwrap();
}
_ => {
log::error!("Unexpected parent {:?} is not perspective", parent_index);
}
}
}
self.get_relative_transform_with_face(node_index, parent_index, Some(&mut face));
}
face
}
#[allow(dead_code)] pubfn print_to_string(&self) -> String { letmut result = String::new();
#[allow(dead_code)] pubfn print(&self) { let result = self.print_to_string(); // If running in Gecko, set RUST_LOG=webrender::spatial_tree=debug // to get this logging to be emitted to stderr/logcat.
debug!("{}", result);
}
}
let p = LayoutPoint::new(px, py); let m = cst.get_relative_transform(child, parent).into_transform(); let pt = m.transform_point2d(p).unwrap();
assert!(pt.x.approx_eq_eps(&expected_x, &EPSILON) &&
pt.y.approx_eq_eps(&expected_y, &EPSILON), "p: {:?} -> {:?}\nm={:?}",
p, pt, m,
);
}
#[test] fn test_cst_simple_translation() { // Basic translations only
letmut cst = SceneSpatialTree::new(); let root_reference_frame_index = cst.root_reference_frame_index();
let root = add_reference_frame(
&mut cst,
root_reference_frame_index,
LayoutTransform::identity(),
LayoutVector2D::zero(),
);
/// Tests that we select the root scroll frame rather than the subframe if both are scrollable. #[test] fn test_find_scroll_root_sub_scroll_frame() { letmut st = SceneSpatialTree::new();
/// Tests that we select the sub scroll frame when the root scroll frame is not scrollable. #[test] fn test_find_scroll_root_not_scrollable() { letmut st = SceneSpatialTree::new();
/// Tests that we select the sub scroll frame when the root scroll frame is too small. #[test] fn test_find_scroll_root_too_small() { letmut st = SceneSpatialTree::new();
/// Tests that we select the root scroll node, even if it is not scrollable, /// when encountering a non-axis-aligned transform. #[test] fn test_find_scroll_root_perspective() { letmut st = SceneSpatialTree::new();
/// Tests that encountering a 2D scale or translation transform does not prevent /// us from selecting the sub scroll frame if the root scroll frame is unscrollable. #[test] fn test_find_scroll_root_2d_scale() { letmut st = SceneSpatialTree::new();
/// Tests that a sticky spatial node is chosen as the scroll root rather than /// its parent scroll frame #[test] fn test_find_scroll_root_sticky() { letmut st = SceneSpatialTree::new();
#[test] fn test_world_transforms() { // Create a spatial tree with a scroll frame node with scroll offset (0, 200). letmut cst = SceneSpatialTree::new(); let scroll = cst.add_scroll_frame(
cst.root_reference_frame_index(),
ExternalScrollId(1, PipelineId::dummy()),
PipelineId::dummy(),
&LayoutRect::from_size(LayoutSize::new(400.0, 400.0)),
&LayoutSize::new(400.0, 800.0),
ScrollFrameKind::Explicit,
LayoutVector2D::new(0.0, 200.0),
APZScrollGeneration::default(),
HasScrollLinkedEffect::No);
letmut st = SpatialTree::new();
st.apply_updates(cst.end_frame_and_get_pending_updates());
st.update_tree(&SceneProperties::new());
// The node's world transform should reflect the scroll offset, // e.g. here it should be (0, -200) to reflect that the content has been // scrolled up by 200px.
assert_eq!(
st.get_world_transform(scroll).into_transform(),
LayoutToWorldTransform::translation(0.0, -200.0, 0.0));
// The node's world viewport transform only reflects enclosing scrolling // or transforms. Here we don't have any, so it should be the identity.
assert_eq!(
st.get_world_viewport_transform(scroll).into_transform(),
LayoutToWorldTransform::identity());
}
/// Tests that a spatial node that is async zooming and all of its descendants /// are correctly marked as having themselves an ancestor that is zooming. #[test] fn test_is_ancestor_or_self_zooming() { letmut cst = SceneSpatialTree::new(); let root_reference_frame_index = cst.root_reference_frame_index();
let root = add_reference_frame(
&mut cst,
root_reference_frame_index,
LayoutTransform::identity(),
LayoutVector2D::zero(),
); let child1 = add_reference_frame(
&mut cst,
root,
LayoutTransform::identity(),
LayoutVector2D::zero(),
); let child2 = add_reference_frame(
&mut cst,
child1,
LayoutTransform::identity(),
LayoutVector2D::zero(),
);
letmut st = SpatialTree::new();
st.apply_updates(cst.end_frame_and_get_pending_updates());
// Mark the root node as async zooming
st.get_spatial_node_mut(root).is_async_zooming = true;
st.update_tree(&SceneProperties::new());
// Ensure that the root node and all descendants are marked as having // themselves or an ancestor zooming
assert!(st.get_spatial_node(root).is_ancestor_or_self_zooming);
assert!(st.get_spatial_node(child1).is_ancestor_or_self_zooming);
assert!(st.get_spatial_node(child2).is_ancestor_or_self_zooming);
}
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.33 Sekunden
(vorverarbeitet am 2026-08-25)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.