import { QuantumElementType , QuantumElement , QuantumElementCreationOptions } from '../document-element' ; import { ref , Ref , watch , reactive , shallowRef , computed , watchEffect , shallowReactive } from 'vue' ; import { UseScopedVariable , UseScopedGetter , ScopeElementType } from './scope-element' ; import { cas } from '../../cas' ; import { assert } from '../../assert' ; import { CasCommand , CasExpression } from '../../../cas/cas' ; import { getGetterNames } from '../../../cas/cas-math' ; import { Expression , match , substitute } from '@cortex-js/compute-engine' ; import { Vector2 } from '../../vectors' ; import { getExpressionValue , handleExpressionValue } from './../../../cas/mathjson-utils' ; import * as Notification from '../../../ui/notification' ; export const ElementType = 'expression-element' ; export class ExpressionElement extends QuantumElement { typeName : typeof ElementType = ElementType ; readonly expression = shallowRef < Expression > ( '' ) ; readonly getters : Map < string , UseScopedGetter > = shallowReactive ( new Map < string , UseScopedGetter > ( ) ) ; readonly variables : Map < string , UseScopedVariable > = shallowReactive ( new Map < string , UseScopedVariable > ( ) ) ; private readonly runningCasCommand : Ref < CasCommand | undefined > = shallowRef ( ) ; private readonly blockPosition = computed ( ( ) => this . position . value ) ; constructor ( options ) { super ( options ) ; watch ( this . scope , ( value ) => { this . updateGetters ( new Set < string > ( ) ) ; this . updateVariables ( new Set < string > ( ) ) ; if ( this . runningCasCommand . value ) { cas . cancelCommand ( this . runningCasCommand . value ) ; } if ( value ) { if ( ! this . hasExpression ) return ; this . updateGetters ( getGetterNames ( this . expression . value ) ) ; this . updateVariables ( getVariableNames ( this . expression . value ) ) ; this . clearResultAndVariables ( ) ; this . evaluateLater ( ) ; } } ) ; } get hasExpression ( ) { return this . expression . value !== '' && this . expression . value !== null && this . expression . value !== undefined ; } returns private getGettersData ( ) { const gettersData = new Map < string , Expression > ( ) ; for ( const [ name , getter ] of this . getters ) { const data = getter . data . value ; if ( data === undefined ) { } else if ( data === null ) { return ; } else { gettersData . set ( name , data ) ; } } return gettersData ; } param value inputExpression ( value ) { this . expression . value = value ; if ( ! this . scope . value ) return ; if ( ! this . hasExpression ) return ; this . updateGetters ( getGetterNames ( this . expression . value ) ) ; this . updateVariables ( getVariableNames ( this . expression . value ) ) ; this . clearResultAndVariables ( ) ; this . evaluateLater ( ) ; } private clearResultAndVariables ( ) { this . variables . forEach ( ( v ) => v . setData ( null ) ) ; this . expression . value = clearResult ( this . expression . value ) ; } private updateGetters ( getterNames ) { const namesToAdd = new Set < string > ( getterNames ) ; this . getters . forEach ( ( getter , variableName ) => { if ( ! namesToAdd . has ( variableName ) ) { getter . remove ( ) ; this . getters . delete ( variableName ) ; } else { namesToAdd . delete ( variableName ) ; } } ) ; if ( namesToAdd . size <= 0 ) return ; const scope = this . scope . value ; assert ( scope , 'Expected the block to have a scope' ) ; namesToAdd . forEach ( ( variableName ) => { const newGetter = scope . addGetter ( variableName , this . blockPosition ) ; watch ( newGetter . data , ( value ) => { this . clearResultAndVariables ( ) ; if ( value !== undefined && value !== null ) this . evaluateLater ( ) ; } ) ; this . getters . set ( variableName , newGetter ) ; } ) ; } private updateVariables ( variableNames ) { const namesToAdd = new Set < string > ( variableNames ) ; this . variables . forEach ( ( variable , variableName ) => { if ( ! namesToAdd . has ( variableName ) ) { variable . remove ( ) ; this . variables . delete ( variableName ) ; } else { namesToAdd . delete ( variableName ) ; } } ) ; if ( namesToAdd . size <= 0 ) return ; const scope = this . scope . value ; assert ( scope , 'Expected the block to have a scope' ) ; namesToAdd . forEach ( ( variableName ) => { const newVariable = scope . addVariable ( variableName , this . blockPosition ) ; this . variables . set ( variableName , newVariable ) ; } ) ; } /**
     * Returns `true` if it's an "Equal" or "Evaluate" expression.
     */ private isCasExpression ( expression ) { const value = getExpressionValue ( expression ) ; if ( value . type === 'function' ) { const { head } = value . value ; if ( head === 'Equal' || head === 'Evaluate' ) { return true ; } } return false ; } private executeCasExpression ( expression , gettersData , callback ) { const value = getExpressionValue ( expression ) ; assert ( value . type === 'function' , 'Expected a function expression' ) ; if ( this . isCasExpression ( expression [ 1 ] ) ) { this . executeCasExpression ( expression [ 1 ] , gettersData , ( result , resultingExpression ) => { expression = expression . slice ( ) as CasExpression ; expression [ 1 ] = resultingExpression ; callback ( null , expression ) ; const computeExpression = expression . slice ( ) as CasExpression ; computeExpression [ 1 ] = result ; this . runningCasCommand . value = new CasCommand ( gettersData , computeExpression , ( result ) => { if ( result instanceof Error ) { Notification . error ( 'CAS error' , result . message ) ; result = [ 'Error' , 'Missing' , { str : ( result . message + '' ) . slice ( 0 , 40 ) } ] ; } const output = expression . slice ( ) ; if ( expression [ 0 ] === 'Evaluate' ) { output [ 3 ] = [ '\\mathinner' , result ] ; } else { output [ 2 ] = [ '\\mathinner' , result ] ; } callback ( result , output ) ; } ) ; cas . executeCommand ( this . runningCasCommand . value ) ; } ) ; } else { this . runningCasCommand . value = new CasCommand ( gettersData , expression , ( result ) => { if ( result instanceof Error ) { Notification . error ( 'CAS error' , result . message ) ; result = [ 'Error' , 'Missing' , { str : ( result . message + '' ) . slice ( 0 , 40 ) } ] ; } const output = expression . slice ( ) ; if ( expression [ 0 ] === 'Evaluate' ) { output [ 3 ] = [ '\\mathinner' , result ] ; } else { output [ 2 ] = [ '\\mathinner' , result ] ; } callback ( result , output ) ; } ) ; cas . executeCommand ( this . runningCasCommand . value ) ; } } private evaluateLater ( ) { if ( this . runningCasCommand . value ) { cas . cancelCommand ( this . runningCasCommand . value ) ; } if ( ! this . hasExpression ) return ; const gettersData = this . getGettersData ( ) ; if ( ! gettersData ) { return ; } const value = getExpressionValue ( this . expression . value ) ; if ( value . type === 'function' ) { const { head , args } = value . value ; if ( head === 'Assign' ) { const innerExpression = args [ 1 ] ; if ( this . isCasExpression ( innerExpression ) ) { this . executeCasExpression ( innerExpression , gettersData , ( result , resultingExpression ) => { this . expression . value = [ head , args [ 0 ] , resultingExpression ] ; assert ( this . variables . size === 1 , 'Assigning to multiple variables not supported yet' ) ; this . variables . forEach ( ( v ) => v . setData ( result ) ) ; } ) ; } else { this . executeCasExpression ( [ 'Evaluate' , innerExpression , 'Missing' , 'Missing' ] , gettersData , ( result , _ ) => { assert ( this . variables . size === 1 , 'Assigning to multiple variables not supported yet' ) ; this . variables . forEach ( ( v ) => v . setData ( result ) ) ; } ) ; } } else if ( this . isCasExpression ( this . expression . value ) ) { assert ( this . variables . size === 0 , 'Expected no variables' ) ; this . executeCasExpression ( this . expression . value , gettersData , ( _ , resultingExpression ) => { this . expression . value = resultingExpression ; } ) ; } } else { } } } function clearResult ( expression ) { if ( Array . isArray ( expression ) ) { const functionName = expression [ 0 ] ; const output = expression . slice ( ) ; if ( functionName == 'Equal' ) { output [ 1 ] = clearResult ( expression [ 1 ] ) ; output [ 2 ] = [ '\\mathinner' , [ 'Missing' , '' ] ] ; } else if ( functionName == 'Evaluate' ) { output [ 1 ] = clearResult ( expression [ 1 ] ) ; output [ 3 ] = [ '\\mathinner' , [ 'Missing' , '' ] ] ; } else { for ( let i = 1 ; i < expression . length ; i ++ ) { output [ i ] = clearResult ( expression [ i ] ) ; } } return output ; } else { return expression ; } } /**
 * Variables that are being *written* to
 */ function getVariableNames ( expression ) { const variables = new Set < string > ( ) ; if ( Array . isArray ( expression ) && expression [ 0 ] == 'Assign' ) { if ( typeof expression [ 1 ] == 'string' ) { variables . add ( expression [ 1 ] ) ; } else { throw new Error ( 'Cannot assign to this ' + expression [ 1 ] ) ; } } return variables ; } export const ExpressionElementType = { typeName : ElementType , elementType : ExpressionElement , serializeElement : ( element ) => { const serializedElement = { id : element . id , typeName : ElementType , position : { x : element . position . value . x , y : element . position . value . y } , size : { x : element . size . value . x , y : element . size . value . y } , resizable : element . resizable . value , expression : element . expression . value , } ; return serializedElement ; } , deserializeElement : ( elementData ) => { const creationOptions = { id : elementData . id , position : new Vector2 ( elementData . position . x , elementData . position . y ) , size : new Vector2 ( elementData . size . x , elementData . size . y ) , resizable : elementData . resizable , } ; const element = new ExpressionElement ( creationOptions ) ; return { element : element , onAddedCallback : ( ) => { element . inputExpression ( elementData . expression ) ; } , } ; } , } ;