Commit 28cf22c0 authored by fmalita@chromium.org's avatar fmalita@chromium.org

[SVG] Refactor getIntersectionList() and getEnclosureList()

This CL moves most of the implementation out of RenderSVGModelObject and
into SVGSVGElement (which is the only client). Some notes:

* use the consolidated SVGGraphicsElement::computeCTM() (which is
  accumulating localCoordinateSpaceTransform()s and not
  localToParentTransform()s like the old getElementCTM() did <- root
  cause for the zooming bug)
* only force a layout update once, at the entry point, instead of on each
  getElementCTM() call.
* detect the stopping element on the fly, instead of an additional upfront
  ancestor crawl.
* transforms are now correctly computed relative to the target <svg>
  element instead of the nearest viewport.

BUG=155277
R=pdr@chromium.org,schenney@chromium.org,fs@opera.com,rob.buis@samsung.com

Review URL: https://codereview.chromium.org/185333004

git-svn-id: svn://svn.chromium.org/blink/trunk@168479 bbb929c8-8fbe-4397-9dbb-9b2b20218538
parent ddc343bd
Verify SVGSVGElement's getIntersectionList() and getEnclosureList() behavior.
On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
rect: [50 50 150 150]
referenceElement: null
PASS svg.getEnclosureList(rect, reference_element).length is 100
PASS svg.getIntersectionList(rect, reference_element).length is 100
rect: [55 55 145 145]
referenceElement: null
PASS svg.getEnclosureList(rect, reference_element).length is 64
PASS svg.getIntersectionList(rect, reference_element).length is 100
rect: [50 50 150 150]
referenceElement: container
PASS svg.getEnclosureList(rect, reference_element).length is 100
PASS svg.getIntersectionList(rect, reference_element).length is 100
rect: [55 55 145 145]
referenceElement: container
PASS svg.getEnclosureList(rect, reference_element).length is 64
PASS svg.getIntersectionList(rect, reference_element).length is 100
rect: [50 50 150 150]
referenceElement: subcontainer
PASS svg.getEnclosureList(rect, reference_element).length is 50
PASS svg.getIntersectionList(rect, reference_element).length is 50
rect: [55 55 145 145]
referenceElement: subcontainer
PASS svg.getEnclosureList(rect, reference_element).length is 32
PASS svg.getIntersectionList(rect, reference_element).length is 50
Zooming should not affect the results.
rect: [50 50 150 150]
referenceElement: null
PASS svg.getEnclosureList(rect, reference_element).length is 100
PASS svg.getIntersectionList(rect, reference_element).length is 100
rect: [55 55 145 145]
referenceElement: null
PASS svg.getEnclosureList(rect, reference_element).length is 64
PASS svg.getIntersectionList(rect, reference_element).length is 100
rect: [50 50 150 150]
referenceElement: container
PASS svg.getEnclosureList(rect, reference_element).length is 100
PASS svg.getIntersectionList(rect, reference_element).length is 100
rect: [55 55 145 145]
referenceElement: container
PASS svg.getEnclosureList(rect, reference_element).length is 64
PASS svg.getIntersectionList(rect, reference_element).length is 100
rect: [50 50 150 150]
referenceElement: subcontainer
PASS svg.getEnclosureList(rect, reference_element).length is 50
PASS svg.getIntersectionList(rect, reference_element).length is 50
rect: [55 55 145 145]
referenceElement: subcontainer
PASS svg.getEnclosureList(rect, reference_element).length is 32
PASS svg.getIntersectionList(rect, reference_element).length is 50
But a viewbox transform should.
rect: [50 50 150 150]
referenceElement: null
PASS svg.getEnclosureList(rect, reference_element).length is 36
PASS svg.getIntersectionList(rect, reference_element).length is 49
rect: [55 55 145 145]
referenceElement: null
PASS svg.getEnclosureList(rect, reference_element).length is 25
PASS svg.getIntersectionList(rect, reference_element).length is 36
rect: [50 50 150 150]
referenceElement: container
PASS svg.getEnclosureList(rect, reference_element).length is 36
PASS svg.getIntersectionList(rect, reference_element).length is 49
rect: [55 55 145 145]
referenceElement: container
PASS svg.getEnclosureList(rect, reference_element).length is 25
PASS svg.getIntersectionList(rect, reference_element).length is 36
rect: [50 50 150 150]
referenceElement: subcontainer
PASS svg.getEnclosureList(rect, reference_element).length is 6
PASS svg.getIntersectionList(rect, reference_element).length is 14
rect: [55 55 145 145]
referenceElement: subcontainer
PASS svg.getEnclosureList(rect, reference_element).length is 0
PASS svg.getIntersectionList(rect, reference_element).length is 6
PASS successfullyParsed is true
TEST COMPLETE
<!DOCTYPE html>
<html>
<head>
<script src="../../resources/js-test.js"></script>
</head>
<body onload="run_test()">
<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<g id="container" transform="translate(50, 50)">
<g id="subcontainer">
</g>
</g>
</svg>
<p id="description"></p>
<div id="console"></div>
<script>
jsTestIsAsync = true;
var svg = document.getElementById('svg');
var container = document.getElementById('container');
var subcontainer = document.getElementById('subcontainer');
for (var i = 0; i < 10; i++) {
for (var j = 0; j < 10; j++) {
var r = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
r.setAttribute('x', i * 10 + 1);
r.setAttribute('y', j * 10 + 1);
r.setAttribute('width', 8);
r.setAttribute('height', 8);
r.setAttribute('fill', 'green');
if (j < 5)
container.appendChild(r);
else
subcontainer.appendChild(r);
}
}
var rect = svg.createSVGRect();
var reference_element;
function check_enclosure_and_intersection(ref_id, e1, i1, e2, i2) {
reference_element = ref_id ? document.getElementById(ref_id) : null;
debug('');
rect.x = rect.y = 50;
rect.width = rect.height = 100;
debug('rect: [' + rect.x + ' ' + rect.y + ' '
+ (rect.x + rect.width) + ' ' + (rect.y + rect.height) + ']');
debug('referenceElement: ' + ref_id);
shouldBe('svg.getEnclosureList(rect, reference_element).length', e1 + '');
shouldBe('svg.getIntersectionList(rect, reference_element).length',i1 + '');
debug('');
rect.x = rect.y = 55;
rect.width = rect.height = 90;
debug('rect: [' + rect.x + ' ' + rect.y + ' '
+ (rect.x + rect.width) + ' ' + (rect.y + rect.height) + ']');
debug('referenceElement: ' + ref_id);
shouldBe('svg.getEnclosureList(rect, reference_element).length', e2 + '');
shouldBe('svg.getIntersectionList(rect, reference_element).length', i2 + '');
}
function run_test() {
description("Verify SVGSVGElement's getIntersectionList() " +
"and getEnclosureList() behavior.");
check_enclosure_and_intersection(null, 100, 100, 64, 100);
check_enclosure_and_intersection('container', 100, 100, 64, 100);
check_enclosure_and_intersection('subcontainer', 50, 50, 32, 50);
debug('');
debug('Zooming should not affect the results.');
if (window.eventSender) {
eventSender.zoomPageIn();
eventSender.zoomPageIn();
eventSender.zoomPageIn();
}
check_enclosure_and_intersection(null, 100, 100, 64, 100);
check_enclosure_and_intersection('container', 100, 100, 64, 100);
check_enclosure_and_intersection('subcontainer', 50, 50, 32, 50);
debug('');
debug('But a viewbox transform should.');
svg.setAttribute('viewBox', '0 0 150 150');
check_enclosure_and_intersection(null, 36, 49, 25, 36);
check_enclosure_and_intersection('container', 36, 49, 25, 36);
check_enclosure_and_intersection('subcontainer', 6, 14, 0, 6);
debug('');
finishJSTest();
}
</script>
</body>
</html>
......@@ -128,51 +128,6 @@ bool RenderSVGModelObject::nodeAtPoint(const HitTestRequest&, HitTestResult&, co
return false;
}
static void getElementCTM(SVGGraphicsElement* element, AffineTransform& transform)
{
ASSERT(element);
element->document().updateLayoutIgnorePendingStylesheets();
SVGElement* stopAtElement = element->nearestViewportElement();
ASSERT(stopAtElement);
AffineTransform localTransform;
Node* current = element;
while (current && current->isSVGElement()) {
SVGElement* currentElement = toSVGElement(current);
localTransform = currentElement->renderer()->localToParentTransform();
transform = localTransform.multiply(transform);
// For getCTM() computation, stop at the nearest viewport element
if (currentElement == stopAtElement)
break;
current = current->parentOrShadowHostNode();
}
}
// FloatRect::intersects does not consider horizontal or vertical lines (because of isEmpty()).
// So special-case handling of such lines.
static bool intersectsAllowingEmpty(const FloatRect& r, const FloatRect& other)
{
if (r.isEmpty() && other.isEmpty())
return false;
if (r.isEmpty() && !other.isEmpty()) {
return (other.contains(r.x(), r.y()) && !other.contains(r.maxX(), r.maxY()))
|| (!other.contains(r.x(), r.y()) && other.contains(r.maxX(), r.maxY()));
}
if (other.isEmpty() && !r.isEmpty())
return intersectsAllowingEmpty(other, r);
return r.intersects(other);
}
// One of the element types that can cause graphics to be drawn onto the target canvas. Specifically: circle, ellipse,
// image, line, path, polygon, polyline, rect, text and use.
static bool isGraphicsElement(RenderObject* renderer)
{
return renderer->isSVGShape() || renderer->isSVGText() || renderer->isSVGImage() || renderer->node()->hasTagName(SVGNames::useTag);
}
// The SVG addFocusRingRects() method adds rects in local coordinates so the default absoluteFocusRingQuads
// returns incorrect values for SVG objects. Overriding this method provides access to the absolute bounds.
void RenderSVGModelObject::absoluteFocusRingQuads(Vector<FloatQuad>& quads)
......@@ -180,30 +135,4 @@ void RenderSVGModelObject::absoluteFocusRingQuads(Vector<FloatQuad>& quads)
quads.append(localToAbsoluteQuad(FloatQuad(repaintRectInLocalCoordinates())));
}
bool RenderSVGModelObject::checkIntersection(RenderObject* renderer, const FloatRect& rect)
{
if (!renderer || renderer->style()->pointerEvents() == PE_NONE)
return false;
if (!isGraphicsElement(renderer))
return false;
AffineTransform ctm;
SVGGraphicsElement* svgElement = toSVGGraphicsElement(renderer->node());
getElementCTM(svgElement, ctm);
ASSERT(svgElement->renderer());
return intersectsAllowingEmpty(rect, ctm.mapRect(svgElement->renderer()->repaintRectInLocalCoordinates()));
}
bool RenderSVGModelObject::checkEnclosure(RenderObject* renderer, const FloatRect& rect)
{
if (!renderer || renderer->style()->pointerEvents() == PE_NONE)
return false;
if (!isGraphicsElement(renderer))
return false;
AffineTransform ctm;
SVGGraphicsElement* svgElement = toSVGGraphicsElement(renderer->node());
getElementCTM(svgElement, ctm);
ASSERT(svgElement->renderer());
return rect.contains(ctm.mapRect(svgElement->renderer()->repaintRectInLocalCoordinates()));
}
} // namespace WebCore
......@@ -63,9 +63,6 @@ public:
virtual const RenderObject* pushMappingToContainer(const RenderLayerModelObject* ancestorToStopAt, RenderGeometryMap&) const OVERRIDE FINAL;
virtual void styleDidChange(StyleDifference, const RenderStyle* oldStyle) OVERRIDE;
static bool checkIntersection(RenderObject*, const FloatRect&);
static bool checkEnclosure(RenderObject*, const FloatRect&);
virtual void computeLayerHitTestRects(LayerHitTestRects&) const OVERRIDE FINAL;
SVGElement* element() const { return toSVGElement(RenderObject::node()); }
......
......@@ -61,7 +61,8 @@ public:
static bool isAnimatableCSSProperty(const QualifiedName&);
enum CTMScope {
NearestViewportScope, // Used by SVGGraphicsElement::getCTM()
ScreenScope // Used by SVGGraphicsElement::getScreenCTM()
ScreenScope, // Used by SVGGraphicsElement::getScreenCTM()
AncestorScope // Used by SVGSVGElement::get{Enclosure|Intersection}List()
};
virtual AffineTransform localCoordinateSpaceTransform(CTMScope) const;
virtual bool needsPendingResourceHandling() const { return true; }
......
/*
* Copyright (C) 2004, 2005, 2008 Nikolas Zimmermann <zimmermann@kde.org>
* Copyright (C) 2004, 2005, 2006 Rob Buis <buis@kde.org>
* Copyright (C) 2014 Google, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
......@@ -66,24 +67,43 @@ PassRefPtr<SVGMatrixTearOff> SVGGraphicsElement::getTransformToElement(SVGElemen
return SVGMatrixTearOff::create(ctm);
}
static AffineTransform computeCTM(SVGGraphicsElement* element, SVGElement::CTMScope mode, SVGGraphicsElement::StyleUpdateStrategy styleUpdateStrategy)
static bool isViewportElement(const Element* element)
{
ASSERT(element);
if (styleUpdateStrategy == SVGGraphicsElement::AllowStyleUpdate)
element->document().updateLayoutIgnorePendingStylesheets();
return (element->hasTagName(SVGNames::svgTag)
|| element->hasTagName(SVGNames::symbolTag)
|| element->hasTagName(SVGNames::foreignObjectTag)
|| element->hasTagName(SVGNames::imageTag));
}
AffineTransform SVGGraphicsElement::computeCTM(SVGElement::CTMScope mode,
SVGGraphicsElement::StyleUpdateStrategy styleUpdateStrategy, const SVGGraphicsElement* ancestor) const
{
if (styleUpdateStrategy == AllowStyleUpdate)
document().updateLayoutIgnorePendingStylesheets();
AffineTransform ctm;
bool done = false;
SVGElement* stopAtElement = mode == SVGGraphicsElement::NearestViewportScope ? element->nearestViewportElement() : 0;
for (Element* currentElement = element; currentElement; currentElement = currentElement->parentOrShadowHostElement()) {
for (const Element* currentElement = this; currentElement && !done;
currentElement = currentElement->parentOrShadowHostElement()) {
if (!currentElement->isSVGElement())
break;
ctm = toSVGElement(currentElement)->localCoordinateSpaceTransform(mode).multiply(ctm);
// For getCTM() computation, stop at the nearest viewport element
if (currentElement == stopAtElement)
switch (mode) {
case NearestViewportScope:
// Stop at the nearest viewport ancestor.
done = currentElement != this && isViewportElement(currentElement);
break;
case AncestorScope:
// Stop at the designated ancestor.
done = currentElement == ancestor;
break;
default:
ASSERT(mode == ScreenScope);
break;
}
}
return ctm;
......@@ -91,12 +111,12 @@ static AffineTransform computeCTM(SVGGraphicsElement* element, SVGElement::CTMSc
AffineTransform SVGGraphicsElement::getCTM(StyleUpdateStrategy styleUpdateStrategy)
{
return computeCTM(this, NearestViewportScope, styleUpdateStrategy);
return computeCTM(NearestViewportScope, styleUpdateStrategy);
}
AffineTransform SVGGraphicsElement::getScreenCTM(StyleUpdateStrategy styleUpdateStrategy)
{
return computeCTM(this, ScreenScope, styleUpdateStrategy);
return computeCTM(ScreenScope, styleUpdateStrategy);
}
PassRefPtr<SVGMatrixTearOff> SVGGraphicsElement::getCTMFromJavascript()
......@@ -204,14 +224,6 @@ void SVGGraphicsElement::svgAttributeChanged(const QualifiedName& attrName)
ASSERT_NOT_REACHED();
}
static bool isViewportElement(Node* node)
{
return (node->hasTagName(SVGNames::svgTag)
|| node->hasTagName(SVGNames::symbolTag)
|| node->hasTagName(SVGNames::foreignObjectTag)
|| node->hasTagName(SVGNames::imageTag));
}
SVGElement* SVGGraphicsElement::nearestViewportElement() const
{
for (Element* current = parentOrShadowHostElement(); current; current = current->parentOrShadowHostElement()) {
......
/*
* Copyright (C) 2004, 2005, 2008 Nikolas Zimmermann <zimmermann@kde.org>
* Copyright (C) 2004, 2005, 2006, 2007 Rob Buis <buis@kde.org>
* Copyright (C) 2014 Google, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
......@@ -64,6 +65,9 @@ public:
SVGAnimatedTransformList* transform() { return m_transform.get(); }
const SVGAnimatedTransformList* transform() const { return m_transform.get(); }
AffineTransform computeCTM(SVGElement::CTMScope mode, SVGGraphicsElement::StyleUpdateStrategy,
const SVGGraphicsElement* ancestor = 0) const;
protected:
SVGGraphicsElement(const QualifiedName&, Document&, ConstructionType = CreateSVGElement);
......
......@@ -2,6 +2,7 @@
* Copyright (C) 2004, 2005, 2006 Nikolas Zimmermann <zimmermann@kde.org>
* Copyright (C) 2004, 2005, 2006, 2007, 2008, 2010 Rob Buis <buis@kde.org>
* Copyright (C) 2007 Apple Inc. All rights reserved.
* Copyright (C) 2014 Google, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
......@@ -329,49 +330,114 @@ void SVGSVGElement::svgAttributeChanged(const QualifiedName& attrName)
SVGGraphicsElement::svgAttributeChanged(attrName);
}
PassRefPtr<NodeList> SVGSVGElement::collectIntersectionOrEnclosureList(const FloatRect& rect, SVGElement* referenceElement, CollectIntersectionOrEnclosure collect) const
// FloatRect::intersects does not consider horizontal or vertical lines (because of isEmpty()).
static bool intersectsAllowingEmpty(const FloatRect& r1, const FloatRect& r2)
{
if (r1.width() < 0 || r1.height() < 0 || r2.width() < 0 || r2.height() < 0)
return false;
return r1.x() < r2.maxX() && r2.x() < r1.maxX()
&& r1.y() < r2.maxY() && r2.y() < r1.maxY();
}
// One of the element types that can cause graphics to be drawn onto the target canvas.
// Specifically: circle, ellipse, image, line, path, polygon, polyline, rect, text and use.
static bool isIntersectionOrEnclosureTarget(RenderObject* renderer)
{
return renderer->isSVGShape()
|| renderer->isSVGText()
|| renderer->isSVGImage()
|| renderer->node()->hasTagName(SVGNames::useTag);
}
bool SVGSVGElement::checkIntersectionOrEnclosure(const SVGElement& element, const FloatRect& rect,
CheckIntersectionOrEnclosure mode) const
{
RenderObject* renderer = element.renderer();
ASSERT(!renderer || renderer->style());
if (!renderer || renderer->style()->pointerEvents() == PE_NONE)
return false;
if (!isIntersectionOrEnclosureTarget(renderer))
return false;
AffineTransform ctm = toSVGGraphicsElement(element).computeCTM(AncestorScope, DisallowStyleUpdate, this);
FloatRect mappedRepaintRect = ctm.mapRect(renderer->repaintRectInLocalCoordinates());
bool result = false;
switch (mode) {
case CheckIntersection:
result = intersectsAllowingEmpty(rect, mappedRepaintRect);
break;
case CheckEnclosure:
result = rect.contains(mappedRepaintRect);
break;
default:
ASSERT_NOT_REACHED();
break;
}
return result;
}
PassRefPtr<NodeList> SVGSVGElement::collectIntersectionOrEnclosureList(const FloatRect& rect,
SVGElement* referenceElement, CheckIntersectionOrEnclosure mode) const
{
Vector<RefPtr<Node> > nodes;
Element* element = ElementTraversal::next(*(referenceElement ? referenceElement : this));
while (element) {
if (element->isSVGElement()) {
SVGElement* svgElement = toSVGElement(element);
if (collect == CollectIntersectionList) {
if (RenderSVGModelObject::checkIntersection(svgElement->renderer(), rect))
nodes.append(element);
} else {
if (RenderSVGModelObject::checkEnclosure(svgElement->renderer(), rect))
nodes.append(element);
}
const SVGElement* root = this;
if (referenceElement) {
// Only the common subtree needs to be traversed.
if (contains(referenceElement)) {
root = referenceElement;
} else if (!isDescendantOf(referenceElement)) {
// No common subtree.
return StaticNodeList::adopt(nodes);
}
}
for (Element* element = ElementTraversal::firstWithin(*root); element;
element = ElementTraversal::next(*element, root)) {
element = ElementTraversal::next(*element, referenceElement ? referenceElement : this);
if (!WebCore::isSVGGraphicsElement(*element))
continue;
SVGElement* svgElement = toSVGElement(element);
if (checkIntersectionOrEnclosure(*svgElement, rect, mode))
nodes.append(element);
}
return StaticNodeList::adopt(nodes);
}
PassRefPtr<NodeList> SVGSVGElement::getIntersectionList(PassRefPtr<SVGRectTearOff> passRect, SVGElement* referenceElement) const
PassRefPtr<NodeList> SVGSVGElement::getIntersectionList(PassRefPtr<SVGRectTearOff> rect, SVGElement* referenceElement) const
{
RefPtr<SVGRectTearOff> rect = passRect;
return collectIntersectionOrEnclosureList(rect->target()->value(), referenceElement, CollectIntersectionList);
document().updateLayoutIgnorePendingStylesheets();
return collectIntersectionOrEnclosureList(rect->target()->value(), referenceElement, CheckIntersection);
}
PassRefPtr<NodeList> SVGSVGElement::getEnclosureList(PassRefPtr<SVGRectTearOff> passRect, SVGElement* referenceElement) const
PassRefPtr<NodeList> SVGSVGElement::getEnclosureList(PassRefPtr<SVGRectTearOff> rect, SVGElement* referenceElement) const
{
RefPtr<SVGRectTearOff> rect = passRect;
return collectIntersectionOrEnclosureList(rect->target()->value(), referenceElement, CollectEnclosureList);
document().updateLayoutIgnorePendingStylesheets();
return collectIntersectionOrEnclosureList(rect->target()->value(), referenceElement, CheckEnclosure);
}
bool SVGSVGElement::checkIntersection(SVGElement* element, PassRefPtr<SVGRectTearOff> passRect) const
bool SVGSVGElement::checkIntersection(SVGElement* element, PassRefPtr<SVGRectTearOff> rect) const
{
RefPtr<SVGRectTearOff> rect = passRect;
return RenderSVGModelObject::checkIntersection(element->renderer(), rect->target()->value());
ASSERT(element);
document().updateLayoutIgnorePendingStylesheets();
return checkIntersectionOrEnclosure(*element, rect->target()->value(), CheckIntersection);
}
bool SVGSVGElement::checkEnclosure(SVGElement* element, PassRefPtr<SVGRectTearOff> passRect) const
bool SVGSVGElement::checkEnclosure(SVGElement* element, PassRefPtr<SVGRectTearOff> rect) const
{
RefPtr<SVGRectTearOff> rect = passRect;
return RenderSVGModelObject::checkEnclosure(element->renderer(), rect->target()->value());
ASSERT(element);
document().updateLayoutIgnorePendingStylesheets();
return checkIntersectionOrEnclosure(*element, rect->target()->value(), CheckEnclosure);
}
void SVGSVGElement::deselectAll()
......
/*
* Copyright (C) 2004, 2005, 2006 Nikolas Zimmermann <zimmermann@kde.org>
* Copyright (C) 2004, 2005, 2006, 2007, 2010 Rob Buis <buis@kde.org>
* Copyright (C) 2014 Google, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
......@@ -151,12 +152,13 @@ private:
void updateCurrentTranslate();
enum CollectIntersectionOrEnclosure {
CollectIntersectionList,
CollectEnclosureList
enum CheckIntersectionOrEnclosure {
CheckIntersection,
CheckEnclosure
};
PassRefPtr<NodeList> collectIntersectionOrEnclosureList(const FloatRect&, SVGElement*, CollectIntersectionOrEnclosure) const;
bool checkIntersectionOrEnclosure(const SVGElement&, const FloatRect&, CheckIntersectionOrEnclosure) const;
PassRefPtr<NodeList> collectIntersectionOrEnclosureList(const FloatRect&, SVGElement*, CheckIntersectionOrEnclosure) const;
RefPtr<SVGAnimatedLength> m_x;
RefPtr<SVGAnimatedLength> m_y;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment