<commit_before>var app = require('http').createServer(),
    io = require('socket.io').listen(app),
    spawn = require('child_process').spawn,
    fs = require('fs'),
    s3 = require('./s3');

var generateUserid = function() {
    var id = (Math.random().toString(36)+'00000000000000000').slice(2, 8);
    if (connectedUsers[id] === undefined) return id;
    else return generateUserid();
};

var connectedUsers = {};

io.of('/ruby').on('connection', function(socket) {

    var ruby,
        userid,
        socketOn = false;

    socket.on('codeBuild', function(data) {
        console.log(data);

        var fileSaveError,
            codeLoadError;

        // Save file
        s3.uploadFile(userid + '.rb', data.fileContent, function(error) {
            fileSaveError = error;

            // Emit file saved
            socket.emit('buildStatus', {
                output: error || 'Code saved.'
            });

            // Check file syntax?

            // Kill old IRB
            if (ruby) {
              oldRuby = ruby;
              oldRuby.kill();
            }

            // Start IRB
            ruby = spawn('irb');

            ruby.stdout.setEncoding('utf8');
            ruby.stderr.setEncoding('utf8');

            // Funnel file into IRB
            socketOn = false;
            ruby.stdin.write(data.fileContent + '\n', function(error) {
                codeLoadError = error;

                // Emit code loaded
                socket.emit('buildStatus', {
                    output: error || 'Game loaded.'
                });

                if (!fileSaveError && !codeLoadError) {
                    socket.emit('buildStatus', {
                        output: 'Build successful!'
                    });
                } else {
                    socket.emit('terminalError', {
                        output: 'Build failed!'
                    });
                }
            });

            // Bind events on IRB
            ruby.stdout.on('data', function(data) {
                console.log(ruby.pid + ' STDOUT: ' + data);
                if (socketOn) {
                    console.log('socket on');
                    socket.emit('terminalOutput', {
                        output: data
                    });
                }
            });

            ruby.stderr.on('data', function(data) {
                console.log(ruby.pid + ' STDERR: ' + data);
                socket.emit('terminalError', {
                    output: data
                });
            });

            ruby.on('close', function(code) {
                console.log(ruby.pid + ' Exit code: ' + code);
            });

            ruby.on('exit', function(code) {
                console.log(ruby.pid + ' Exit code: ' + code);
                if (code === 0) {
                    socket.emit('terminalError', {
                        output: 'Ruby process exited.'
                    });
                }
            });

        });
    });

    socket.on('terminalInput', function(data) {
        console.log(data);
        socketOn = true;
        try {
            ruby.stdin.write(data.input + '\n');
        } catch (e) {
            socket.emit('terminalError', {
                output: 'Couldn\'t talk to ruby. Did you Build your game?'
            });
        }
    });

    socket.on('reportUserid', function(data) {
        var oldSocket = connectedUsers[data.userid];
        var newOrAuthenticatedUser = !oldSocket || oldSocket === data.authid;

        if (data.userid && newOrAuthenticatedUser) {
            userid = data.userid;
        } else userid = generateUserid();

        console.log('User ' + userid + ' connected on ' + socket.id);

        // Store socket id because XHR polling causes race conditions
        connectedUsers[userid] = socket.id;
        socket.emit('confirmUserid', {
            userid: userid,
            authid: socket.id
        });

        s3.getFile(userid + '.rb', function(error, contents) {
            if (error) {
                fs.readFile('games/game.rb', function(error, contents) {
                    socket.emit('ready', {
                        output: 'Ready!',
                        fileContent: contents.toString()
                    });
                });
            } else {
                socket.emit('ready', {
                    output: 'Ready! Welcome back.',
                    fileContent: contents.toString()
                });
            }
        });
    });

    socket.on('disconnect', function() {
        console.log('User ' + userid + ' disconnected from ' + socket.id);
        // Only set to false if user doesn't already have a new socket
        if (socket.id == connectedUsers[userid]) {
            connectedUsers[userid] = false;
            console.log('User ' + userid + ' disconnected completely');
        }
    });

});

app.listen(process.env.PORT || 8888);
<commit_msg>Hide function call echo stdout from irb, fixes #2<commit_after>var app = require('http').createServer(),
    io = require('socket.io').listen(app),
    spawn = require('child_process').spawn,
    fs = require('fs'),
    s3 = require('./s3');

var generateUserid = function() {
    var id = (Math.random().toString(36)+'00000000000000000').slice(2, 8);
    if (connectedUsers[id] === undefined) return id;
    else return generateUserid();
};

var connectedUsers = {};

io.of('/ruby').on('connection', function(socket) {

    var ruby,
        userid,
        socketOn = false;

    socket.on('codeBuild', function(data) {
        console.log(data);

        var fileSaveError,
            codeLoadError;

        // Save file
        s3.uploadFile(userid + '.rb', data.fileContent, function(error) {
            fileSaveError = error;

            // Emit file saved
            socket.emit('buildStatus', {
                output: error || 'Code saved.'
            });

            // Check file syntax?

            // Kill old IRB
            if (ruby) {
              oldRuby = ruby;
              oldRuby.kill();
            }

            // Start IRB
            ruby = spawn('irb');

            ruby.stdout.setEncoding('utf8');
            ruby.stderr.setEncoding('utf8');

            // Funnel file into IRB
            socketOn = false;
            ruby.stdin.write(data.fileContent + '\n', function(error) {
                codeLoadError = error;

                // Emit code loaded
                socket.emit('buildStatus', {
                    output: error || 'Game loaded.'
                });

                if (!fileSaveError && !codeLoadError) {
                    socket.emit('buildStatus', {
                        output: 'Build successful!'
                    });
                } else {
                    socket.emit('terminalError', {
                        output: 'Build failed!'
                    });
                }
            });

            // Bind events on IRB
            ruby.stdout.on('data', function(data) {
                console.log(ruby.pid + ' STDOUT: ' + data);
                if (socketOn) {
                    console.log('socket on');
                    socket.emit('terminalOutput', {
                        output: data.replace(/^(\b|\s)\w+\(\)(\s\n|\n)/, '')
                    });
                }
            });

            ruby.stderr.on('data', function(data) {
                console.log(ruby.pid + ' STDERR: ' + data);
                socket.emit('terminalError', {
                    output: data
                });
            });

            ruby.on('close', function(code) {
                console.log(ruby.pid + ' Exit code: ' + code);
            });

            ruby.on('exit', function(code) {
                console.log(ruby.pid + ' Exit code: ' + code);
                if (code === 0) {
                    socket.emit('terminalError', {
                        output: 'Ruby process exited.'
                    });
                }
            });

        });
    });

    socket.on('terminalInput', function(data) {
        console.log(data);
        socketOn = true;
        try {
            ruby.stdin.write(data.input + '\n');
        } catch (e) {
            socket.emit('terminalError', {
                output: 'Couldn\'t talk to ruby. Did you Build your game?'
            });
        }
    });

    socket.on('reportUserid', function(data) {
        var oldSocket = connectedUsers[data.userid];
        var newOrAuthenticatedUser = !oldSocket || oldSocket === data.authid;

        if (data.userid && newOrAuthenticatedUser) {
            userid = data.userid;
        } else userid = generateUserid();

        console.log('User ' + userid + ' connected on ' + socket.id);

        // Store socket id because XHR polling causes race conditions
        connectedUsers[userid] = socket.id;
        socket.emit('confirmUserid', {
            userid: userid,
            authid: socket.id
        });

        s3.getFile(userid + '.rb', function(error, contents) {
            if (error) {
                fs.readFile('games/game.rb', function(error, contents) {
                    socket.emit('ready', {
                        output: 'Ready!',
                        fileContent: contents.toString()
                    });
                });
            } else {
                socket.emit('ready', {
                    output: 'Ready! Welcome back.',
                    fileContent: contents.toString()
                });
            }
        });
    });

    socket.on('disconnect', function() {
        console.log('User ' + userid + ' disconnected from ' + socket.id);
        // Only set to false if user doesn't already have a new socket
        if (socket.id == connectedUsers[userid]) {
            connectedUsers[userid] = false;
            console.log('User ' + userid + ' disconnected completely');
        }
    });

});

app.listen(process.env.PORT || 8888);
<|endoftext|>


<commit_before>/**
 * PostSearcherAutoSuggest component.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * 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
 *
 *     https://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.
 */

/**
 * External dependencies
 */
import PropTypes from 'prop-types';
import {
	Combobox,
	ComboboxInput,
	ComboboxPopover,
	ComboboxList,
	ComboboxOption,
} from '@reach/combobox';

/**
 * WordPress dependencies
 */
import { useState, useEffect, useCallback, useRef } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { END, ENTER, ESCAPE, HOME } from '@wordpress/keycodes';

/**
 * Internal dependencies
 */
import API from 'googlesitekit-api';
import Data from 'googlesitekit-data';
import { useDebouncedState } from '../hooks/useDebouncedState';
import { useFeature } from '../hooks/useFeature';
import { CORE_SITE } from '../googlesitekit/datastore/site/constants';

const { useSelect } = Data;

export default function PostSearcherAutoSuggest( {
	id,
	match,
	setMatch,
	isLoading,
	showDropdown = true,
	setIsLoading,
	setIsActive = () => {},
	autoFocus,
	setCanSubmit = () => {},
	onClose = () => {},
	placeholder = '',
} ) {
	const [ searchTerm, setSearchTerm ] = useState( '' );

	// eslint-disable-next-line camelcase
	const postTitleFromMatch = match?.post_title;
	/**
	 * As a fix for #4562, we should hide the loading indicator
	 * after pressing enter/return key on a URL Entity Search Result.
	 *
	 * In the useEffect condition we check for:
	 * `debouncedValue !== postTitleFromMatch` to set the loading state to true.
	 * However, the `debouncedValue` is always delayed. Hence this condition is always true.
	 * Hence the loading state is to true.
	 * Therefore, we need to set the debounce delay/timer value to `0` if the
	 * `searchTerm === postTitleFromMatch` to not to set the loading state to true.
	 */
	const debouncedValue = useDebouncedState(
		searchTerm,
		searchTerm === postTitleFromMatch ? 0 : 200
	);

	const [ results, setResults ] = useState( [] );
	const noResultsMessage = __( 'No results found', 'google-site-kit' );

	const unifiedDashboardEnabled = useFeature( 'unifiedDashboard' );

	const currentEntityTitle = useSelect( ( select ) =>
		select( CORE_SITE ).getCurrentEntityTitle()
	);

	const postTitle = useRef( null );

	const onFocus = useCallback( () => {
		setIsActive( true );
	}, [ setIsActive ] );

	const onBlur = useCallback(
		( event ) => {
			if (
				! event.relatedTarget?.classList.contains(
					'autocomplete__option--result'
				)
			) {
				setIsActive( false );
				setSearchTerm( postTitle.current ?? currentEntityTitle ?? '' );
			}
		},
		[ currentEntityTitle, setIsActive ]
	);

	const onSelectCallback = useCallback(
		( value ) => {
			if ( Array.isArray( results ) && value !== noResultsMessage ) {
				const foundMatch = results.find(
					( post ) =>
						post.post_title.toLowerCase() === value.toLowerCase()
				);
				if ( foundMatch ) {
					postTitle.current = foundMatch.post_title;
					setCanSubmit( true );
					setMatch( foundMatch );
					setSearchTerm( foundMatch.post_title );
				} else {
					postTitle.current = null;
				}
			} else {
				postTitle.current = null;
				setCanSubmit( false );
			}
		},
		[ results, setCanSubmit, setMatch, noResultsMessage, setSearchTerm ]
	);

	const onInputChange = useCallback(
		( event ) => {
			setCanSubmit( false );
			setSearchTerm( event.target.value );
		},
		[ setCanSubmit ]
	);

	useEffect( () => {
		if (
			debouncedValue !== '' &&
			debouncedValue !== currentEntityTitle &&
			debouncedValue?.toLowerCase() !== postTitleFromMatch?.toLowerCase()
		) {
			setIsLoading?.( true );
			/**
			 * Create AbortController instance to pass
			 * the signal property to the API.get() method.
			 */
			const controller =
				typeof AbortController === 'undefined'
					? undefined
					: new AbortController();
			API.get(
				'core',
				'search',
				'post-search',
				{ query: encodeURIComponent( debouncedValue ) },
				{ useCache: false, signal: controller?.signal }
			)
				.then( ( res ) => setResults( res ) )
				.catch( () => setResults( [] ) )
				.finally( () => setIsLoading?.( false ) );

			// Clean-up abort
			return () => controller?.abort();
		}
	}, [
		debouncedValue,
		setIsLoading,
		currentEntityTitle,
		postTitleFromMatch,
	] );

	useEffect( () => {
		if ( ! searchTerm ) {
			setResults( [] );
		}
	}, [ searchTerm ] );

	useEffect( () => {
		if ( currentEntityTitle ) {
			setSearchTerm( currentEntityTitle );
		}
	}, [ currentEntityTitle ] );

	const inputRef = useRef();

	const onKeyDown = useCallback(
		( e ) => {
			if ( ! unifiedDashboardEnabled ) {
				return;
			}

			const input = inputRef.current;

			switch ( e.keyCode ) {
				case ESCAPE:
					return onClose();
				case ENTER:
					return onSelectCallback( searchTerm );
				case HOME:
					if ( input?.value ) {
						e.preventDefault();
						input.selectionStart = 0;
						input.selectionEnd = 0;
					}
					break;
				case END:
					if ( input?.value ) {
						e.preventDefault();
						input.selectionStart = input.value.length;
						input.selectionEnd = input.value.length;
					}
					break;
				default:
					break;
			}
		},
		[ onClose, onSelectCallback, searchTerm, unifiedDashboardEnabled ]
	);

	return (
		<Combobox
			className="autocomplete__wrapper"
			onSelect={ onSelectCallback }
		>
			<ComboboxInput
				ref={ inputRef }
				id={ id }
				className="autocomplete__input autocomplete__input--default"
				type="text"
				onBlur={ onBlur }
				onChange={ onInputChange }
				onFocus={ onFocus }
				placeholder={ placeholder }
				onKeyDown={ onKeyDown }
				value={ searchTerm }
				/* eslint-disable-next-line jsx-a11y/no-autofocus */
				autoFocus={ autoFocus }
			/>

			{ ( ! unifiedDashboardEnabled || ! isLoading ) &&
				showDropdown &&
				debouncedValue !== currentEntityTitle &&
				debouncedValue !== '' &&
				results.length === 0 && (
					<ComboboxPopover portal={ false }>
						<ComboboxList className="autocomplete__menu autocomplete__menu--inline">
							<ComboboxOption
								value={ noResultsMessage }
								className="autocomplete__option autocomplete__option--no-results"
							/>
						</ComboboxList>
					</ComboboxPopover>
				) }

			{ showDropdown &&
				debouncedValue !== '' &&
				debouncedValue !== currentEntityTitle &&
				results.length > 0 && (
					<ComboboxPopover portal={ false }>
						<ComboboxList className="autocomplete__menu autocomplete__menu--inline">
							{ results.map( ( { ID, post_title: title } ) => (
								<ComboboxOption
									key={ ID }
									value={ title }
									className="autocomplete__option autocomplete__option--result"
								/>
							) ) }
						</ComboboxList>
					</ComboboxPopover>
				) }
		</Combobox>
	);
}

PostSearcherAutoSuggest.propTypes = {
	id: PropTypes.string,
	match: PropTypes.object,
	setCanSubmit: PropTypes.func,
	setMatch: PropTypes.func,
	isLoading: PropTypes.bool,
	setIsLoading: PropTypes.func,
	onKeyDown: PropTypes.func,
	autoFocus: PropTypes.bool,
	placeholder: PropTypes.string,
};
<commit_msg>Preserve loading state until last fetch finishes.<commit_after>/**
 * PostSearcherAutoSuggest component.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * 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
 *
 *     https://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.
 */

/**
 * External dependencies
 */
import PropTypes from 'prop-types';
import {
	Combobox,
	ComboboxInput,
	ComboboxPopover,
	ComboboxList,
	ComboboxOption,
} from '@reach/combobox';

/**
 * WordPress dependencies
 */
import { useState, useEffect, useCallback, useRef } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { END, ENTER, ESCAPE, HOME } from '@wordpress/keycodes';

/**
 * Internal dependencies
 */
import API from 'googlesitekit-api';
import Data from 'googlesitekit-data';
import { useDebouncedState } from '../hooks/useDebouncedState';
import { useFeature } from '../hooks/useFeature';
import { CORE_SITE } from '../googlesitekit/datastore/site/constants';

const { useSelect } = Data;

export default function PostSearcherAutoSuggest( {
	id,
	match,
	setMatch,
	isLoading,
	showDropdown = true,
	setIsLoading,
	setIsActive = () => {},
	autoFocus,
	setCanSubmit = () => {},
	onClose = () => {},
	placeholder = '',
} ) {
	const lastfetchRequest = useRef();
	const [ searchTerm, setSearchTerm ] = useState( '' );

	// eslint-disable-next-line camelcase
	const postTitleFromMatch = match?.post_title;
	/**
	 * As a fix for #4562, we should hide the loading indicator
	 * after pressing enter/return key on a URL Entity Search Result.
	 *
	 * In the useEffect condition we check for:
	 * `debouncedValue !== postTitleFromMatch` to set the loading state to true.
	 * However, the `debouncedValue` is always delayed. Hence this condition is always true.
	 * Hence the loading state is to true.
	 * Therefore, we need to set the debounce delay/timer value to `0` if the
	 * `searchTerm === postTitleFromMatch` to not to set the loading state to true.
	 */
	const debouncedValue = useDebouncedState(
		searchTerm,
		searchTerm === postTitleFromMatch ? 0 : 200
	);

	const [ results, setResults ] = useState( [] );
	const noResultsMessage = __( 'No results found', 'google-site-kit' );

	const unifiedDashboardEnabled = useFeature( 'unifiedDashboard' );

	const currentEntityTitle = useSelect( ( select ) =>
		select( CORE_SITE ).getCurrentEntityTitle()
	);

	const postTitle = useRef( null );

	const onFocus = useCallback( () => {
		setIsActive( true );
	}, [ setIsActive ] );

	const onBlur = useCallback(
		( event ) => {
			if (
				! event.relatedTarget?.classList.contains(
					'autocomplete__option--result'
				)
			) {
				setIsActive( false );
				setSearchTerm( postTitle.current ?? currentEntityTitle ?? '' );
			}
		},
		[ currentEntityTitle, setIsActive ]
	);

	const onSelectCallback = useCallback(
		( value ) => {
			if ( Array.isArray( results ) && value !== noResultsMessage ) {
				const foundMatch = results.find(
					( post ) =>
						post.post_title.toLowerCase() === value.toLowerCase()
				);
				if ( foundMatch ) {
					postTitle.current = foundMatch.post_title;
					setCanSubmit( true );
					setMatch( foundMatch );
					setSearchTerm( foundMatch.post_title );
				} else {
					postTitle.current = null;
				}
			} else {
				postTitle.current = null;
				setCanSubmit( false );
			}
		},
		[ results, setCanSubmit, setMatch, noResultsMessage, setSearchTerm ]
	);

	const onInputChange = useCallback(
		( event ) => {
			setCanSubmit( false );
			setSearchTerm( event.target.value );
		},
		[ setCanSubmit ]
	);

	useEffect( () => {
		if (
			debouncedValue !== '' &&
			debouncedValue !== currentEntityTitle &&
			debouncedValue?.toLowerCase() !== postTitleFromMatch?.toLowerCase()
		) {
			setIsLoading?.( true );
			/**
			 * Create AbortController instance to pass
			 * the signal property to the API.get() method.
			 */
			const controller =
				typeof AbortController === 'undefined'
					? undefined
					: new AbortController();

			const fetchPromise = API.get(
				'core',
				'search',
				'post-search',
				{ query: encodeURIComponent( debouncedValue ) },
				{ useCache: false, signal: controller?.signal }
			)
				.then( ( res ) => setResults( res ) )
				.catch( () => setResults( null ) );

			lastfetchRequest.current = fetchPromise;
			fetchPromise.finally( () => {
				if ( fetchPromise === lastfetchRequest.current ) {
					setIsLoading?.( false );
				}
			} );

			// Clean-up abort
			return () => controller?.abort();
		}
	}, [
		debouncedValue,
		setIsLoading,
		currentEntityTitle,
		postTitleFromMatch,
	] );

	useEffect( () => {
		if ( ! searchTerm ) {
			setResults( [] );
		}
	}, [ searchTerm ] );

	useEffect( () => {
		if ( currentEntityTitle ) {
			setSearchTerm( currentEntityTitle );
		}
	}, [ currentEntityTitle ] );

	const inputRef = useRef();

	const onKeyDown = useCallback(
		( e ) => {
			if ( ! unifiedDashboardEnabled ) {
				return;
			}

			const input = inputRef.current;

			switch ( e.keyCode ) {
				case ESCAPE:
					return onClose();
				case ENTER:
					return onSelectCallback( searchTerm );
				case HOME:
					if ( input?.value ) {
						e.preventDefault();
						input.selectionStart = 0;
						input.selectionEnd = 0;
					}
					break;
				case END:
					if ( input?.value ) {
						e.preventDefault();
						input.selectionStart = input.value.length;
						input.selectionEnd = input.value.length;
					}
					break;
				default:
					break;
			}
		},
		[ onClose, onSelectCallback, searchTerm, unifiedDashboardEnabled ]
	);

	return (
		<Combobox
			className="autocomplete__wrapper"
			onSelect={ onSelectCallback }
		>
			<ComboboxInput
				ref={ inputRef }
				id={ id }
				className="autocomplete__input autocomplete__input--default"
				type="text"
				onBlur={ onBlur }
				onChange={ onInputChange }
				onFocus={ onFocus }
				placeholder={ placeholder }
				onKeyDown={ onKeyDown }
				value={ searchTerm }
				/* eslint-disable-next-line jsx-a11y/no-autofocus */
				autoFocus={ autoFocus }
			/>

			{ ( ! unifiedDashboardEnabled || ! isLoading ) &&
				showDropdown &&
				debouncedValue !== currentEntityTitle &&
				debouncedValue !== '' &&
				results?.length === 0 && (
					<ComboboxPopover portal={ false }>
						<ComboboxList className="autocomplete__menu autocomplete__menu--inline">
							<ComboboxOption
								value={ noResultsMessage }
								className="autocomplete__option autocomplete__option--no-results"
							/>
						</ComboboxList>
					</ComboboxPopover>
				) }

			{ showDropdown &&
				debouncedValue !== '' &&
				debouncedValue !== currentEntityTitle &&
				results?.length > 0 && (
					<ComboboxPopover portal={ false }>
						<ComboboxList className="autocomplete__menu autocomplete__menu--inline">
							{ results.map( ( { ID, post_title: title } ) => (
								<ComboboxOption
									key={ ID }
									value={ title }
									className="autocomplete__option autocomplete__option--result"
								/>
							) ) }
						</ComboboxList>
					</ComboboxPopover>
				) }
		</Combobox>
	);
}

PostSearcherAutoSuggest.propTypes = {
	id: PropTypes.string,
	match: PropTypes.object,
	setCanSubmit: PropTypes.func,
	setMatch: PropTypes.func,
	isLoading: PropTypes.bool,
	setIsLoading: PropTypes.func,
	onKeyDown: PropTypes.func,
	autoFocus: PropTypes.bool,
	placeholder: PropTypes.string,
};
<|endoftext|>


<commit_before>/* global TimelineMax, ScrollMagic, Power2, Bounce, $ */
'use strict';

$(function () {

  // init scroll magic
  var controller = new ScrollMagic.Controller({
    globalSceneOptions:{
      triggerHook:'onLeave'
    }
  });

  // get all section window
  var windows = $('section.window');
  var $anim = $('#anim');
  var $overlay = $('#anim-overlay');
  var $screens = $('.screen');
  var $widgets = $('.widget');
  var $widgetTooltips = $('.widget-intro');
  var $illusSync = $('.illus-sync');

  var $animWidget1 = $anim.find('.widget-1');
  var $animWidget2 = $anim.find('.widget-2');
  var $animWidget3 = $anim.find('.widget-3');
  var $animWidget4 = $anim.find('.widget-4');
  var $animWidget5 = $anim.find('.widget-5');
  var $animWidget6 = $anim.find('.widget-6');
  var $animWidget7 = $anim.find('.widget-7');

  // Intro
  var intro = new TimelineMax();
  intro
    .set($screens, {opacity:0})
    .set($anim, {opacity:0, scale:0})
    .set('#anim img', {opacity:0})
    .set($overlay, {opacity:0})
    .set('#anim-overlay img', {scale:0})
    .set($widgets, {opacity:0, scale:0})
    .to($anim, 2, {opacity:1, scale:1, rotationX:30, rotationZ:-6, ease:Power2.easeInOut})
    .to('#anim img', 1, {opacity:1}, '-=1')
    .to($animWidget1, 0.3, {scale:1 , opacity:1 })
    .to($animWidget2, 0.3, {scale:1 , opacity:1 })
    .to($animWidget3, 0.3, {scale:1 , opacity:1 })
    .to($animWidget4, 0.3, {scale:1 , opacity:1 }, '-=.2')
    .to($animWidget5, 0.3, {scale:1 , opacity:1 }, '-=.2')
    .to($animWidget6, 0.3, {scale:1 , opacity:1 }, '-=.2')
    .to($animWidget7, 0.3, {scale:1 , opacity:1 }, '-=.5')
    .to($overlay, 0.3, {opacity:1, ease:Power2.easeInOut }, '3.5')
    .to('#anim-overlay img', 0.4, {scale:1, ease:Bounce.easeInOut }, '3.6');

  // An array to stock our timelines, relative to each sections
  var tl = [];
  // First Section
  tl[0] = new TimelineMax();
  tl[0]
    .to($widgetTooltips, 2, {opacity:0})
    .to($widgets, 8, {z:400, opacity:0})
    .to($animWidget1, 8, {y:-350 }, '-=8')
    .to($animWidget3, 8, {x:-200, y:-50 }, '-=8')
    .to($animWidget4, 8, {x:-200 }, '-=8')
    .to($animWidget5, 8, {x:-200, y:50 }, '-=8')
    .to($animWidget6, 8, {x:-200, y:200 }, '-=8')
    .to($animWidget7, 8, {y:350 }, '-=8')
    .to($anim, 8, {opacity:1, scale:1, rotationX:0, rotationZ:0, ease:Power2.easeInOut}, '-=8')
    .to($illusSync, 4, {opacity:1}, '-=4')
    .to($overlay, 8, {backgroundColor:'#00B6BA' }, '0');

  // Second Section
  tl[1] = new TimelineMax();
  tl[1]
    .to($widgets, 0.2, {x:0, y:0, z:0, scale:0 })
    .to($illusSync, 3, {scale:0, opacity:0}, '+=3')
    .to($anim, 3, {scale:0.5 }, '-=3')
    .to('#anim img', 4, {opacity:0}, '-=5')
    .to('#anim > .widget', 2, {opacity:1, scale:1})
    .to('.screen-1', 3, {opacity:1})
    .to('.screen-1 .widget', 5, {opacity:1, scale:1},'-=2')
    .to('.screen-2', 3, {opacity:1})
    .to('.screen-2 .widget', 5, {opacity:1, scale:1},'-=2')
    .to('.screen-3', 3, {opacity:1})
    .to('.screen-3 .widget', 5, {opacity:1, scale:1},'-=2')
    .to($overlay, 8, {backgroundColor:'#F6624E' }, '0');

  // Third Section
  tl[2] = new TimelineMax();
  tl[2]
    .to($anim, 10, {scale:1, y:100, rotationX:20, rotationZ:-6, ease:Power2.easeInOut })
    .to($screens, 5, {opacity:0}, '-=8')
    .to($animWidget5, 4, {z:200})
    .to($animWidget5, 4, {rotationY:180, backgroundColor:'#674492'}, '-=2')
    .to($animWidget5, 4, {z:0})
    .to($overlay, 8, {backgroundColor:'#674492' }, '0');

  // Fourth Section
  tl[3] = new TimelineMax();
  tl[3]
    .to($anim, 12, {y:-400, rotationX:60}, '2s')
    .to($anim, 4, {opacity:0}, '5');

  // Create a scene for every window
  var scenes = [];
  for (var i=0; i<windows.length; i++) {

    scenes[i] = new ScrollMagic.Scene({
        triggerElement:windows[i],
        duration:800
      })
      .setTween(tl[i])
      .offset(20)
      .addIndicators() // add indicators (requires plugin)
      .addTo(controller);
  }

  scenes[0].on('start', function () {
    intro.seek(20);
  });

  scenes[3].on('leave', function () {
    $('body:after').addClass('hide');
  });

});

// Space Canvas

var canvas;
var context;
var screenH;
var screenW;
var stars = [];
var fps = 10;
var numStars = 200;

// Calculate the screen size
screenH = $(window).height();
screenW = $(window).width();

// Get the canvas
canvas = $('#space');

// Fill out the canvas
canvas.attr('height', screenH);
canvas.attr('width', screenW);
context = canvas[0].getContext('2d');

// Create all the stars
for (var i = 0; i < numStars; i++) {
  var x = Math.round(Math.random() * screenW);
  var y = Math.round(Math.random() * screenH);
  var len = 1 + Math.random() * 2;
  var opacity = Math.random();

  // Create a new star and draw
  var star = new Star(x, y, len, opacity);

  // Add the the stars array
  stars.push(star);
}

setInterval(animate, 1000 / fps);

// Animate the canvas
function animate() {
  context.clearRect(0, 0, screenW, screenH);
  $.each(stars, function() {
    this.draw(context);
  });
}

// Star
function Star(x, y, length, opacity) {
  this.x = parseInt(x);
  this.y = parseInt(y);
  this.length = parseInt(length);
  this.opacity = opacity;
  this.factor = 1;
  this.increment = Math.random() * 0.03;
}

// Draw a star
Star.prototype.draw = function() {
  context.rotate((Math.PI * 1 / 10));

  // Save the context
  context.save();

  // move into the middle of the canvas, just to make room
  context.translate(this.x, this.y);

  // Change the opacity
  if (this.opacity > 1) {
    this.factor = -1;
  } else if (this.opacity <= 0) {
    this.factor = 1;
    this.x = Math.round(Math.random() * screenW);
    this.y = Math.round(Math.random() * screenH);
  }

  this.opacity += this.increment * this.factor;

  context.beginPath();
  for (var i = 5; i--;) {
    context.lineTo(0, this.length);
    context.translate(0, this.length);
    context.rotate((Math.PI * 2 / 10));
    context.lineTo(0, -this.length);
    context.translate(0, -this.length);
    context.rotate(-(Math.PI * 6 / 10));
  }
  context.lineTo(0, this.length);
  context.closePath();
  context.fillStyle = 'rgba(200, 200, 250, ' + this.opacity + ')';
  // context.shadowBlur = 5;
  // context.shadowColor = '#1D96C7';
  context.fill();
  context.restore();
};
<commit_msg>fix(website): removed animation debug<commit_after>/* global TimelineMax, ScrollMagic, Power2, Bounce, $ */
'use strict';

$(function () {

  // init scroll magic
  var controller = new ScrollMagic.Controller({
    globalSceneOptions:{
      triggerHook:'onLeave'
    }
  });

  // get all section window
  var windows = $('section.window');
  var $anim = $('#anim');
  var $overlay = $('#anim-overlay');
  var $screens = $('.screen');
  var $widgets = $('.widget');
  var $widgetTooltips = $('.widget-intro');
  var $illusSync = $('.illus-sync');

  var $animWidget1 = $anim.find('.widget-1');
  var $animWidget2 = $anim.find('.widget-2');
  var $animWidget3 = $anim.find('.widget-3');
  var $animWidget4 = $anim.find('.widget-4');
  var $animWidget5 = $anim.find('.widget-5');
  var $animWidget6 = $anim.find('.widget-6');
  var $animWidget7 = $anim.find('.widget-7');

  // Intro
  var intro = new TimelineMax();
  intro
    .set($screens, {opacity:0})
    .set($anim, {opacity:0, scale:0})
    .set('#anim img', {opacity:0})
    .set($overlay, {opacity:0})
    .set('#anim-overlay img', {scale:0})
    .set($widgets, {opacity:0, scale:0})
    .to($anim, 2, {opacity:1, scale:1, rotationX:30, rotationZ:-6, ease:Power2.easeInOut})
    .to('#anim img', 1, {opacity:1}, '-=1')
    .to($animWidget1, 0.3, {scale:1 , opacity:1 })
    .to($animWidget2, 0.3, {scale:1 , opacity:1 })
    .to($animWidget3, 0.3, {scale:1 , opacity:1 })
    .to($animWidget4, 0.3, {scale:1 , opacity:1 }, '-=.2')
    .to($animWidget5, 0.3, {scale:1 , opacity:1 }, '-=.2')
    .to($animWidget6, 0.3, {scale:1 , opacity:1 }, '-=.2')
    .to($animWidget7, 0.3, {scale:1 , opacity:1 }, '-=.5')
    .to($overlay, 0.3, {opacity:1, ease:Power2.easeInOut }, '3.5')
    .to('#anim-overlay img', 0.4, {scale:1, ease:Bounce.easeInOut }, '3.6');

  // An array to stock our timelines, relative to each sections
  var tl = [];
  // First Section
  tl[0] = new TimelineMax();
  tl[0]
    .to($widgetTooltips, 2, {opacity:0})
    .to($widgets, 8, {z:400, opacity:0})
    .to($animWidget1, 8, {y:-350 }, '-=8')
    .to($animWidget3, 8, {x:-200, y:-50 }, '-=8')
    .to($animWidget4, 8, {x:-200 }, '-=8')
    .to($animWidget5, 8, {x:-200, y:50 }, '-=8')
    .to($animWidget6, 8, {x:-200, y:200 }, '-=8')
    .to($animWidget7, 8, {y:350 }, '-=8')
    .to($anim, 8, {opacity:1, scale:1, rotationX:0, rotationZ:0, ease:Power2.easeInOut}, '-=8')
    .to($illusSync, 4, {opacity:1}, '-=4')
    .to($overlay, 8, {backgroundColor:'#00B6BA' }, '0');

  // Second Section
  tl[1] = new TimelineMax();
  tl[1]
    .to($widgets, 0.2, {x:0, y:0, z:0, scale:0 })
    .to($illusSync, 3, {scale:0, opacity:0}, '+=3')
    .to($anim, 3, {scale:0.5 }, '-=3')
    .to('#anim img', 4, {opacity:0}, '-=5')
    .to('#anim > .widget', 2, {opacity:1, scale:1})
    .to('.screen-1', 3, {opacity:1})
    .to('.screen-1 .widget', 5, {opacity:1, scale:1},'-=2')
    .to('.screen-2', 3, {opacity:1})
    .to('.screen-2 .widget', 5, {opacity:1, scale:1},'-=2')
    .to('.screen-3', 3, {opacity:1})
    .to('.screen-3 .widget', 5, {opacity:1, scale:1},'-=2')
    .to($overlay, 8, {backgroundColor:'#F6624E' }, '0');

  // Third Section
  tl[2] = new TimelineMax();
  tl[2]
    .to($anim, 10, {scale:1, y:100, rotationX:20, rotationZ:-6, ease:Power2.easeInOut })
    .to($screens, 5, {opacity:0}, '-=8')
    .to($animWidget5, 4, {z:200})
    .to($animWidget5, 4, {rotationY:180, backgroundColor:'#674492'}, '-=2')
    .to($animWidget5, 4, {z:0})
    .to($overlay, 8, {backgroundColor:'#674492' }, '0');

  // Fourth Section
  tl[3] = new TimelineMax();
  tl[3]
    .to($anim, 12, {y:-400, rotationX:60}, '2s')
    .to($anim, 4, {opacity:0}, '5');

  // Create a scene for every window
  var scenes = [];
  for (var i=0; i<windows.length; i++) {

    scenes[i] = new ScrollMagic.Scene({
        triggerElement:windows[i],
        duration:800
      })
      .setTween(tl[i])
      .offset(20)
      // .addIndicators() // add indicators (requires plugin)
      .addTo(controller);
  }

  scenes[0].on('start', function () {
    intro.seek(20);
  });

  scenes[3].on('leave', function () {
    $('body:after').addClass('hide');
  });

});

// Space Canvas

var canvas;
var context;
var screenH;
var screenW;
var stars = [];
var fps = 10;
var numStars = 200;

// Calculate the screen size
screenH = $(window).height();
screenW = $(window).width();

// Get the canvas
canvas = $('#space');

// Fill out the canvas
canvas.attr('height', screenH);
canvas.attr('width', screenW);
context = canvas[0].getContext('2d');

// Create all the stars
for (var i = 0; i < numStars; i++) {
  var x = Math.round(Math.random() * screenW);
  var y = Math.round(Math.random() * screenH);
  var len = 1 + Math.random() * 2;
  var opacity = Math.random();

  // Create a new star and draw
  var star = new Star(x, y, len, opacity);

  // Add the the stars array
  stars.push(star);
}

setInterval(animate, 1000 / fps);

// Animate the canvas
function animate() {
  context.clearRect(0, 0, screenW, screenH);
  $.each(stars, function() {
    this.draw(context);
  });
}

// Star
function Star(x, y, length, opacity) {
  this.x = parseInt(x);
  this.y = parseInt(y);
  this.length = parseInt(length);
  this.opacity = opacity;
  this.factor = 1;
  this.increment = Math.random() * 0.03;
}

// Draw a star
Star.prototype.draw = function() {
  context.rotate((Math.PI * 1 / 10));

  // Save the context
  context.save();

  // move into the middle of the canvas, just to make room
  context.translate(this.x, this.y);

  // Change the opacity
  if (this.opacity > 1) {
    this.factor = -1;
  } else if (this.opacity <= 0) {
    this.factor = 1;
    this.x = Math.round(Math.random() * screenW);
    this.y = Math.round(Math.random() * screenH);
  }

  this.opacity += this.increment * this.factor;

  context.beginPath();
  for (var i = 5; i--;) {
    context.lineTo(0, this.length);
    context.translate(0, this.length);
    context.rotate((Math.PI * 2 / 10));
    context.lineTo(0, -this.length);
    context.translate(0, -this.length);
    context.rotate(-(Math.PI * 6 / 10));
  }
  context.lineTo(0, this.length);
  context.closePath();
  context.fillStyle = 'rgba(200, 200, 250, ' + this.opacity + ')';
  // context.shadowBlur = 5;
  // context.shadowColor = '#1D96C7';
  context.fill();
  context.restore();
};
<|endoftext|>


<commit_before>const http = require('http');
const https = require('https');
const fs = require('fs');
const path = require('path');
const express = require('express');
const helmet = require('helmet');
const bodyParser = require('body-parser');
const crypto = require('crypto');

const config = require('./config.server.js');

const buildDir = 'dist';

const app = express();

const nonceMiddleware = (req, res, next) => {
  // Cache static html file to avoid reading it from the filesystem on each request
  if (!global.html) {
    console.log('Caching static HTML');
    global.html = fs.readFileSync(`${staticDir}/index.html`, 'utf8');
  }

  // Set a unique nonce for each request
  res.locals.nonce = crypto.randomBytes(16).toString('base64');
  res.locals.htmlWithNonces = global.html.replace(/<(script)/g, `<$1 nonce="${res.locals.nonce}"`);
  next();
}

app.use(helmet());
app.use(nonceMiddleware, helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'none'"],
    baseUri: ['https://docs.helpscout.net'],
    scriptSrc: [
      "'self'",
      "'strict-dynamic'",
      (req, res) => {
        return `'nonce-${res.locals.nonce}'`;
      },
      'https://beacon-v2.helpscout.net',
      'https://d12wqas9hcki3z.cloudfront.net',
      'https://d33v4339jhl8k0.cloudfront.net',
    ],
    styleSrc: [
      "'self'",
      "'unsafe-inline'",
      'https://fonts.googleapis.com',
      'https://beacon-v2.helpscout.net',
      'https://djtflbt20bdde.cloudfront.net',
    ],
    imgSrc: [
      "'self'",
      'data:',
      'https://d33v4339jhl8k0.cloudfront.net',
      'https://*.gravatar.com',
    ],
    fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com'],
    reportUri: '/event/csp-report/violation',
    objectSrc: ['blob:', 'https://beacon-v2.helpscout.net'],
    workerSrc: ["'self'", 'blob:'],
    childSrc: ["'self'", 'blob:', 'https://docs.google.com/forms'],
    frameSrc: ['https://beacon-v2.helpscout.net', 'https://docs.google.com/forms'],
    connectSrc: [].concat([
      process.env.API_HOST,
      'https://api.github.com/repos/tidepool-org/chrome-uploader/releases',
      'https://beaconapi.helpscout.net',
      'https://chatapi.helpscout.net',
      'https://d3hb14vkzrxvla.cloudfront.net',
      'wss\://*.pusher.com',
      '*.sumologic.com',
      'sentry.io',
    ]),
  },
  reportOnly: false,
}));

app.use(bodyParser.json({
  type: ['json', 'application/csp-report']
}))

const staticDir = path.join(__dirname, buildDir);
app.use(express.static(staticDir, { index: false }));

//So that we can use react-router and browser history
app.get('*', (req, res) => {
  res.send(res.locals.htmlWithNonces);
});

app.post('/event/csp-report/violation', (req, res) => {
  if (req.body) {
    console.log('CSP Violation: ', req.body);
  } else {
    console.log('CSP Violation: No data received!');
  }
  res.status(204).end();
})

// If no ports specified, just start on default HTTP port
if (!(config.httpPort || config.httpsPort)) {
  config.httpPort = 3000;
}

if (config.httpPort) {
  app.server = http.createServer(app).listen(config.httpPort, () => {
    console.log('Connect server started on port', config.httpPort);
    console.log('Serving static directory "' + staticDir + '/"');
  });
}

if (config.httpsPort) {
  https.createServer(config.httpsConfig, app).listen(config.httpsPort, () => {
    console.log('Connect server started on HTTPS port', config.httpsPort);
    console.log('Serving static directory "' + staticDir + '/"');
  });
}

if (config.discovery && config.publishHost) {
  const hakken = require('hakken')(config.discovery).client();
  hakken.start();

  const serviceDescriptor = {service: config.serviceName};

  if (config.httpsPort) {
    serviceDescriptor.host = config.publishHost + ':' + config.httpsPort;
    serviceDescriptor.protocol = 'https';
  }
  else if (config.httpPort) {
    serviceDescriptor.host = config.publishHost + ':' + config.httpPort;
    serviceDescriptor.protocol = 'http';
  }

  console.log('Publishing to service discovery: ', serviceDescriptor);
  hakken.publish(serviceDescriptor);
}

module.exports = app;
<commit_msg>expand docs csp exception<commit_after>const http = require('http');
const https = require('https');
const fs = require('fs');
const path = require('path');
const express = require('express');
const helmet = require('helmet');
const bodyParser = require('body-parser');
const crypto = require('crypto');

const config = require('./config.server.js');

const buildDir = 'dist';

const app = express();

const nonceMiddleware = (req, res, next) => {
  // Cache static html file to avoid reading it from the filesystem on each request
  if (!global.html) {
    console.log('Caching static HTML');
    global.html = fs.readFileSync(`${staticDir}/index.html`, 'utf8');
  }

  // Set a unique nonce for each request
  res.locals.nonce = crypto.randomBytes(16).toString('base64');
  res.locals.htmlWithNonces = global.html.replace(/<(script)/g, `<$1 nonce="${res.locals.nonce}"`);
  next();
}

app.use(helmet());
app.use(nonceMiddleware, helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'none'"],
    baseUri: ['https://docs.helpscout.net'],
    scriptSrc: [
      "'self'",
      "'strict-dynamic'",
      (req, res) => {
        return `'nonce-${res.locals.nonce}'`;
      },
      'https://beacon-v2.helpscout.net',
      'https://d12wqas9hcki3z.cloudfront.net',
      'https://d33v4339jhl8k0.cloudfront.net',
    ],
    styleSrc: [
      "'self'",
      "'unsafe-inline'",
      'https://fonts.googleapis.com',
      'https://beacon-v2.helpscout.net',
      'https://djtflbt20bdde.cloudfront.net',
    ],
    imgSrc: [
      "'self'",
      'data:',
      'https://d33v4339jhl8k0.cloudfront.net',
      'https://*.gravatar.com',
    ],
    fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com'],
    reportUri: '/event/csp-report/violation',
    objectSrc: ['blob:', 'https://beacon-v2.helpscout.net'],
    workerSrc: ["'self'", 'blob:'],
    childSrc: ["'self'", 'blob:', 'https://docs.google.com'],
    frameSrc: ['https://beacon-v2.helpscout.net', 'https://docs.google.com'],
    connectSrc: [].concat([
      process.env.API_HOST,
      'https://api.github.com/repos/tidepool-org/chrome-uploader/releases',
      'https://beaconapi.helpscout.net',
      'https://chatapi.helpscout.net',
      'https://d3hb14vkzrxvla.cloudfront.net',
      'wss\://*.pusher.com',
      '*.sumologic.com',
      'sentry.io',
    ]),
  },
  reportOnly: false,
}));

app.use(bodyParser.json({
  type: ['json', 'application/csp-report']
}))

const staticDir = path.join(__dirname, buildDir);
app.use(express.static(staticDir, { index: false }));

//So that we can use react-router and browser history
app.get('*', (req, res) => {
  res.send(res.locals.htmlWithNonces);
});

app.post('/event/csp-report/violation', (req, res) => {
  if (req.body) {
    console.log('CSP Violation: ', req.body);
  } else {
    console.log('CSP Violation: No data received!');
  }
  res.status(204).end();
})

// If no ports specified, just start on default HTTP port
if (!(config.httpPort || config.httpsPort)) {
  config.httpPort = 3000;
}

if (config.httpPort) {
  app.server = http.createServer(app).listen(config.httpPort, () => {
    console.log('Connect server started on port', config.httpPort);
    console.log('Serving static directory "' + staticDir + '/"');
  });
}

if (config.httpsPort) {
  https.createServer(config.httpsConfig, app).listen(config.httpsPort, () => {
    console.log('Connect server started on HTTPS port', config.httpsPort);
    console.log('Serving static directory "' + staticDir + '/"');
  });
}

if (config.discovery && config.publishHost) {
  const hakken = require('hakken')(config.discovery).client();
  hakken.start();

  const serviceDescriptor = {service: config.serviceName};

  if (config.httpsPort) {
    serviceDescriptor.host = config.publishHost + ':' + config.httpsPort;
    serviceDescriptor.protocol = 'https';
  }
  else if (config.httpPort) {
    serviceDescriptor.host = config.publishHost + ':' + config.httpPort;
    serviceDescriptor.protocol = 'http';
  }

  console.log('Publishing to service discovery: ', serviceDescriptor);
  hakken.publish(serviceDescriptor);
}

module.exports = app;
<|endoftext|>


<commit_before>import * as THREE from 'three';
import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { MeshBVH, ExtendedTriangle } from '..';
import { generateEdges, lineIntersectTrianglePoint, getTriYAtPoint } from './utils/edgeUtils.js';

const params = {

};

let renderer, camera, scene, model, clock, gui, helper, controls;
let bvh;

init();
render();

function init() {

	const bgColor = 0xeeeeee;

	// renderer setup
	renderer = new THREE.WebGLRenderer( { antialias: true } );
	renderer.setPixelRatio( window.devicePixelRatio );
	renderer.setSize( window.innerWidth, window.innerHeight );
	renderer.setClearColor( bgColor, 1 );
	renderer.outputEncoding = THREE.sRGBEncoding;
	document.body.appendChild( renderer.domElement );

	// scene setup
	scene = new THREE.Scene();
	scene.fog = new THREE.Fog( 0xffca28, 20, 60 );

	const light = new THREE.DirectionalLight( 0xffffff, 1 );
	light.position.set( 1, 1, 1 );
	scene.add( light );
	scene.add( new THREE.AmbientLight( 0xb0bec5, 0.8 ) );

	// model = new THREE.Mesh( new THREE.TorusKnotBufferGeometry( undefined, undefined, 15, 5 ) );
	// model = new THREE.Mesh( new THREE.RingGeometry( undefined, undefined, 20 ) );
	model = new THREE.Mesh( new THREE.BoxBufferGeometry( 1, 1, 1, 5, 5, 5 ), new THREE.MeshStandardMaterial( { side: 2 }) );
	model.geometry.rotateX( - Math.PI / 3 ).rotateZ( - Math.PI / 3 );
	model.geometry.clearGroups();
	// model.geometry.index = new THREE.BufferAttribute( new Uint32Array( [  14, 15, 13, 16, 18, 17, 18, 19, 17 ] ), 1 );
	// model.geometry = model.geometry.toNonIndexed();
	console.log( model.geometry.attributes.position.array )
	scene.add( model );

// 0, 2, 1, 2, 3, 1,
// 4, 6, 5, 6, 7, 5,
// 8, 10, 9, 10, 11, 9,
// 12, 14, 13, 14, 15, 13
// 22,23,21,20,22,21
// 16, 18, 17, 18, 19, 17

	console.log( model )

	bvh = new MeshBVH( model.geometry );

	// camera setup
	camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 50 );
	// camera = new THREE.OrthographicCamera( - 10, 10, 10, - 10 );
	camera.position.set( 0, 0, 4 );
	camera.far = 100;
	camera.updateProjectionMatrix();

	clock = new THREE.Clock();

	controls = new OrbitControls( camera, renderer.domElement );

	gui = new GUI();

	updateEdges();

	window.addEventListener( 'resize', function () {

		camera.aspect = window.innerWidth / window.innerHeight;
		camera.updateProjectionMatrix();

		renderer.setSize( window.innerWidth, window.innerHeight );

	}, false );

	scene.add( new THREE.AxesHelper ())

}

function updateEdges() {

	// // const a = new THREE.Vector3( - 1, 0, - 1 );
	// // const b = new THREE.Vector3( 1, 0, - 1 );
	// // const c = new THREE.Vector3( 0, 0, 1 );

	// // const a = new THREE.Vector3( -0.40849366784095764, 0, -0.6830127239227295 );
	// // const b = new THREE.Vector3( -0.8415063619613647, 0, 0.1830127090215683 );
	// // const c = new THREE.Vector3( 0.34150636196136475, 0, -0.1830127090215683 );

	// const a = new THREE.Vector3( -0.09150634706020355, 0, 0.6830127239227295 );
	// const b = new THREE.Vector3( 0.40849366784095764, 0, 0.6830127239227295 );
	// const c = new THREE.Vector3( 0.8415063619613647, 0, -0.1830127090215683 );

	// const l = new THREE.Line3();
	// l.start.set( 2, - 1, - 2.5 );
	// l.end.set( - 2, - 1, 0.5 );

	// l.start.set( -0.34150636196136475, 0, 0.1830127090215683 );
	// l.end.set( 0.40849042024572324, 0, 0.6830105588592614 );
	// l.start.y = 0;
	// l.end.y = 0;

	// const edgeArray = [
	// 	a.x, a.y, a.z,
	// 	b.x, b.y, b.z,

	// 	a.x, a.y, a.z,
	// 	c.x, c.y, c.z,

	// 	b.x, b.y, b.z,
	// 	c.x, c.y, c.z,

	// 	l.start.x, l.start.y, l.start.z,
	// 	l.end.x, l.end.y, l.end.z,
	// ];

	// const tri = new ExtendedTriangle( a, b, c );
	// tri.needsUpdate = true;

	// // debugger
	// const target = lineIntersectTrianglePoint( l, tri );
	// edgeArray.push(
	// 	target.line.start.x, 1, target.line.start.z,
	// 	target.line.end.x, 1, target.line.end.z,
	// );

	// const tempVec0 = new THREE.Vector3();
	// const tempVec1 = new THREE.Vector3();
	// const tempDir = new THREE.Vector3();

	// // find the overlap by using directions and dot products
	// l.delta( tempDir );
	// tempVec0.subVectors( target.line.start, l.start );
	// tempVec1.subVectors( target.line.end, l.start );

	// const d0 = tempVec0.length() / tempDir.length();
	// const d1 = tempVec1.length() / tempDir.length();

	// console.log( d0, d1 );

	// tempVec0.lerpVectors( l.start, l.end, d0 );
	// edgeArray.push( tempVec0.x, 2, tempVec0.z );

	// tempVec0.lerpVectors( l.start, l.end, d1 );
	// edgeArray.push( tempVec0.x, 2, tempVec0.z );

	// console.log( d0, d1 );

	// const edgeGeom = new THREE.BufferGeometry();
	// const edgeBuffer = new THREE.BufferAttribute( new Float32Array( edgeArray ), 3, true );
	// edgeGeom.setAttribute( 'position', edgeBuffer );
	// scene.add( new THREE.LineSegments( edgeGeom, new THREE.LineBasicMaterial( { color: 0 } ) ) );

	const edges = generateEdges( model.geometry, new THREE.Vector3( 0, 1, 0 ), 89 );
	const finalEdges = [];
	const tempLine = new THREE.Line3();
	const tempRay = new THREE.Ray();
	const tempVec = new THREE.Vector3();
	const tempVec0 = new THREE.Vector3();
	const tempVec1 = new THREE.Vector3();
	const tempDir = new THREE.Vector3();
	let target = {
		line: new THREE.Line3(),
		point: new THREE.Vector3(),
		planeHit: new THREE.Vector3(),
		type: '',
	};

	// TODO: iterate over all edges and check visibility upwards using BVH
	for ( let i = 0, l = edges.length; i < l; i ++ ) {

		const line = edges[ i ];
		line.start.y += 1e-5;
		line.end.y += 1e-5;

		const lowestLineY = Math.min( line.start.y, line.end.y );
		const highestLineY = Math.max( line.start.y, line.end.y );
		const overlaps = [];
		let tris = 0;
		bvh.shapecast( {

			intersectsBounds: box => {

				return true;

				// check if the box bounds are above the lowest line point
				box.min.y = Math.min( lowestLineY, box.min.y );
				tempRay.origin.copy( line.start );
				line.delta( tempRay.direction );

				tempRay.intersectsBox( box, tempVec );
				return line.start.distanceToSquared( tempVec ) < line.distanceSq();

			},

			intersectsTriangle: tri => {

				// skip the triangle if it is completely below the line
				const highestTriangleY = Math.max( tri.a.y, tri.b.y, tri.c.y );

				if ( highestTriangleY < lowestLineY ) {

					return false;

				}

				// if this is the same line as on the triangle then skip it
				const triPoints = tri.points;
				for ( let i = 0; i < 3; i ++ ) {

					const ni = ( i + 1 ) % 3;

					const t0 = triPoints[ i ];
					const t1 = triPoints[ ni ];

					if (
						line.start.distanceTo( t0 ) < 1e-10 && line.end.distanceTo( t1 ) < 1e-10 ||
						line.start.distanceTo( t1 ) < 1e-10 && line.end.distanceTo( t0 ) < 1e-10
					) {

						return false;

					}

				}

				tempLine.copy( line );
				if ( lineIntersectTrianglePoint( tempLine, tri, target ) && target.type === 'point' ) {

					const point = new THREE.Vector3();
					const p = new THREE.Vector3();

					let testPoint;
					let flipped = false;
					if ( tempLine.start.distanceTo( target.point ) > tempLine.end.distanceTo( target.point ) ) {

						testPoint = tempLine.start;

					} else {

						testPoint = tempLine.end;
						flipped = true;

					}

					point.lerpVectors( testPoint, target.point, 0.5 );
					getTriYAtPoint( tri, point, p );

					console.log( testPoint.distanceTo( target.point ) );

					// console.log( p.y < point.y, target.point.distanceTo( tempLine.start ), target.point.distanceTo( tempLine.end ) )
					if ( p.y < point.y ) {

						if ( flipped ) tempLine.end.copy( target.point );
						else tempLine.start.copy( target.point );

					} else {

						if ( flipped ) tempLine.start.copy( target.point );
						else tempLine.end.copy( target.point );

					}

					// debugger
					console.log( target )

					// console.log( p.y < point.y, target.point.distanceTo( tempLine.start ), target.point.distanceTo( tempLine.end ) )

					// console.log( tempLine.distance() )

					// tempLine.delta( tempDir );

					// // shorten the edge to check to the line the falls below the triangle
					// if ( Math.sign( tri.plane.normal.dot( tempDir ) ) > 0 ) {

					// 	tempLine.start.copy( target.point );

					// } else {

					// 	tempLine.end.copy( target.point );

					// }

					// console.log( tempLine );

				} else {

					const point = new THREE.Vector3();
					const p = new THREE.Vector3();

					point.lerpVectors( tempLine.start, tempLine.end, 0.5 );
					getTriYAtPoint( tri, point, p );

					if ( p.y < point.y ) {

						return false;

					}

				}

				// flatten them to a common plane
				tempLine.start.y = 0;
				tempLine.end.y = 0;
				tri.a.y = 0;
				tri.b.y = 0;
				tri.c.y = 0;
				tri.needsUpdate = true;

				if ( lineIntersectTrianglePoint( tempLine, tri, target ) && target.type === 'line' ) {

					// TODO: we need to make sure the d0 and 1 are always small -> large
					// find the overlap by using directions and dot products
					tempLine.delta( tempDir );
					tempVec0.subVectors( target.line.start, tempLine.start );
					tempVec1.subVectors( target.line.end, tempLine.start );

					const d0 = tempVec0.length() / tempDir.length();
					const d1 = tempVec1.length() / tempDir.length();

					if ( ! ( Math.abs( d0 - d1 ) < 1e-10 ) ) {

						overlaps.push( [ d0, d1 ] );

					}

				}

				return false;

			},

		} );

		console.log( overlaps )

		overlaps.sort( ( a, b ) => {

			return a[ 0 ] - b[ 0 ];

		} );

		for ( let i = 1; i < overlaps.length; i ++ ) {

			const overlap = overlaps[ i ];
			const prevOverlap = overlaps[ i - 1 ];

			if ( overlap[ 0 ] <= prevOverlap[ 1 ] ) {

				prevOverlap[ 1 ] = Math.max( prevOverlap[ 1 ], overlap[ 1 ] );
				overlaps.splice( i, 1 );
				i --;
				continue;

			}

		}

		const invOverlaps = [[ 0, 1 ]];
		for ( let i = 0, l = overlaps.length; i < l; i ++ ) {

			invOverlaps[ i ][ 1 ] = overlaps[ i ][ 0 ];
			invOverlaps.push( [ overlaps[ i ][ 1 ], 1 ] );

		}

		for ( let i = 0, l = invOverlaps.length; i < l; i ++ ) {

			const newLine = new THREE.Line3();
			newLine.start.lerpVectors( line.start, line.end, invOverlaps[ i ][ 0 ] );
			newLine.end.lerpVectors( line.start, line.end, invOverlaps[ i ][ 1 ] );
			finalEdges.push( newLine );

		}

	}

	const edgeArray = [];
	finalEdges.forEach( l => {

		edgeArray.push( l.start.x, - 2, l.start.z );
		edgeArray.push( l.end.x, - 2, l.end.z );

	} );

	const edgeGeom = new THREE.BufferGeometry();
	const edgeBuffer = new THREE.BufferAttribute( new Float32Array( edgeArray ), 3, true );
	edgeGeom.setAttribute( 'position', edgeBuffer );
	scene.add( new THREE.LineSegments( edgeGeom, new THREE.LineBasicMaterial( { color: 0 } ) ) );

}

function render() {

	requestAnimationFrame( render );

	if ( helper ) {

		helper.visible = params.displayHelper;

	}

	renderer.render( scene, camera );

}
<commit_msg>start debugging<commit_after>import * as THREE from 'three';
import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { MeshBVH, ExtendedTriangle } from '..';
import { generateEdges, lineIntersectTrianglePoint, getTriYAtPoint } from './utils/edgeUtils.js';

const params = {

};

let renderer, camera, scene, model, clock, gui, helper, controls;
let bvh;

init();
render();

function init() {

	const bgColor = 0xeeeeee;

	// renderer setup
	renderer = new THREE.WebGLRenderer( { antialias: true } );
	renderer.setPixelRatio( window.devicePixelRatio );
	renderer.setSize( window.innerWidth, window.innerHeight );
	renderer.setClearColor( bgColor, 1 );
	renderer.outputEncoding = THREE.sRGBEncoding;
	document.body.appendChild( renderer.domElement );

	// scene setup
	scene = new THREE.Scene();
	scene.fog = new THREE.Fog( 0xffca28, 20, 60 );

	const light = new THREE.DirectionalLight( 0xffffff, 1 );
	light.position.set( 1, 1, 1 );
	scene.add( light );
	scene.add( new THREE.AmbientLight( 0xb0bec5, 0.8 ) );

	// model = new THREE.Mesh( new THREE.TorusKnotBufferGeometry( undefined, undefined, 15, 5 ) );
	// model = new THREE.Mesh( new THREE.RingGeometry( undefined, undefined, 20 ) );
	model = new THREE.Mesh( new THREE.BoxBufferGeometry( 1, 1, 1, 5, 5, 5 ), new THREE.MeshStandardMaterial( { side: 2 }) );
	model.geometry.rotateX( - Math.PI / 3 ).rotateZ( - Math.PI / 3 );
	model.geometry.clearGroups();
	// model.geometry.index = new THREE.BufferAttribute( new Uint32Array( [  14, 15, 13, 16, 18, 17, 18, 19, 17 ] ), 1 );
	// model.geometry = model.geometry.toNonIndexed();
	console.log( model.geometry.attributes.position.array )
	// scene.add( model );

// 0, 2, 1, 2, 3, 1,
// 4, 6, 5, 6, 7, 5,
// 8, 10, 9, 10, 11, 9,
// 12, 14, 13, 14, 15, 13
// 22,23,21,20,22,21
// 16, 18, 17, 18, 19, 17

	console.log( model )

	bvh = new MeshBVH( model.geometry );

	// camera setup
	camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 50 );
	// camera = new THREE.OrthographicCamera( - 10, 10, 10, - 10 );
	camera.position.set( 0, 0, 4 );
	camera.far = 100;
	camera.updateProjectionMatrix();

	clock = new THREE.Clock();

	controls = new OrbitControls( camera, renderer.domElement );

	gui = new GUI();

	updateEdges();

	window.addEventListener( 'resize', function () {

		camera.aspect = window.innerWidth / window.innerHeight;
		camera.updateProjectionMatrix();

		renderer.setSize( window.innerWidth, window.innerHeight );

	}, false );

	scene.add( new THREE.AxesHelper ())

}

function updateEdges() {

	const a = new THREE.Vector3( - 1, 1, - 1 );
	const b = new THREE.Vector3( 1, 0, - 1 );
	const c = new THREE.Vector3( 0, - 1, 1 );
	const tri = new ExtendedTriangle( a, b, c );
	tri.needsUpdate = true;

	const l = new THREE.Line3();
	l.end.set( 0.5, 2, - .5 );
	l.start.set( 0.5, - 2, .5 );

	const res = trimToBeneathTriPlane( tri, l, l );
	console.log( res );

	const edgeArray = [
		a.x, a.y, a.z,
		b.x, b.y, b.z,

		a.x, a.y, a.z,
		c.x, c.y, c.z,

		b.x, b.y, b.z,
		c.x, c.y, c.z,

		l.start.x, l.start.y, l.start.z,
		l.end.x, l.end.y, l.end.z,
	];

	const edgeGeom = new THREE.BufferGeometry();
	const edgeBuffer = new THREE.BufferAttribute( new Float32Array( edgeArray ), 3, true );
	edgeGeom.setAttribute( 'position', edgeBuffer );
	scene.add( new THREE.LineSegments( edgeGeom, new THREE.LineBasicMaterial( { color: 0 } ) ) );

	// TODO: write a simple test function for triangle / line functions
	// Algorithm to find overlaps:
	// - See if the edge intersects the triangle (use three.js ray function?)
	// - if it does not, check if the line is above or below the triangle
	//    - if it's above the tri then do nothing (no overlaps)
	// - Otherwise find the overlap segments and add the ranges to the array by trimming a flattened version of both

	// const edges = generateEdges( model.geometry, new THREE.Vector3( 0, 1, 0 ), 89 );
	// const finalEdges = [];
	// const tempLine = new THREE.Line3();
	// const tempRay = new THREE.Ray();
	// const tempVec = new THREE.Vector3();
	// const tempVec0 = new THREE.Vector3();
	// const tempVec1 = new THREE.Vector3();
	// const tempDir = new THREE.Vector3();
	// let target = {
	// 	line: new THREE.Line3(),
	// 	point: new THREE.Vector3(),
	// 	planeHit: new THREE.Vector3(),
	// 	type: '',
	// };

	// // TODO: iterate over all edges and check visibility upwards using BVH
	// for ( let i = 0, l = edges.length; i < l; i ++ ) {

	// 	const line = edges[ i ];
	// 	line.start.y += 1e-5;
	// 	line.end.y += 1e-5;

	// 	const lowestLineY = Math.min( line.start.y, line.end.y );
	// 	const highestLineY = Math.max( line.start.y, line.end.y );
	// 	const overlaps = [];
	// 	let tris = 0;
	// 	bvh.shapecast( {

	// 		intersectsBounds: box => {

	// 			return true;

	// 			// check if the box bounds are above the lowest line point
	// 			box.min.y = Math.min( lowestLineY, box.min.y );
	// 			tempRay.origin.copy( line.start );
	// 			line.delta( tempRay.direction );

	// 			tempRay.intersectsBox( box, tempVec );
	// 			return line.start.distanceToSquared( tempVec ) < line.distanceSq();

	// 		},

	// 		intersectsTriangle: tri => {

	// 			// skip the triangle if it is completely below the line
	// 			const highestTriangleY = Math.max( tri.a.y, tri.b.y, tri.c.y );

	// 			if ( highestTriangleY < lowestLineY ) {

	// 				return false;

	// 			}

	// 			if ( isLineTriangleEdge( tri, line ) ) {

	// 				return false;

	// 			}

	// 			if ( ! trimToBeneathTriPlane( tri, line, tempLine ) && isLineAboveTriangle( tri, line ) ) {

	// 				return false;

	// 			}

	//			if ( tempLine.distance() < 1e-5 ) {

	// 				return false;

	// 			}

	// 			getProjectedOverlaps( tri, tempLine, overlaps );
	// 			return false;

	// 		},

	// 	} );

	// 	overlapsToLines( line, overlaps, finalEdges );

	// }

	// scene.add( edgesToGeometry( finalEdges, - 2 ) );

}

function getProjectedOverlaps( tri, line, overlaps = [] ) {

	const target = {
		line: new THREE.Line3(),
		point: new THREE.Vector3(),
		planeHit: new THREE.Vector3(),
		type: '',
	};

	const tempDir = new THREE.Vector3();
	const tempVec0 = new THREE.Vector3();
	const tempVec1 = new THREE.Vector3();
	const _tri = new ExtendedTriangle();
	const _line = new THREE.Line3();

	_line.copy( line );
	_tri.copy( tri );
	_tri.needsUpdate = true;

	// flatten them to a common plane
	_line.start.y = 0;
	_line.end.y = 0;
	tri.a.y = 0;
	tri.b.y = 0;
	tri.c.y = 0;
	tri.needsUpdate = true;

	if ( lineIntersectTrianglePoint( _line, tri, target ) && target.type === 'line' ) {

		_line.delta( tempDir );
		tempVec0.subVectors( target.line.start, _line.start );
		tempVec1.subVectors( target.line.end, _line.start );

		const d0 = tempVec0.length() / tempDir.length();
		const d1 = tempVec1.length() / tempDir.length();

		if ( ! ( Math.abs( d0 - d1 ) < 1e-10 ) ) {

			overlaps.push( [ d0, d1 ] );

		}

	}

	return overlaps;

}

function isLineAboveTriangle( tri, line ) {

	const point = new THREE.Vector3();
	const outputPoint = new THREE.Vector3();

	point.lerpVectors( line.start, line.end, 0.5 );
	getTriYAtPoint( tri, point, outputPoint );

	return outputPoint.y < point.y;

}

function trimToBeneathTriPlane( tri, line, lineTarget ) {

	if ( tri.needsUpdate ) {

		tri.update();

	}

	const dir = new THREE.Vector3();
	const planeHit = new THREE.Vector3();
	line.delta( dir );

	const { plane } = tri;
	const areCoplanar = plane.normal.dot( dir ) === 0.0;

	lineTarget.copy( line );

	if ( areCoplanar ) {

		return false;

	}

	const doesLineIntersect = plane.intersectLine( line, planeHit );
	if ( doesLineIntersect ) {

		const point = new THREE.Vector3();
		const p = new THREE.Vector3();

		let testPoint;
		let flipped = false;
		if ( lineTarget.start.distanceTo( planeHit ) > lineTarget.end.distanceTo( planeHit ) ) {

			testPoint = lineTarget.start;

		} else {

			testPoint = lineTarget.end;
			flipped = true;

		}

		point.lerpVectors( testPoint, planeHit, 0.5 );
		getTriYAtPoint( tri, point, p );

		if ( p.y < point.y ) {

			if ( flipped ) lineTarget.end.copy( planeHit );
			else lineTarget.start.copy( planeHit );

		} else {

			if ( flipped ) lineTarget.start.copy( planeHit );
			else lineTarget.end.copy( planeHit );

		}

		return true;

	}

	return false;

}

function isLineTriangleEdge( tri, line ) {

	// if this is the same line as on the triangle
	const triPoints = tri.points;
	for ( let i = 0; i < 3; i ++ ) {

		const ni = ( i + 1 ) % 3;
		const t0 = triPoints[ i ];
		const t1 = triPoints[ ni ];

		if (
			line.start.distanceTo( t0 ) < 1e-10 && line.end.distanceTo( t1 ) < 1e-10 ||
			line.start.distanceTo( t1 ) < 1e-10 && line.end.distanceTo( t0 ) < 1e-10
		) {

			return true;

		}

	}

	return false;

}

function overlapsToLines( line, overlaps, target = [] ) {

	overlaps = [ ...overlaps ];

	overlaps.sort( ( a, b ) => {

		return a[ 0 ] - b[ 0 ];

	} );

	for ( let i = 1; i < overlaps.length; i ++ ) {

		const overlap = overlaps[ i ];
		const prevOverlap = overlaps[ i - 1 ];

		if ( overlap[ 0 ] <= prevOverlap[ 1 ] ) {

			prevOverlap[ 1 ] = Math.max( prevOverlap[ 1 ], overlap[ 1 ] );
			overlaps.splice( i, 1 );
			i --;
			continue;

		}

	}

	const invOverlaps = [[ 0, 1 ]];
	for ( let i = 0, l = overlaps.length; i < l; i ++ ) {

		invOverlaps[ i ][ 1 ] = overlaps[ i ][ 0 ];
		invOverlaps.push( [ overlaps[ i ][ 1 ], 1 ] );

	}

	for ( let i = 0, l = invOverlaps.length; i < l; i ++ ) {

		const newLine = new THREE.Line3();
		newLine.start.lerpVectors( line.start, line.end, invOverlaps[ i ][ 0 ] );
		newLine.end.lerpVectors( line.start, line.end, invOverlaps[ i ][ 1 ] );
		target.push( newLine );

	}

	return target;

}

function edgesToGeometry( edges, y = null ) {

	const edgeArray = [];
	edges.forEach( l => {

		edgeArray.push( l.start.x, y === null ? l.start.y : y, l.start.z );
		edgeArray.push( l.end.x, y === null ? l.end.y : y, l.end.z );

	} );

	const edgeGeom = new THREE.BufferGeometry();
	const edgeBuffer = new THREE.BufferAttribute( new Float32Array( edgeArray ), 3, true );
	edgeGeom.setAttribute( 'position', edgeBuffer );
	return new THREE.LineSegments( edgeGeom, new THREE.LineBasicMaterial( { color: 0 } ) );

}

function render() {

	requestAnimationFrame( render );

	if ( helper ) {

		helper.visible = params.displayHelper;

	}

	renderer.render( scene, camera );

}
<|endoftext|>


<commit_before>/*
 * DealPort
 * Copyright (c) 2014  DealPort B.V.
 *
 * This file is part of DealPort
 *
 * DealPort is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, version 3 of the License.
 *
 * DealPort is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with DealPort.  If not, see <http://www.gnu.org/licenses/>.
 *
 * In addition, the following supplemental terms apply, based on section 7 of
 * the GNU Affero General Public License (version 3):
 * a) Preservation of all legal notices and author attributions
 */

'use strict';

var P = require('bluebird');
var merge = require('merge');
var ObjectID = require('mongodb').ObjectID;

var Model = require('./Model');

/**
 *
 * Looks like in mongo:
 *
 * {
 *  "_id" : "object id hex string",
 *  "companyImportID": "1234",
 *  "name" : "{ New company! }",
 *  "logoUploadedImage": "object id hex string"
 *  "logoURL" : "https://example.com/foo.png", // not used in case logoUploadedImage is set
 *  "openForInvestment" : false,
 *  "hiring" : false,
 *  "sectors" : [
 *      "please",
 *      "fill in the",
 *      "sectors"
 *  ],
 *  "revenueModel" : "Unknown",
 *  "payoff" : "payoff goes here",
 *  "productDesc" : "long text with newlines",
 *  "hidden" : true,
 *  "visible" : false,
 *  "private" : {
 *      "editableByUser" : [
 *          ObjectId(...)
 *      ],
 *      "createdByUser" : ObjectId(...)
 *  }
 * }
 *
 *
 * Virtual keys:
 *
 * editableByCurrentUser
 *
 * @param modelIndex
 * @constructor
 */

function CompanyModel(modelIndex)
{
        Model.call(this, modelIndex);
}

module.exports = CompanyModel;
require('inherits')(CompanyModel, Model);

CompanyModel.prototype.isEditableByUser = function(company, user)
{
        var ret = false;

        if (user &&
            user.private &&
            user.private.allPermissions)
        {
                return true;
        }

        if (user &&
            company.private &&
            company.private.editableByUser)
        {
                company.private.editableByUser.forEach(function(userId)
                {
                        if (user._id === userId)
                        {
                                ret = true;
                        }
                });
        }

        return ret;
};

CompanyModel.prototype.isVisibleToUser = function(company, user)
{
        if (company.visible || company.visible === undefined)
        {
                return true;
        }

        return this.isEditableByUser(company, user);
};

CompanyModel.prototype.addVirtualFields = function(id, data, user)
{
        data._id = id;

        data.editableByCurrentUser = this.isEditableByUser(data, user);

        if (data.visible === undefined) { data.visible = true; }
};

CompanyModel.prototype.filterDocument = P.method(function(docId, data, user)
{
        this.addVirtualFields(docId, data.data, user);
        data.data.private = undefined; // delete is slow

});

CompanyModel.prototype.validateReadingAllowed = P.method(function(docId, user)
{
});

CompanyModel.prototype._validateComponent = function(comp)
{
        if (!Array.isArray(comp.p))
        {
                console.error(comp);
                throw Error('Invalid operation, missing path');
        }

        function assert(cond)
        {
                if (!cond)
                {
                        throw Error('Invalid operation for path ' + comp.p.join(', '));
                }
        }

        // todo: make this stuff more generic
        var cleanComp = {p : comp.p};

        // allow complete replacements for these keys
        if (comp.p.length === 1 && 'oi' in comp)
        {
                switch (comp.p[0])
                {
                        case 'name':
                        case 'payoff':
                        case 'revenueModel':
                        case 'productDesc':
                                if ('oi' in comp)
                                {
                                        assert(typeof comp.oi === 'string');
                                        cleanComp.oi = comp.oi;
                                }

                                if ('od' in comp)
                                {
                                        assert(typeof comp.od === 'string');
                                        cleanComp.od = comp.od;

                                        if (!('oi' in cleanComp))
                                        {
                                                // make sure the key is not deleted (turn it into a replacement)
                                                cleanComp.oi = '';
                                        }
                                }
                                break;
                        case 'homepage': // nullable string
                        case 'logoUploadedImage':
                                if ('oi' in comp)
                                {
                                        assert(comp.oi === null || typeof comp.oi === 'string');
                                        cleanComp.oi = comp.oi;
                                }

                                if ('od' in comp)
                                {
                                        assert(comp.od === null || typeof comp.od === 'string');
                                        cleanComp.od = comp.od;

                                        if (!('oi' in cleanComp))
                                        {
                                                // make sure the key is not deleted (turn it into a replacement)
                                                cleanComp.oi = '';
                                        }
                                }
                                break;
                        case 'hiring':
                        case 'openForInvestment':
                        case 'visible':
                                if ('oi' in comp)
                                {
                                        assert(typeof comp.oi === 'boolean');
                                        cleanComp.oi = comp.oi;
                                }

                                if ('od' in comp)
                                {
                                        assert(typeof comp.od === 'boolean');
                                        cleanComp.od = comp.od;

                                        if (!('oi' in cleanComp))
                                        {
                                                // make sure the key is not deleted
                                                cleanComp.oi = false;
                                        }
                                }
                                break;
                        case 'sectors':
                                if ('oi' in comp)
                                {
                                        assert(Array.isArray(comp.oi));
                                        comp.oi.forEach(function(item){ assert(typeof item === 'string'); });

                                        cleanComp.oi = comp.oi;
                                }

                                if ('od' in comp)
                                {
                                        assert(Array.isArray(comp.od));
                                        comp.od.forEach(function(item){ assert(typeof item === 'string'); });

                                        cleanComp.od = comp.od;

                                        if (!('oi' in cleanComp))
                                        {
                                                // make sure the key is not deleted
                                                cleanComp.oi = false;
                                        }
                                }
                                break;
                }

                return cleanComp;
        }

        switch(comp.p[0])
        {
                case 'name': // strings
                case 'payoff':
                case 'revenueModel':
                case 'homepage':
                case 'productDesc':
                        assert(comp.p.length === 2);
                        assert(Number.isFinite(comp[1]));

                        if ('si' in comp)
                        {
                                assert(typeof comp.si === 'string');
                                cleanComp.si = comp.si;
                        }

                        if ('sd' in comp)
                        {
                                assert(typeof comp.sd === 'string');
                                cleanComp.sd = comp.si;
                        }

                        break;

                case 'sectors':

                        if ('li' in comp)
                        {
                                assert(comp.p.length === 2);
                                assert(Number.isFinite(comp[1]));
                                assert(typeof comp.li === 'string');
                                cleanComp.li = comp.li;
                        }

                        if ('ld' in comp)
                        {
                                assert(comp.p.length === 2);
                                assert(Number.isFinite(comp[1]));
                                assert(typeof comp.ld === 'string');
                                cleanComp.ld = comp.ld;
                        }

                        if ('si' in comp)
                        {
                                assert(comp.p.length === 3);
                                assert(Number.isFinite(comp[1]));
                                assert(typeof comp.si === 'string');
                                cleanComp.si = comp.si;
                        }

                        if ('sd' in comp)
                        {
                                assert(comp.p.length === 3);
                                assert(Number.isFinite(comp[1]));
                                assert(typeof comp.si === 'string');
                                cleanComp.sd = comp.si;
                        }
        }

        return cleanComp;
};

CompanyModel.prototype.validateOperation = P.method(function(docId, opData, user)
{
        return P.using(this.modelIndex.acquire(), function(db)
        {
                return db.company.findOneAsync({_id: docId}, {_id: 1, private: 1})
                .bind(this)
                .then(function(companyPrivateOnly)
                {
                        if (!companyPrivateOnly)
                        {
                                throw Error('Invalid company id: ' + docId);
                        }

                        if (!this.isEditableByUser(companyPrivateOnly, user))
                        {
                                throw Error('You are not allowed to edit the company: ' + docId);
                        }

                        opData.op.forEach(function(comp, index)
                        {
                                opData.op[index] = this._validateComponent(comp);
                        }, this);

                        return true;
                });
        }.bind(this));
});

CompanyModel.prototype.afterSubmit = P.method(function(docId, opData, snapshot, user)
{
        var nameChanged = false;

        opData.op.forEach(function(comp, index)
        {
                if (comp.p &&
                    comp.p[0] === 'name')
                {
                        nameChanged = true;
                }
        }, this);

        if (nameChanged)
        {
                return this.modelIndex.namedEntity.createByGuessingId({
                        company: docId
                });
        }

        return null;
});

var companyDefaultFields = {
        name: '{ New company! }',
        logoURL: 'https://angel.co/images/shared/nopic_startup.png',
        openForInvestment: false,
        hiring: false,
        sectors: ['please', 'fill in the', 'sectors'],
        revenueModel : 'Unknown',
        payoff: 'payoff goes here',
        hidden: true,
        visible: false,
        private: {
                editableByUser: [],
                createdByUser: null
        }
};

CompanyModel.prototype.newCompany = P.method(function(initialFields)
{
        var fields = merge.recursive(companyDefaultFields, initialFields);
        var id = new ObjectID().toString();

        return this.modelIndex.livedb.submitAsync('company', id, {
                create: {
                        type: 'json0',
                        data: fields
                }
        })
        .bind(this)
        .spread(function(version, transformedByOps, snapshot)
        {
                this.addVirtualFields(id, snapshot.data, null);
                return snapshot.data;
        });
});<commit_msg>This is not a real field<commit_after>/*
 * DealPort
 * Copyright (c) 2014  DealPort B.V.
 *
 * This file is part of DealPort
 *
 * DealPort is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, version 3 of the License.
 *
 * DealPort is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with DealPort.  If not, see <http://www.gnu.org/licenses/>.
 *
 * In addition, the following supplemental terms apply, based on section 7 of
 * the GNU Affero General Public License (version 3):
 * a) Preservation of all legal notices and author attributions
 */

'use strict';

var P = require('bluebird');
var merge = require('merge');
var ObjectID = require('mongodb').ObjectID;

var Model = require('./Model');

/**
 *
 * Looks like in mongo:
 *
 * {
 *  "_id" : "object id hex string",
 *  "companyImportID": "1234",
 *  "name" : "{ New company! }",
 *  "logoUploadedImage": "object id hex string"
 *  "logoURL" : "https://example.com/foo.png", // not used in case logoUploadedImage is set
 *  "openForInvestment" : false,
 *  "hiring" : false,
 *  "sectors" : [
 *      "please",
 *      "fill in the",
 *      "sectors"
 *  ],
 *  "revenueModel" : "Unknown",
 *  "payoff" : "payoff goes here",
 *  "productDesc" : "long text with newlines",
 *  "visible" : false,
 *  "private" : {
 *      "editableByUser" : [
 *          ObjectId(...)
 *      ],
 *      "createdByUser" : ObjectId(...)
 *  }
 * }
 *
 *
 * Virtual keys:
 *
 * editableByCurrentUser
 *
 * @param modelIndex
 * @constructor
 */

function CompanyModel(modelIndex)
{
        Model.call(this, modelIndex);
}

module.exports = CompanyModel;
require('inherits')(CompanyModel, Model);

CompanyModel.prototype.isEditableByUser = function(company, user)
{
        var ret = false;

        if (user &&
            user.private &&
            user.private.allPermissions)
        {
                return true;
        }

        if (user &&
            company.private &&
            company.private.editableByUser)
        {
                company.private.editableByUser.forEach(function(userId)
                {
                        if (user._id === userId)
                        {
                                ret = true;
                        }
                });
        }

        return ret;
};

CompanyModel.prototype.isVisibleToUser = function(company, user)
{
        if (company.visible || company.visible === undefined)
        {
                return true;
        }

        return this.isEditableByUser(company, user);
};

CompanyModel.prototype.addVirtualFields = function(id, data, user)
{
        data._id = id;

        data.editableByCurrentUser = this.isEditableByUser(data, user);

        if (data.visible === undefined) { data.visible = true; }
};

CompanyModel.prototype.filterDocument = P.method(function(docId, data, user)
{
        this.addVirtualFields(docId, data.data, user);
        data.data.private = undefined; // delete is slow

});

CompanyModel.prototype.validateReadingAllowed = P.method(function(docId, user)
{
});

CompanyModel.prototype._validateComponent = function(comp)
{
        if (!Array.isArray(comp.p))
        {
                console.error(comp);
                throw Error('Invalid operation, missing path');
        }

        function assert(cond)
        {
                if (!cond)
                {
                        throw Error('Invalid operation for path ' + comp.p.join(', '));
                }
        }

        // todo: make this stuff more generic
        var cleanComp = {p : comp.p};

        // allow complete replacements for these keys
        if (comp.p.length === 1 && 'oi' in comp)
        {
                switch (comp.p[0])
                {
                        case 'name':
                        case 'payoff':
                        case 'revenueModel':
                        case 'productDesc':
                                if ('oi' in comp)
                                {
                                        assert(typeof comp.oi === 'string');
                                        cleanComp.oi = comp.oi;
                                }

                                if ('od' in comp)
                                {
                                        assert(typeof comp.od === 'string');
                                        cleanComp.od = comp.od;

                                        if (!('oi' in cleanComp))
                                        {
                                                // make sure the key is not deleted (turn it into a replacement)
                                                cleanComp.oi = '';
                                        }
                                }
                                break;
                        case 'homepage': // nullable string
                        case 'logoUploadedImage':
                                if ('oi' in comp)
                                {
                                        assert(comp.oi === null || typeof comp.oi === 'string');
                                        cleanComp.oi = comp.oi;
                                }

                                if ('od' in comp)
                                {
                                        assert(comp.od === null || typeof comp.od === 'string');
                                        cleanComp.od = comp.od;

                                        if (!('oi' in cleanComp))
                                        {
                                                // make sure the key is not deleted (turn it into a replacement)
                                                cleanComp.oi = '';
                                        }
                                }
                                break;
                        case 'hiring':
                        case 'openForInvestment':
                        case 'visible':
                                if ('oi' in comp)
                                {
                                        assert(typeof comp.oi === 'boolean');
                                        cleanComp.oi = comp.oi;
                                }

                                if ('od' in comp)
                                {
                                        assert(typeof comp.od === 'boolean');
                                        cleanComp.od = comp.od;

                                        if (!('oi' in cleanComp))
                                        {
                                                // make sure the key is not deleted
                                                cleanComp.oi = false;
                                        }
                                }
                                break;
                        case 'sectors':
                                if ('oi' in comp)
                                {
                                        assert(Array.isArray(comp.oi));
                                        comp.oi.forEach(function(item){ assert(typeof item === 'string'); });

                                        cleanComp.oi = comp.oi;
                                }

                                if ('od' in comp)
                                {
                                        assert(Array.isArray(comp.od));
                                        comp.od.forEach(function(item){ assert(typeof item === 'string'); });

                                        cleanComp.od = comp.od;

                                        if (!('oi' in cleanComp))
                                        {
                                                // make sure the key is not deleted
                                                cleanComp.oi = false;
                                        }
                                }
                                break;
                }

                return cleanComp;
        }

        switch(comp.p[0])
        {
                case 'name': // strings
                case 'payoff':
                case 'revenueModel':
                case 'homepage':
                case 'productDesc':
                        assert(comp.p.length === 2);
                        assert(Number.isFinite(comp[1]));

                        if ('si' in comp)
                        {
                                assert(typeof comp.si === 'string');
                                cleanComp.si = comp.si;
                        }

                        if ('sd' in comp)
                        {
                                assert(typeof comp.sd === 'string');
                                cleanComp.sd = comp.si;
                        }

                        break;

                case 'sectors':

                        if ('li' in comp)
                        {
                                assert(comp.p.length === 2);
                                assert(Number.isFinite(comp[1]));
                                assert(typeof comp.li === 'string');
                                cleanComp.li = comp.li;
                        }

                        if ('ld' in comp)
                        {
                                assert(comp.p.length === 2);
                                assert(Number.isFinite(comp[1]));
                                assert(typeof comp.ld === 'string');
                                cleanComp.ld = comp.ld;
                        }

                        if ('si' in comp)
                        {
                                assert(comp.p.length === 3);
                                assert(Number.isFinite(comp[1]));
                                assert(typeof comp.si === 'string');
                                cleanComp.si = comp.si;
                        }

                        if ('sd' in comp)
                        {
                                assert(comp.p.length === 3);
                                assert(Number.isFinite(comp[1]));
                                assert(typeof comp.si === 'string');
                                cleanComp.sd = comp.si;
                        }
        }

        return cleanComp;
};

CompanyModel.prototype.validateOperation = P.method(function(docId, opData, user)
{
        return P.using(this.modelIndex.acquire(), function(db)
        {
                return db.company.findOneAsync({_id: docId}, {_id: 1, private: 1})
                .bind(this)
                .then(function(companyPrivateOnly)
                {
                        if (!companyPrivateOnly)
                        {
                                throw Error('Invalid company id: ' + docId);
                        }

                        if (!this.isEditableByUser(companyPrivateOnly, user))
                        {
                                throw Error('You are not allowed to edit the company: ' + docId);
                        }

                        opData.op.forEach(function(comp, index)
                        {
                                opData.op[index] = this._validateComponent(comp);
                        }, this);

                        return true;
                });
        }.bind(this));
});

CompanyModel.prototype.afterSubmit = P.method(function(docId, opData, snapshot, user)
{
        var nameChanged = false;

        opData.op.forEach(function(comp, index)
        {
                if (comp.p &&
                    comp.p[0] === 'name')
                {
                        nameChanged = true;
                }
        }, this);

        if (nameChanged)
        {
                return this.modelIndex.namedEntity.createByGuessingId({
                        company: docId
                });
        }

        return null;
});

var companyDefaultFields = {
        name: '{ New company! }',
        logoURL: 'https://angel.co/images/shared/nopic_startup.png',
        openForInvestment: false,
        hiring: false,
        sectors: ['please', 'fill in the', 'sectors'],
        revenueModel : 'Unknown',
        payoff: 'payoff goes here',
        visible: false,
        private: {
                editableByUser: [],
                createdByUser: null
        }
};

CompanyModel.prototype.newCompany = P.method(function(initialFields)
{
        var fields = merge.recursive(companyDefaultFields, initialFields);
        var id = new ObjectID().toString();

        return this.modelIndex.livedb.submitAsync('company', id, {
                create: {
                        type: 'json0',
                        data: fields
                }
        })
        .bind(this)
        .spread(function(version, transformedByOps, snapshot)
        {
                this.addVirtualFields(id, snapshot.data, null);
                return snapshot.data;
        });
});<|endoftext|>