@google/model-viewer
Version: 
Easily display interactive 3D models on the web and in AR!
397 lines • 16.4 kB
JavaScript
/* @license
 * Copyright 2019 Google LLC. All Rights Reserved.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
var _a, _b, _c;
import { normalizeUnit } from './conversions';
import { numberNode, ZERO } from './parsers';
const $evaluate = Symbol('evaluate');
const $lastValue = Symbol('lastValue');
/**
 * An Evaluator is used to derive a computed style from part (or all) of a CSS
 * expression AST. This construct is particularly useful for complex ASTs
 * containing function calls such as calc, var and env. Such styles could be
 * costly to re-evaluate on every frame (and in some cases we may try to do
 * that). The Evaluator construct allows us to mark sub-trees of the AST as
 * constant, so that only the dynamic parts are re-evaluated. It also separates
 * one-time AST preparation work from work that necessarily has to happen upon
 * each evaluation.
 */
export class Evaluator {
    constructor() {
        this[_a] = null;
    }
    /**
     * An Evaluatable is a NumberNode or an Evaluator that evaluates a NumberNode
     * as the result of invoking its evaluate method. This is mainly used to
     * ensure that CSS function nodes are cast to the corresponding Evaluators
     * that will resolve the result of the function, but is also used to ensure
     * that a percentage nested at arbitrary depth in the expression will always
     * be evaluated against the correct basis.
     */
    static evaluatableFor(node, basis = ZERO) {
        if (node instanceof Evaluator) {
            return node;
        }
        if (node.type === 'number') {
            if (node.unit === '%') {
                return new PercentageEvaluator(node, basis);
            }
            return node;
        }
        switch (node.name.value) {
            case 'calc':
                return new CalcEvaluator(node, basis);
            case 'env':
                return new EnvEvaluator(node);
        }
        return ZERO;
    }
    /**
     * If the input is an Evaluator, returns the result of evaluating it.
     * Otherwise, returns the input.
     *
     * This is a helper to aide in resolving a NumberNode without conditionally
     * checking if the Evaluatable is an Evaluator everywhere.
     */
    static evaluate(evaluatable) {
        if (evaluatable instanceof Evaluator) {
            return evaluatable.evaluate();
        }
        return evaluatable;
    }
    /**
     * If the input is an Evaluator, returns the value of its isConstant property.
     * Returns true for all other input values.
     */
    static isConstant(evaluatable) {
        if (evaluatable instanceof Evaluator) {
            return evaluatable.isConstant;
        }
        return true;
    }
    /**
     * This method applies a set of structured intrinsic metadata to an evaluated
     * result from a parsed CSS-like string of expressions. Intrinsics provide
     * sufficient metadata (e.g., basis values, analogs for keywords) such that
     * omitted values in the input string can be backfilled, and keywords can be
     * converted to concrete numbers.
     *
     * The result of applying intrinsics is a tuple of NumberNode values whose
     * units match the units used by the basis of the intrinsics.
     *
     * The following is a high-level description of how intrinsics are applied:
     *
     *  1. Determine the value of 'auto' for the current term
     *  2. If there is no corresponding input value for this term, substitute the
     *     'auto' value.
     *  3. If the term is an IdentNode, treat it as a keyword and perform the
     *     appropriate substitution.
     *  4. If the term is still null, fallback to the 'auto' value
     *  5. If the term is a percentage, apply it to the basis and return that
     *     value
     *  6. Normalize the unit of the term
     *  7. If the term's unit does not match the basis unit, return the basis
     *     value
     *  8. Return the term as is
     */
    static applyIntrinsics(evaluated, intrinsics) {
        const { basis, keywords } = intrinsics;
        const { auto } = keywords;
        return basis.map((basisNode, index) => {
            // Use an auto value if we have it, otherwise the auto value is the basis:
            const autoSubstituteNode = auto[index] == null ? basisNode : auto[index];
            // If the evaluated nodes do not have a node at the current
            // index, fallback to the "auto" substitute right away:
            let evaluatedNode = evaluated[index] ? evaluated[index] : autoSubstituteNode;
            // Any ident node is considered a keyword:
            if (evaluatedNode.type === 'ident') {
                const keyword = evaluatedNode.value;
                // Substitute any keywords for concrete values first:
                if (keyword in keywords) {
                    evaluatedNode = keywords[keyword][index];
                }
            }
            // If we don't have a NumberNode at this point, fall back to whatever
            // is specified for auto:
            if (evaluatedNode == null || evaluatedNode.type === 'ident') {
                evaluatedNode = autoSubstituteNode;
            }
            // For percentages, we always apply the percentage to the basis value:
            if (evaluatedNode.unit === '%') {
                return numberNode(evaluatedNode.number / 100 * basisNode.number, basisNode.unit);
            }
            // Otherwise, normalize whatever we have:
            evaluatedNode = normalizeUnit(evaluatedNode, basisNode);
            // If the normalized units do not match, return the basis as a fallback:
            if (evaluatedNode.unit !== basisNode.unit) {
                return basisNode;
            }
            // Finally, return the evaluated node with intrinsics applied:
            return evaluatedNode;
        });
    }
    /**
     * If true, the Evaluator will only evaluate its AST one time. If false, the
     * Evaluator will re-evaluate the AST each time that the public evaluate
     * method is invoked.
     */
    get isConstant() {
        return false;
    }
    /**
     * Evaluate the Evaluator and return the result. If the Evaluator is constant,
     * the corresponding AST will only be evaluated once, and the result of
     * evaluating it the first time will be returned on all subsequent
     * evaluations.
     */
    evaluate() {
        if (!this.isConstant || this[$lastValue] == null) {
            this[$lastValue] = this[$evaluate]();
        }
        return this[$lastValue];
    }
}
_a = $lastValue;
const $percentage = Symbol('percentage');
const $basis = Symbol('basis');
/**
 * A PercentageEvaluator scales a given basis value by a given percentage value.
 * The evaluated result is always considered to be constant.
 */
export class PercentageEvaluator extends Evaluator {
    constructor(percentage, basis) {
        super();
        this[$percentage] = percentage;
        this[$basis] = basis;
    }
    get isConstant() {
        return true;
    }
    [$evaluate]() {
        return numberNode(this[$percentage].number / 100 * this[$basis].number, this[$basis].unit);
    }
}
const $identNode = Symbol('identNode');
/**
 * Evaluator for CSS-like env() functions. Currently, only one environment
 * variable is accepted as an argument for such functions: window-scroll-y.
 *
 * The env() Evaluator is explicitly dynamic because it always refers to
 * external state that changes as the user scrolls, so it should always be
 * re-evaluated to ensure we get the most recent value.
 *
 * Some important notes about this feature include:
 *
 *  - There is no such thing as a "window-scroll-y" CSS environment variable in
 *    any stable browser at the time that this comment is being written.
 *  - The actual CSS env() function accepts a second argument as a fallback for
 *    the case that the specified first argument isn't set; our syntax does not
 *    support this second argument.
 *
 * @see https://developer.mozilla.org/en-US/docs/Web/CSS/env
 */
export class EnvEvaluator extends Evaluator {
    constructor(envFunction) {
        super();
        this[_b] = null;
        const identNode = envFunction.arguments.length ? envFunction.arguments[0].terms[0] : null;
        if (identNode != null && identNode.type === 'ident') {
            this[$identNode] = identNode;
        }
    }
    get isConstant() {
        return false;
    }
    ;
    [(_b = $identNode, $evaluate)]() {
        if (this[$identNode] != null) {
            switch (this[$identNode].value) {
                case 'window-scroll-y':
                    const verticalScrollPosition = window.pageYOffset;
                    const verticalScrollMax = Math.max(document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight);
                    const scrollY = verticalScrollPosition /
                        (verticalScrollMax - window.innerHeight) ||
                        0;
                    return { type: 'number', number: scrollY, unit: null };
            }
        }
        return ZERO;
    }
}
const IS_MULTIPLICATION_RE = /[\*\/]/;
const $evaluator = Symbol('evalutor');
/**
 * Evaluator for CSS-like calc() functions. Our implementation of calc()
 * evaluation currently support nested function calls, an unlimited number of
 * terms, and all four algebraic operators (+, -, * and /).
 *
 * The Evaluator is marked as constant unless the calc expression contains an
 * internal env expression at any depth, in which case it will be marked as
 * dynamic.
 *
 * @see https://www.w3.org/TR/css-values-3/#calc-syntax
 * @see https://developer.mozilla.org/en-US/docs/Web/CSS/calc
 */
export class CalcEvaluator extends Evaluator {
    constructor(calcFunction, basis = ZERO) {
        super();
        this[_c] = null;
        if (calcFunction.arguments.length !== 1) {
            return;
        }
        const terms = calcFunction.arguments[0].terms.slice();
        const secondOrderTerms = [];
        while (terms.length) {
            const term = terms.shift();
            if (secondOrderTerms.length > 0) {
                const previousTerm = secondOrderTerms[secondOrderTerms.length - 1];
                if (previousTerm.type === 'operator' &&
                    IS_MULTIPLICATION_RE.test(previousTerm.value)) {
                    const operator = secondOrderTerms.pop();
                    const leftValue = secondOrderTerms.pop();
                    if (leftValue == null) {
                        return;
                    }
                    secondOrderTerms.push(new OperatorEvaluator(operator, Evaluator.evaluatableFor(leftValue, basis), Evaluator.evaluatableFor(term, basis)));
                    continue;
                }
            }
            secondOrderTerms.push(term.type === 'operator' ? term :
                Evaluator.evaluatableFor(term, basis));
        }
        while (secondOrderTerms.length > 2) {
            const [left, operator, right] = secondOrderTerms.splice(0, 3);
            if (operator.type !== 'operator') {
                return;
            }
            secondOrderTerms.unshift(new OperatorEvaluator(operator, Evaluator.evaluatableFor(left, basis), Evaluator.evaluatableFor(right, basis)));
        }
        // There should only be one combined evaluator at this point:
        if (secondOrderTerms.length === 1) {
            this[$evaluator] = secondOrderTerms[0];
        }
    }
    get isConstant() {
        return this[$evaluator] == null || Evaluator.isConstant(this[$evaluator]);
    }
    [(_c = $evaluator, $evaluate)]() {
        return this[$evaluator] != null ? Evaluator.evaluate(this[$evaluator]) :
            ZERO;
    }
}
const $operator = Symbol('operator');
const $left = Symbol('left');
const $right = Symbol('right');
/**
 * An Evaluator for the operators found inside CSS calc() functions.
 * The evaluator accepts an operator and left/right operands. The operands can
 * be any valid expression term typically allowed inside a CSS calc function.
 *
 * As detail of this implementation, the only supported unit types are angles
 * expressed as radians or degrees, and lengths expressed as meters, centimeters
 * or millimeters.
 *
 * @see https://developer.mozilla.org/en-US/docs/Web/CSS/calc
 */
export class OperatorEvaluator extends Evaluator {
    constructor(operator, left, right) {
        super();
        this[$operator] = operator;
        this[$left] = left;
        this[$right] = right;
    }
    get isConstant() {
        return Evaluator.isConstant(this[$left]) &&
            Evaluator.isConstant(this[$right]);
    }
    [$evaluate]() {
        const leftNode = normalizeUnit(Evaluator.evaluate(this[$left]));
        const rightNode = normalizeUnit(Evaluator.evaluate(this[$right]));
        const { number: leftValue, unit: leftUnit } = leftNode;
        const { number: rightValue, unit: rightUnit } = rightNode;
        // Disallow operations for mismatched normalized units e.g., m and rad:
        if (rightUnit != null && leftUnit != null && rightUnit != leftUnit) {
            return ZERO;
        }
        // NOTE(cdata): rules for calc type checking are defined here
        // https://drafts.csswg.org/css-values-3/#calc-type-checking
        // This is a simplification and may not hold up once we begin to support
        // additional unit types:
        const unit = leftUnit || rightUnit;
        let value;
        switch (this[$operator].value) {
            case '+':
                value = leftValue + rightValue;
                break;
            case '-':
                value = leftValue - rightValue;
                break;
            case '/':
                value = leftValue / rightValue;
                break;
            case '*':
                value = leftValue * rightValue;
                break;
            default:
                return ZERO;
        }
        return { type: 'number', number: value, unit };
    }
}
const $evaluatables = Symbol('evaluatables');
const $intrinsics = Symbol('intrinsics');
/**
 * A VectorEvaluator evaluates a series of numeric terms that usually represent
 * a data structure such as a multi-dimensional vector or a spherical
 *
 * The form of the evaluator's result is determined by the Intrinsics that are
 * given to it when it is constructed. For example, spherical intrinsics would
 * establish two angle terms and a length term, so the result of evaluating the
 * evaluator that is configured with spherical intrinsics is a three element
 * array where the first two elements represent angles in radians and the third
 * element representing a length in meters.
 */
export class StyleEvaluator extends Evaluator {
    constructor(expressions, intrinsics) {
        super();
        this[$intrinsics] = intrinsics;
        const firstExpression = expressions[0];
        const terms = firstExpression != null ? firstExpression.terms : [];
        this[$evaluatables] =
            intrinsics.basis.map((basisNode, index) => {
                const term = terms[index];
                if (term == null) {
                    return { type: 'ident', value: 'auto' };
                }
                if (term.type === 'ident') {
                    return term;
                }
                return Evaluator.evaluatableFor(term, basisNode);
            });
    }
    get isConstant() {
        for (const evaluatable of this[$evaluatables]) {
            if (!Evaluator.isConstant(evaluatable)) {
                return false;
            }
        }
        return true;
    }
    [$evaluate]() {
        const evaluated = this[$evaluatables].map(evaluatable => Evaluator.evaluate(evaluatable));
        return Evaluator.applyIntrinsics(evaluated, this[$intrinsics])
            .map(numberNode => numberNode.number);
    }
}
//# sourceMappingURL=evaluators.js.map