diff --git a/admin/three-model-viewer-settings/App.js b/admin/three-model-viewer-settings/App.js deleted file mode 100644 index 071af07..0000000 --- a/admin/three-model-viewer-settings/App.js +++ /dev/null @@ -1,60 +0,0 @@ -import { useState, useEffect } from '@wordpress/element'; - -//Main component for admin page app -export default function App( { getSettings, updateSettings } ) { - //Track settings state - const [ settings, setSettings ] = useState( {} ); - //Use to show loading spinner - - const [ isLoading, setIsLoading ] = useState( true ); - //When app loads, get settings - useEffect( () => { - getSettings().then( ( r ) => { - setSettings( r ); - setIsLoading( false ); - } ); - }, [ getSettings, setSettings ] ); - - //Function to update settings via API - const onSave = () => { - updateSettings( settings ).then( ( r ) => { - setSettings( r ); - } ); - }; - - //Show a spinner if loading - if ( isLoading ) { - return
; - } - - //Show settings if not loading - return ( -
-
{ settings.enabled ? 'Enabled' : 'Not enabled' }
-
- - { - setSettings( { - ...settings, - enabled: ! settings.enabled, - } ); - } } - /> -
-
- - -
-
- ); -} diff --git a/admin/three-object-viewer-settings/App.js b/admin/three-object-viewer-settings/App.js new file mode 100644 index 0000000..0a4653c --- /dev/null +++ b/admin/three-object-viewer-settings/App.js @@ -0,0 +1,203 @@ +import { Suspense, useState, useEffect } from "@wordpress/element"; +import { Canvas, useLoader, useFrame, useThree } from '@react-three/fiber'; +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; +import { VRM, VRMUtils, VRMSchema, VRMLoaderPlugin } from '@pixiv/three-vrm' +import { + OrthographicCamera, + OrbitControls, + useAnimations, +} from '@react-three/drei'; +import * as THREE from 'three'; +import defaultAikonaut from '../../inc/avatars/mummy.vrm'; + +function SavedObject( props ) { + const [ url, set ] = useState( props.url ); + useEffect( () => { + setTimeout( () => set( props.url ), 2000 ); + }, [] ); + const [ listener ] = useState( () => new THREE.AudioListener() ); + + useThree( ( { camera } ) => { + camera.add( listener ); + } ); + const fallbackURL = threeObjectPlugin + defaultAikonaut; + const playerURL = props.url ? props.url : fallbackURL; + + const someSceneState = useLoader( GLTFLoader, playerURL, ( loader ) => { + loader.register( ( parser ) => { + return new VRMLoaderPlugin( parser ); + } ); + } ); + + if(someSceneState?.userData?.gltfExtensions?.VRM){ + const playerController = someSceneState.userData.vrm; + VRMUtils.rotateVRM0( playerController ); + const rotationVRM = playerController.scene.rotation.y; + playerController.scene.rotation.set( 0, rotationVRM, 0 ); + playerController.scene.scale.set( 3, 3, 3 ); + playerController.scene.position.set( 0, -2.5, 0 ); + return <>; + } +} + +//Main component for admin page app +export default function App({ getSettings, updateSettings }) { + + let frame + + //Track settings state + const [settings, setSettings] = useState({}); + //Use to show loading spinner + const [defaultVRM, setDefaultVRM] = useState(); + //Use to show loading spinner + + const [isLoading, setIsLoading] = useState(true); + //When app loads, get settings + useEffect(() => { + getSettings().then((r) => { + setSettings(r); + setIsLoading(false); + }); + }, [getSettings, setSettings]); + + //Function to update settings via API + const onSave = () => { + updateSettings(settings).then((r) => { + setSettings(r); + }); + }; + const runUploader = (event) => { + event.preventDefault() + + // If the media frame already exists, reopen it. + if (frame) { + frame.open() + return + } + + // Create a new media frame + frame = wp.media({ + title: 'Select or Upload Media', + button: { + text: 'Use this media', + }, + multiple: false, // Set to true to allow multiple files to be selected + }) + frame.on( 'select', function() { + + // Get media attachment details from the frame state + var attachment = frame.state().get('selection').first().toJSON(); + // console.log(attachment); + setDefaultVRM(attachment.url); + // Send the attachment URL to our custom image input field. + }); + + + // Finally, open the modal on click + frame.open() + } + + //Show a spinner if loading + if (isLoading) { + return
; + } + + //Show settings if not loading + return ( +
+
+

Three Object Viewer Settings

+
+
+

Avatar and World Defaults

+

This avatar will be used for guest visitors or logged in users that have not set their main avatar in the user profile page.

+
+
+ + + + + + {defaultVRM ? + : + + } + + + +

+ { defaultVRM && defaultVRM } +

+ +
+
+

Network Settings

+
+
+
Network Settings
+
+ + { + setSettings({ ...settings, enabled: !settings.enabled }); + }} + /> +
+ + { + setSettings({ ...settings, networkWorker: !settings.networkWorker }); + }} + /> +
+
+ + +
+
+ ); +} diff --git a/admin/three-model-viewer-settings/App.test.js.test b/admin/three-object-viewer-settings/App.test.js.test similarity index 100% rename from admin/three-model-viewer-settings/App.test.js.test rename to admin/three-object-viewer-settings/App.test.js.test diff --git a/admin/three-model-viewer-settings/index.js b/admin/three-object-viewer-settings/index.js similarity index 83% rename from admin/three-model-viewer-settings/index.js rename to admin/three-object-viewer-settings/index.js index b359266..25c724f 100644 --- a/admin/three-model-viewer-settings/index.js +++ b/admin/three-object-viewer-settings/index.js @@ -5,7 +5,7 @@ import apiFetch from '@wordpress/api-fetch'; window.addEventListener( 'load', async function () { //Endpoint URL - const path = '/three-object-viewer/v1/three-model-viewer-settings/'; + const path = '/three-object-viewer/v1/three-object-viewer-settings/'; //Get settings from the REST API endpoint const getSettings = async () => { @@ -28,6 +28,6 @@ window.addEventListener( 'load', async function () { render( , - document.getElementById( 'three-model-viewer-settings' ) + document.getElementById( 'three-object-viewer-settings' ) ); } ); diff --git a/admin/three-model-viewer-settings/init.php b/admin/three-object-viewer-settings/init.php similarity index 66% rename from admin/three-model-viewer-settings/init.php rename to admin/three-object-viewer-settings/init.php index 519948b..370df69 100644 --- a/admin/three-model-viewer-settings/init.php +++ b/admin/three-object-viewer-settings/init.php @@ -1,7 +1,8 @@ ['GET'], @@ -47,22 +51,22 @@ //Enqueue assets for Model Viewer Settings on admin page only add_action('admin_enqueue_scripts', function ($hook) { - if ('toplevel_page_three-model-viewer-settings' != $hook) { + if ('toplevel_page_three-object-viewer-settings' != $hook) { return; } - wp_enqueue_script('three-model-viewer-settings'); + wp_enqueue_script('three-object-viewer-settings'); }); //Register Model Viewer Settings menu page -// add_action('admin_menu', function () { -// add_menu_page( -// __('Model Viewer Settings', 'three-object-viewer'), -// __('Model Viewer Settings', 'three-object-viewer'), -// 'manage_options', -// 'three-model-viewer-settings', -// function () { -// //React root -// echo '
'; -// } -// ); -// }); +add_action('admin_menu', function () { + add_menu_page( + __('Model Viewer Settings', 'three-object-viewer'), + __('Model Viewer Settings', 'three-object-viewer'), + 'manage_options', + 'three-object-viewer-settings', + function () { + //React root + echo '
'; + } + ); +}); diff --git a/blocks/environment/Edit.js b/blocks/environment/Edit.js new file mode 100644 index 0000000..4df0dde --- /dev/null +++ b/blocks/environment/Edit.js @@ -0,0 +1,284 @@ +import { __ } from '@wordpress/i18n'; +import React, { useState } from 'react'; +import { DropZone } from '@wordpress/components'; +import './editor.scss'; +import { + useBlockProps, + ColorPalette, + InspectorControls, + MediaUpload, + InnerBlocks +} from '@wordpress/block-editor'; +import { + Panel, + PanelBody, + PanelRow, + RangeControl, + ToggleControl, + SelectControl, + TextControl, +} from '@wordpress/components'; +import { more } from '@wordpress/icons'; + +import ThreeObjectEdit from './components/ThreeObjectEdit'; + +export default function Edit( { attributes, setAttributes, isSelected } ) { + const ALLOWED_BLOCKS = ['three-object-viewer/model-block', 'three-object-viewer/sky-block', 'three-object-viewer/npc-block', 'three-object-viewer/three-image-block', 'three-object-viewer/three-video-block', 'three-object-viewer/three-audio-block' ]; + + const onChangeAnimations = ( animations ) => { + setAttributes( { animations: animations } ); + }; + + const onImageSelect = ( imageObject ) => { + setAttributes( { threeObjectUrl: null } ); + setAttributes( { threeObjectUrl: imageObject.url } ); + }; + const onChangePositionY = ( posy ) => { + setAttributes( { positionY: posy } ); + }; + + const onChangeScale = ( scale ) => { + setAttributes( { scale: scale } ); + }; + + const onChangerotationY = ( rotz ) => { + setAttributes( { rotationY: rotz } ); + }; + + const setDeviceTarget = ( target ) => { + setAttributes( { deviceTarget: target } ); + }; + + const [ enteredURL, setEnteredURL ] = useState( "" ); + + const { mediaUpload } = wp.editor; + + const ALLOWED_MEDIA_TYPES = [ + 'model/gltf-binary', + 'application/octet-stream', + ]; + + const MyDropZone = () => { + const [ hasDropped, setHasDropped ] = useState( false ); + return ( +
+ { hasDropped ? 'Dropped!' : 'Drop a glb here or' } + + mediaUpload( { + allowedTypes: ALLOWED_MEDIA_TYPES, + filesList: files, + onFileChange: ( [ images ] ) => { + onImageSelect( images ); + }, + } ) + } + /> +
+ ); + }; + + return ( +
+ + + + + + Select a glb file from your media library. This will be treated as a collidable mesh that visitors can walk on: + + + + + onImageSelect( imageObject ) + } + type="image" + label="GLB File" + allowedTypes={ ALLOWED_MEDIA_TYPES } + value={ attributes.threeObjectUrl } + render={ ( { open } ) => ( + + ) } + /> + + + + + Object Display Type: + + + + setDeviceTarget( target ) + } + /> + + + + onChangeAnimations( value ) + } + /> + + + + + + + + + + + + + + { isSelected ? ( + <> + { attributes.threeObjectUrl ? ( + + ) : ( +
+ + +
+ + Select a glb file to render in the canvas: + + {/*
+ setEnteredURL(e.target.value)}> + +
*/} + + onImageSelect( imageObject ) + } + type="image" + allowedTypes={ ALLOWED_MEDIA_TYPES } + value={ attributes.threeObjectUrl } + render={ ( { open } ) => ( + + ) } + /> +
+
+ ) } + + + ) : ( + <> + { attributes.threeObjectUrl ? ( + + ) : ( +
+ +
+ + Select a glb file to render in the canvas: + + {/*
+ console.log(e.target.value) && setEnteredURL(e.target.value)}> + +
*/} +
+ + onImageSelect( imageObject ) + } + type="image" + allowedTypes={ ALLOWED_MEDIA_TYPES } + value={ attributes.threeObjectUrl } + render={ ( { open } ) => ( + + ) } + /> +
+ ) } + + + ) } +
+ ); +} diff --git a/blocks/environment/Edit.test.js b/blocks/environment/Edit.test.js new file mode 100644 index 0000000..ab10afe --- /dev/null +++ b/blocks/environment/Edit.test.js @@ -0,0 +1,57 @@ + +//Import React +import React from 'react'; +//Import test renderer +import { render, fireEvent, cleanup } from '@testing-library/react'; +//Import component to test +import { Editor } from './Edit'; + + +describe("Editor componet", () => { + afterEach(cleanup); + it('matches snapshot when selected', () => { + const onChange = jest.fn(); + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot when not selected', () => { + const onChange = jest.fn(); + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("Calls the onchange function", () => { + const onChange = jest.fn(); + const { getByDisplayValue } = render(); + fireEvent.change(getByDisplayValue("Salad"), { + target: { value: "New Value" } + }); + expect(onChange).toHaveBeenCalledTimes(1); + }); + + it("Passes updated value, not event to onChange callback", () => { + const onChange = jest.fn(); + const { getByDisplayValue } = render(); + fireEvent.change(getByDisplayValue("Seltzer"), { + target: { value: "Boring Water" } + }); + expect(onChange).toHaveBeenCalledWith("Boring Water"); + }); +}); \ No newline at end of file diff --git a/blocks/environment/Save.js b/blocks/environment/Save.js new file mode 100644 index 0000000..79a0576 --- /dev/null +++ b/blocks/environment/Save.js @@ -0,0 +1,41 @@ +import { __ } from '@wordpress/i18n'; +import { useBlockProps, InnerBlocks } from '@wordpress/block-editor'; + +export default function save( { attributes } ) { + return ( +
+ <> +
+

+ { attributes.deviceTarget } +

+

+ { attributes.threeObjectUrl } +

+

{ attributes.scale }

+

+ { attributes.bg_color } +

+

{ attributes.zoom }

+

+ { attributes.hasZoom ? 1 : 0 } +

+

+ { attributes.hasTip ? 1 : 0 } +

+

+ { attributes.positionY } +

+

+ { attributes.rotationY } +

+

{ attributes.scale }

+

+ { attributes.animations } +

+ +
+ +
+ ); +} diff --git a/blocks/environment/block.json b/blocks/environment/block.json new file mode 100644 index 0000000..722fa06 --- /dev/null +++ b/blocks/environment/block.json @@ -0,0 +1,44 @@ +{ + "name": "three-object-viewer/environment", + "title": "Environment Block", + "description": "A 3D environment component for VersePress", + "attributes": { + "scale": { + "type": "integer", + "default": 1 + }, + "positionX": { + "type": "integer", + "default": 0 + }, + "positionY": { + "type": "integer", + "default": 0 + }, + "rotationY": { + "type": "integer", + "default": 0 + }, + "threeObjectUrl": { + "type": "string", + "default": null + }, + "deviceTarget": { + "type": "string", + "default": "2d" + }, + "animations": { + "type": "string", + "default": "" + } + }, + "category": "3D", + "apiVersion": 2, + "supports": { + "html": false, + "multiple": true + }, + "editorScript": "file:../../build/block-environment.js", + "editorStyle": "file:../../build/block-environment.css", + "style": "file:../../build/block-environment.css" +} diff --git a/blocks/environment/components/Controls.js b/blocks/environment/components/Controls.js new file mode 100644 index 0000000..fbca728 --- /dev/null +++ b/blocks/environment/components/Controls.js @@ -0,0 +1,109 @@ +import React, { useRef, useState } from 'react'; +import { useFrame } from '@react-three/fiber'; +import { PointerLockControls } from '@react-three/drei'; +import previewOptions from '@wordpress/block-editor/build/components/preview-options'; + +const Controls = () => { + const controlsRef = useRef(); + const isLocked = useRef( false ); + const [ moveForward, setMoveForward ] = useState( false ); + const [ moveBackward, setMoveBackward ] = useState( false ); + const [ moveLeft, setMoveLeft ] = useState( false ); + const [ moveRight, setMoveRight ] = useState( false ); + + useFrame( () => { + const velocity = 0.5; + if ( moveForward ) { + controlsRef.current.moveForward( velocity ); + } else if ( moveLeft ) { + controlsRef.current.moveRight( -velocity ); + } else if ( moveBackward ) { + controlsRef.current.moveForward( -velocity ); + } else if ( moveRight ) { + controlsRef.current.moveRight( velocity ); + } + } ); + + const onKeyDown = function ( event ) { + switch ( event.code ) { + case 'ArrowUp': + case 'KeyW': + setMoveForward( true ); + break; + + case 'ArrowLeft': + case 'KeyA': + setMoveLeft( true ); + break; + + case 'ArrowDown': + case 'KeyS': + setMoveBackward( true ); + break; + + case 'ArrowRight': + case 'KeyD': + setMoveRight( true ); + break; + case "Space": + window.addEventListener('keydown', (e) => { + if (e.keyCode === 32 && e.target === document.body) { + e.preventDefault(); + } + }); + console.log("boing"); + break; + default: + return; + } + }; + + const onKeyUp = function ( event ) { + switch ( event.code ) { + case 'ArrowUp': + case 'KeyW': + setMoveForward( false ); + break; + + case 'ArrowLeft': + case 'KeyA': + setMoveLeft( false ); + break; + + case 'ArrowDown': + case 'KeyS': + setMoveBackward( false ); + break; + + case 'ArrowRight': + case 'KeyD': + setMoveRight( false ); + break; + + default: + return; + } + }; + + document.addEventListener( 'keydown', onKeyDown ); + document.addEventListener( 'keyup', onKeyUp ); + return ( + { + if ( controlsRef.current ) { + controlsRef.current.addEventListener( 'lock', () => { + console.log( 'lock' ); + isLocked.current = true; + } ); + controlsRef.current.addEventListener( 'unlock', () => { + console.log( 'unlock' ); + isLocked.current = false; + } ); + } + } } + ref={ controlsRef } + /> + ); +}; + +export default Controls; diff --git a/blocks/environment/components/EnvironmentFront.js b/blocks/environment/components/EnvironmentFront.js new file mode 100644 index 0000000..d0d009c --- /dev/null +++ b/blocks/environment/components/EnvironmentFront.js @@ -0,0 +1,513 @@ +import * as THREE from 'three'; +import React, { Suspense, useRef, useState, useEffect } from 'react'; +import { Canvas, useLoader, useFrame, useThree } from '@react-three/fiber'; +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; +import { Physics, RigidBody } from "@react-three/rapier"; + +import { + OrthographicCamera, + OrbitControls, + useAnimations, +} from '@react-three/drei'; +import { GLTFAudioEmitterExtension } from 'three-omi'; +import { + VRCanvas, + ARCanvas, + DefaultXRControllers, + Hands, +} from '@react-three/xr'; +import { VRM, VRMUtils, VRMSchema, VRMLoaderPlugin } from '@pixiv/three-vrm' +import TeleportTravel from './TeleportTravel'; +import defaultVRM from '../../../inc/avatars/mummy.vrm'; +import Controls from './Controls'; +import { useAspect } from '@react-three/drei' + +function SavedObject( props ) { + const [ url, set ] = useState( props.url ); + const [cameraPosition, setCameraPosition] = useState(); + useEffect( () => { + setTimeout( () => set( props.url ), 2000 ); + }, [] ); + const [ listener ] = useState( () => new THREE.AudioListener() ); + + useThree( ( { camera } ) => { + camera.add( listener ); + } ); + + const gltf = useLoader( GLTFLoader, url, ( loader ) => { + loader.register( + ( parser ) => new GLTFAudioEmitterExtension( parser, listener ) + ); + loader.register( ( parser ) => { + return new VRMLoaderPlugin( parser ); + } ); + } ); + + const { actions } = useAnimations( gltf.animations, gltf.scene ); + + const animationList = props.animations ? props.animations.split( ',' ) : ''; + useEffect( () => { + if ( animationList ) { + animationList.forEach( ( name ) => { + if ( Object.keys( actions ).includes( name ) ) { + actions[ name ].play(); + } + } ); + } + }, [] ); + + // Player controller. + // const fallbackURL = threeObjectPlugin + defaultVRM; + // const playerURL = props.playerData.vrm ? props.playerData.vrm : fallbackURL + + // const someSceneState = useLoader( GLTFLoader, playerURL, ( loader ) => { + // loader.register( + // ( parser ) => new GLTFAudioEmitterExtension( parser, listener ) + // ); + // loader.register( ( parser ) => { + // return new VRMLoaderPlugin( parser ); + // } ); + // } ); + + // if(someSceneState?.userData?.gltfExtensions?.VRM){ + // const playerController = someSceneState.userData.vrm; + // const { camera } = useThree(); + // useFrame(() => { + // const offsetZ = camera.position.z - 0.4; + // const offsetY = camera.position.y - 10; + // playerController.scene.position.set( camera.position.x, offsetY, offsetZ ); + // playerController.scene.rotation.set( camera.rotation.x, camera.rotation.y, camera.rotation.z ); + // }); + // VRMUtils.rotateVRM0( playerController ); + // const rotationVRM = playerController.scene.rotation.y; + // playerController.scene.rotation.set( 0, rotationVRM, 0 ); + // playerController.scene.scale.set( 1, 1, 1 ); + // gltf.scene.position.set( 0, props.positionY, 0 ); + // gltf.scene.rotation.set( 0, props.rotationY, 0 ); + // gltf.scene.scale.set( props.scale, props.scale, props.scale ); + // return <>; + // } + // End controller. + + if(gltf?.userData?.gltfExtensions?.VRM){ + const vrm = gltf.userData.vrm; + vrm.scene.position.set( 0, props.positionY, 0 ); + VRMUtils.rotateVRM0( vrm ); + const rotationVRM = vrm.scene.rotation.y + parseFloat(props.rotationY); + vrm.scene.rotation.set( 0, rotationVRM, 0 ); + vrm.scene.scale.set( props.scale, props.scale, props.scale ); + return ; + } + gltf.scene.position.set( 0, props.positionY, 0 ); + gltf.scene.rotation.set( 0, props.rotationY, 0 ); + gltf.scene.scale.set( props.scale, props.scale, props.scale ); + return <>; +} + +function ModelObject( model ) { + const [ url, set ] = useState( model.url ); + useEffect( () => { + setTimeout( () => set( model.url ), 2000 ); + }, [] ); + const [ listener ] = useState( () => new THREE.AudioListener() ); + + useThree( ( { camera } ) => { + camera.add( listener ); + } ); + + const gltf = useLoader( GLTFLoader, url, ( loader ) => { + loader.register( + ( parser ) => new GLTFAudioEmitterExtension( parser, listener ) + ); + loader.register( ( parser ) => { + return new VRMLoaderPlugin( parser ); + } ); + } ); + + const { actions } = useAnimations( gltf.animations, gltf.scene ); + + const animationList = model.animations ? model.animations.split( ',' ) : ''; + useEffect( () => { + if ( animationList ) { + animationList.forEach( ( name ) => { + if ( Object.keys( actions ).includes( name ) ) { + actions[ name ].play(); + } + } ); + } + }, [] ); + if(gltf?.userData?.gltfExtensions?.VRM){ + const vrm = gltf.userData.vrm; + vrm.scene.position.set( model.positionX, model.positionY, model.positionZ ); + VRMUtils.rotateVRM0( vrm ); + const rotationVRM = vrm.scene.rotation.y + parseFloat(0); + vrm.scene.rotation.set( 0, rotationVRM, 0 ); + vrm.scene.scale.set( 1, 1, 1 ); + // vrm.scene.scale.set( props.scale, props.scale, props.scale ); + return ; + } + gltf.scene.position.set( model.positionX, model.positionY, model.positionZ ); + gltf.scene.rotation.set( 0, 0, 0 ); + gltf.scene.scale.set(model.scaleX , model.scaleY, model.scaleZ ); + // console.log(model.rotationX, model.rotationY, model.rotationZ); + gltf.scene.rotation.set(model.rotationX , model.rotationY, model.rotationZ ); + // gltf.scene.scale.set( props.scale, props.scale, props.scale ); + return <>; +} + + +function Sky( sky ) { + // console.log(sky.src); + const skyUrl = sky.src[0].querySelector( 'p.sky-block-url' ) + ? sky.src[0].querySelector( 'p.sky-block-url' ).innerText + : ''; + + const texture_1 = useLoader(THREE.TextureLoader, skyUrl); + + return ( + + + + + ); +} + +function ThreeImage( threeImage ) { + // console.log(threeImage.aspectWidth, threeImage.aspectHeight); + const texture_2 = useLoader(THREE.TextureLoader, threeImage.url); + + return ( + + + + + ); +} + +function ThreeVideo(threeVideo) { + console.log(threeVideo); + const clicked = true; + const [video] = useState(() => Object.assign(document.createElement('video'), { src: threeVideo.url, crossOrigin: 'Anonymous', loop: true, muted: true })); + + useEffect(() => void (clicked && video.play()), [video, clicked]); + + return ( + + + + + + + ); +} + + +function Floor( props ) { + return ( + + + + + ); +} + +export default function EnvironmentFront( props ) { + if ( props.deviceTarget === 'vr' ) { + return ( + <> + + + + + + + + + { props.threeUrl && ( + <> + + + + + { props.threeUrl && ( + <> + + + )} + { Object.values(props.imagesToAdd).map((item, index)=>{ + const imagePosX = item.querySelector( 'p.image-block-positionX' ) + ? item.querySelector( 'p.image-block-positionX' ).innerText + : ''; + + const imagePosY = item.querySelector( 'p.image-block-positionY' ) + ? item.querySelector( 'p.image-block-positionY' ).innerText + : ''; + + const imagePosZ = item.querySelector( 'p.image-block-positionZ' ) + ? item.querySelector( 'p.image-block-positionZ' ).innerText + : ''; + + const imageScaleX = item.querySelector( 'p.image-block-scaleX' ) + ? item.querySelector( 'p.image-block-scaleX' ).innerText + : ''; + + const imageScaleY = item.querySelector( 'p.image-block-scaleY' ) + ? item.querySelector( 'p.image-block-scaleY' ).innerText + : ''; + + const imageScaleZ = item.querySelector( 'p.image-block-scaleZ' ) + ? item.querySelector( 'p.image-block-scaleZ' ).innerText + : ''; + + const imageRotationX = item.querySelector( 'p.image-block-rotationX' ) + ? item.querySelector( 'p.image-block-rotationX' ).innerText + : ''; + + const imageRotationY = item.querySelector( 'p.image-block-rotationY' ) + ? item.querySelector( 'p.image-block-rotationY' ).innerText + : ''; + + const imageRotationZ = item.querySelector( 'p.image-block-rotationZ' ) + ? item.querySelector( 'p.image-block-rotationZ' ).innerText + : ''; + + const imageUrl = item.querySelector( 'p.image-block-url' ) + ? item.querySelector( 'p.image-block-url' ).innerText + : ''; + + const aspectHeight = item.querySelector( 'p.image-block-aspect-height' ) + ? item.querySelector( 'p.image-block-aspect-height' ).innerText + : ''; + + const aspectWidth = item.querySelector( 'p.image-block-aspect-width' ) + ? item.querySelector( 'p.image-block-aspect-width' ).innerText + : ''; + + return(); + })} + { Object.values(props.videosToAdd).map((item, index)=>{ + const videoPosX = item.querySelector( 'p.video-block-positionX' ) + ? item.querySelector( 'p.video-block-positionX' ).innerText + : ''; + + const videoPosY = item.querySelector( 'p.video-block-positionY' ) + ? item.querySelector( 'p.video-block-positionY' ).innerText + : ''; + + const videoPosZ = item.querySelector( 'p.video-block-positionZ' ) + ? item.querySelector( 'p.video-block-positionZ' ).innerText + : ''; + + const videoScaleX = item.querySelector( 'p.video-block-scaleX' ) + ? item.querySelector( 'p.video-block-scaleX' ).innerText + : ''; + + const videoScaleY = item.querySelector( 'p.video-block-scaleY' ) + ? item.querySelector( 'p.video-block-scaleY' ).innerText + : ''; + + const videoScaleZ = item.querySelector( 'p.video-block-scaleZ' ) + ? item.querySelector( 'p.video-block-scaleZ' ).innerText + : ''; + + const videoRotationX = item.querySelector( 'p.video-block-rotationX' ) + ? item.querySelector( 'p.video-block-rotationX' ).innerText + : ''; + + const videoRotationY = item.querySelector( 'p.video-block-rotationY' ) + ? item.querySelector( 'p.video-block-rotationY' ).innerText + : ''; + + const videoRotationZ = item.querySelector( 'p.video-block-rotationZ' ) + ? item.querySelector( 'p.video-block-rotationZ' ).innerText + : ''; + + const videoUrl = item.querySelector( 'p.video-block-url' ) + ? item.querySelector( 'p.video-block-url' ).innerText + : ''; + console.log("no url?", videoUrl); + console.log(item); + + const aspectHeight = item.querySelector( 'p.video-block-aspect-height' ) + ? item.querySelector( 'p.video-block-aspect-height' ).innerText + : ''; + + const aspectWidth = item.querySelector( 'p.video-block-aspect-width' ) + ? item.querySelector( 'p.video-block-aspect-width' ).innerText + : ''; + + return(); + })} + + { Object.values(props.modelsToAdd).map((model, index)=>{ + const modelPosX = model.querySelector( 'p.model-block-position-x' ) + ? model.querySelector( 'p.model-block-position-x' ).innerText + : ''; + + const modelPosY = model.querySelector( 'p.model-block-position-y' ) + ? model.querySelector( 'p.model-block-position-y' ).innerText + : ''; + + const modelPosZ = model.querySelector( 'p.model-block-position-z' ) + ? model.querySelector( 'p.model-block-position-z' ).innerText + : ''; + + const modelScaleX = model.querySelector( 'p.model-block-scale-x' ) + ? model.querySelector( 'p.model-block-scale-x' ).innerText + : ''; + + const modelScaleY = model.querySelector( 'p.model-block-scale-y' ) + ? model.querySelector( 'p.model-block-scale-y' ).innerText + : ''; + + const modelScaleZ = model.querySelector( 'p.model-block-scale-z' ) + ? model.querySelector( 'p.model-block-scale-z' ).innerText + : ''; + + const modelRotationX = model.querySelector( 'p.model-block-rotation-x' ) + ? model.querySelector( 'p.model-block-rotation-x' ).innerText + : ''; + + const modelRotationY = model.querySelector( 'p.model-block-rotation-y' ) + ? model.querySelector( 'p.model-block-rotation-y' ).innerText + : ''; + + const modelRotationZ = model.querySelector( 'p.model-block-rotation-z' ) + ? model.querySelector( 'p.model-block-rotation-z' ).innerText + : ''; + + const url = model.querySelector( 'p.model-block-url' ) + ? model.querySelector( 'p.model-block-url' ).innerText + : ''; + + return(); + })} + + + + + + ) } + + + {/* */} + + + ); + } + if ( props.deviceTarget === '2d' ) { + return ( + <> + + + + + { props.threeUrl && ( + + ) } + + + + { props.hasTip === '1' ? ( +

Click and drag ^

+ ) : ( +

+ ) } + + ); + } +} diff --git a/blocks/environment/components/TeleportTravel.js b/blocks/environment/components/TeleportTravel.js new file mode 100644 index 0000000..3c0b646 --- /dev/null +++ b/blocks/environment/components/TeleportTravel.js @@ -0,0 +1,103 @@ +import { Raycaster, Vector3 } from 'three'; +import { useXR, Interactive } from '@react-three/xr'; +import { useFrame } from '@react-three/fiber'; +import { useCallback, useRef, useState } from 'react'; + +export function TeleportIndicator( props ) { + return ( + <> + + + + + + + ); +} + +export default function TeleportTravel( props ) { + const { + centerOnTeleport, + Indicator = TeleportIndicator, + useNormal = true, + } = props; + const [ isHovered, setIsHovered ] = useState( false ); + const target = useRef(); + const targetLoc = useRef(); + const ray = useRef( new Raycaster() ); + + const rayDir = useRef( { + pos: new Vector3(), + dir: new Vector3(), + } ); + + const { controllers, player } = useXR(); + + useFrame( () => { + if ( + isHovered && + controllers.length > 0 && + ray.current && + target.current && + targetLoc.current + ) { + controllers[ 0 ].controller.getWorldDirection( rayDir.current.dir ); + controllers[ 0 ].controller.getWorldPosition( rayDir.current.pos ); + rayDir.current.dir.multiplyScalar( -1 ); + ray.current.set( rayDir.current.pos, rayDir.current.dir ); + + const [ intersection ] = ray.current.intersectObject( + target.current + ); + + if ( intersection ) { + if ( useNormal ) { + const p = intersection.point; + + targetLoc.current.position.set( 0, 0, 0 ); + + const n = intersection.face.normal.clone(); + n.transformDirection( intersection.object.matrixWorld ); + + targetLoc.current.lookAt( n ); + targetLoc.current.rotateOnAxis( + new Vector3( 1, 0, 0 ), + Math.PI / 2 + ); + targetLoc.current.position.copy( p ); + } else { + targetLoc.current.position.copy( intersection.point ); + } + } + } + } ); + + const click = useCallback( () => { + if ( isHovered ) { + player.position.copy( targetLoc.current.position ); + } + }, [ centerOnTeleport, isHovered, useNormal ] ); + + return ( + <> + { isHovered && ( + + + + ) } + setIsHovered( true ) } + onBlur={ () => setIsHovered( false ) } + > + { props.children } + + + ); +} diff --git a/blocks/environment/components/ThreeObjectEdit.js b/blocks/environment/components/ThreeObjectEdit.js new file mode 100644 index 0000000..2ae77f2 --- /dev/null +++ b/blocks/environment/components/ThreeObjectEdit.js @@ -0,0 +1,101 @@ +import * as THREE from 'three'; +import React, { Suspense, useRef, useState, useEffect } from 'react'; +import { Canvas, useLoader, useFrame, useThree } from '@react-three/fiber'; +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; +import { + PerspectiveCamera, + OrbitControls, + useAnimations, +} from '@react-three/drei'; +import { VRMUtils, VRMLoaderPlugin } from '@pixiv/three-vrm' +import { GLTFAudioEmitterExtension } from 'three-omi'; + +function ThreeObject( props ) { + const [ url, set ] = useState( props.url ); + useEffect( () => { + setTimeout( () => set( props.url ), 2000 ); + }, [] ); + const [ listener ] = useState( () => new THREE.AudioListener() ); + + useThree( ( { camera } ) => { + camera.add( listener ); + } ); + + const gltf = useLoader( GLTFLoader, url, ( loader ) => { + loader.register( + ( parser ) => new GLTFAudioEmitterExtension( parser, listener ) + ); + loader.register( ( parser ) => { + + return new VRMLoaderPlugin( parser ); + + } ); + + } ); + + const { actions } = useAnimations( gltf.animations, gltf.scene ); + + const animationList = props.animations ? props.animations.split( ',' ) : ''; + + useEffect( () => { + if ( animationList ) { + animationList.forEach( ( name ) => { + if ( Object.keys( actions ).includes( name ) ) { + actions[ name ].play(); + } + } ); + } + }, [] ); + + if(gltf?.userData?.gltfExtensions?.VRM){ + const vrm = gltf.userData.vrm; + vrm.scene.position.set( 0, props.positionY, 0 ); + VRMUtils.rotateVRM0( vrm ); + const rotationVRM = vrm.scene.rotation.y + parseFloat(props.rotationY); + vrm.scene.rotation.set( 0, rotationVRM, 0 ); + vrm.scene.scale.set( props.scale, props.scale, props.scale ); + return ; + } + gltf.scene.position.set( 0, props.positionY, 0 ); + gltf.scene.rotation.set( 0, props.rotationY, 0 ); + gltf.scene.scale.set( props.scale, props.scale, props.scale ); + return ; +} + +export default function ThreeObjectEdit( props ) { + return ( + <> + + + + + { props.url && ( + + + + ) } + + + + ); +} diff --git a/blocks/environment/editor.scss b/blocks/environment/editor.scss new file mode 100644 index 0000000..a41c19e --- /dev/null +++ b/blocks/environment/editor.scss @@ -0,0 +1,48 @@ + + .wp-block-three-object-block { + border: 1px dotted #f00; +} + .glb-preview-container { + padding: 100px; + text-align: center; + align-items: center; + align-content: center; + background-color:#f2f2f2; + } + + .glb-preview-container button{ + padding: 15px; + border-radius: 30px; +} + +.three-object-viewer-button { + background-color:rgb(199, 254, 0); + color: black; + border: solid 1.5px black; +} + +.glb-preview-container button:hover{ + border-radius: 30px; + background-color:rgb(156, 199, 0); + cursor: pointer; +} +.three-object-block-tip { + overflow-wrap: break-word; + background-color: black; + color: white; + padding: 10px; + font-weight: 500; + max-width: 160px; + font-size: 12px; + margin-top: 0px; + margin: 0 auto; + text-align: center; +} + +.three-object-block-url-input { + padding-bottom: 20px; +} + +.three-object-block-url-input input{ + height: 40px; +} diff --git a/blocks/environment/frontend.js b/blocks/environment/frontend.js new file mode 100644 index 0000000..0a4c388 --- /dev/null +++ b/blocks/environment/frontend.js @@ -0,0 +1,72 @@ +const { Component, render } = wp.element; + +import EnvironmentFront from './components/EnvironmentFront'; + +const threeApp = document.querySelectorAll( '.three-object-three-app-environment' ); +const modelsToAdd = document.querySelectorAll( '.three-object-three-app-model-block' ); +const sky = document.querySelectorAll( '.three-object-three-app-sky-block' ); +const imagesToAdd = document.querySelectorAll( '.three-object-three-app-image-block' ); +const videosToAdd = document.querySelectorAll( '.three-object-three-app-video-block' ); + +threeApp.forEach( ( threeApp ) => { + if ( threeApp ) { + const threeUrl = threeApp.querySelector( 'p.three-object-block-url' ) + ? threeApp.querySelector( 'p.three-object-block-url' ).innerText + : ''; + const deviceTarget = threeApp.querySelector( + 'p.three-object-block-device-target' + ) + ? threeApp.querySelector( 'p.three-object-block-device-target' ) + .innerText + : '2D'; + const backgroundColor = threeApp.querySelector( + 'p.three-object-background-color' + ) + ? threeApp.querySelector( 'p.three-object-background-color' ).innerText + : '#ffffff'; + const zoom = threeApp.querySelector( 'p.three-object-zoom' ) + ? threeApp.querySelector( 'p.three-object-zoom' ).innerText + : 90; + const scale = threeApp.querySelector( 'p.three-object-scale' ) + ? threeApp.querySelector( 'p.three-object-scale' ).innerText + : 1; + const hasZoom = threeApp.querySelector( 'p.three-object-has-zoom' ) + ? threeApp.querySelector( 'p.three-object-has-zoom' ).innerText + : false; + const hasTip = threeApp.querySelector( 'p.three-object-has-tip' ) + ? threeApp.querySelector( 'p.three-object-has-tip' ).innerText + : true; + const positionY = threeApp.querySelector( 'p.three-object-position-y' ) + ? threeApp.querySelector( 'p.three-object-position-y' ).innerText + : 0; + const rotationY = threeApp.querySelector( 'p.three-object-rotation-y' ) + ? threeApp.querySelector( 'p.three-object-rotation-y' ).innerText + : 0; + const animations = threeApp.querySelector( 'p.three-object-animations' ) + ? threeApp.querySelector( 'p.three-object-animations' ).innerText + : ''; + + render( + , + threeApp + ); + } +} ); + + diff --git a/blocks/environment/index.js b/blocks/environment/index.js new file mode 100644 index 0000000..22b14b6 --- /dev/null +++ b/blocks/environment/index.js @@ -0,0 +1,197 @@ +import { registerBlockType } from '@wordpress/blocks'; +import Edit from './Edit'; +import Save from './Save'; +import { useBlockProps } from '@wordpress/block-editor'; + +const icon = ( + + + + + +); + +const blockConfig = require( './block.json' ); +registerBlockType( blockConfig.name, { + ...blockConfig, + icon: icon, + apiVersion: 2, + edit: Edit, + save: Save, + deprecated: [ + { + attributes: { + bg_color: { + type: 'string', + default: '#FFFFFF', + }, + zoom: { + type: 'integer', + default: 90, + }, + scale: { + type: 'integer', + default: 1, + }, + positionX: { + type: 'integer', + default: 0, + }, + positionY: { + type: 'integer', + default: 0, + }, + rotationY: { + type: 'integer', + default: 0, + }, + threeObjectUrl: { + type: 'string', + default: null, + }, + hasZoom: { + type: 'bool', + default: false, + }, + hasTip: { + type: 'bool', + default: true, + }, + deviceTarget: { + type: 'string', + default: '2d', + }, + }, + save( props ) { + return ( +
+ <> +
+

+ { props.attributes.deviceTarget } +

+

+ { props.attributes.threeObjectUrl } +

+

+ { props.attributes.scale } +

+

+ { props.attributes.bg_color } +

+

+ { props.attributes.zoom } +

+

+ { props.attributes.hasZoom ? 1 : 0 } +

+

+ { props.attributes.hasTip ? 1 : 0 } +

+

+ { props.attributes.positionY } +

+

+ { props.attributes.rotationY } +

+

+ { props.attributes.scale } +

+
+ +
+ ); + }, + }, + { + attributes: { + bg_color: { + type: 'string', + default: '#FFFFFF', + }, + zoom: { + type: 'integer', + default: 90, + }, + scale: { + type: 'integer', + default: 1, + }, + positionX: { + type: 'integer', + default: 0, + }, + positionY: { + type: 'integer', + default: 0, + }, + rotationY: { + type: 'integer', + default: 0, + }, + threeObjectUrl: { + type: 'string', + default: null, + }, + hasZoom: { + type: 'bool', + default: false, + }, + hasTip: { + type: 'bool', + default: true, + }, + deviceTarget: { + type: 'string', + default: '2d', + }, + animations: { + type: 'string', + default: '', + } + }, + save( props ) { + return ( +
+ <> +
+

+ { props.attributes.deviceTarget } +

+

+ { props.attributes.threeObjectUrl } +

+

{ props.attributes.scale }

+

+ { props.attributes.bg_color } +

+

{ props.attributes.zoom }

+

+ { props.attributes.hasZoom ? 1 : 0 } +

+

+ { props.attributes.hasTip ? 1 : 0 } +

+

+ { props.attributes.positionY } +

+

+ { props.attributes.rotationY } +

+

{ props.attributes.scale }

+

+ { props.attributes.animations } +

+
+ +
+ ); + }, + }, + ], +} ); diff --git a/blocks/environment/init.php b/blocks/environment/init.php new file mode 100644 index 0000000..4d16124 --- /dev/null +++ b/blocks/environment/init.php @@ -0,0 +1,10 @@ + { + setAttributes( { positionX: positionX } ); + }; + const onChangePositionY = ( positionY ) => { + setAttributes( { positionY: positionY } ); + }; + const onChangePositionZ = ( positionZ ) => { + setAttributes( { positionZ: positionZ } ); + }; + + const onChangeRotationX = ( rotationX ) => { + setAttributes( { rotationX: rotationX } ); + }; + const onChangeRotationY = ( rotationY ) => { + setAttributes( { rotationY: rotationY } ); + }; + const onChangeRotationZ = ( rotationZ ) => { + setAttributes( { rotationZ: rotationZ } ); + }; + + const onChangeScaleX = ( scaleX ) => { + setAttributes( { scaleX: scaleX } ); + }; + const onChangeScaleY = ( scaleY ) => { + setAttributes( { scaleY: scaleY } ); + }; + const onChangeScaleZ = ( scaleZ ) => { + setAttributes( { scaleZ: scaleZ } ); + }; + + const onChangeAnimations = ( animations ) => { + setAttributes( { animations: animations } ); + }; + + const onImageSelect = ( imageObject ) => { + setAttributes( { threeObjectUrl: null } ); + setAttributes( { threeObjectUrl: imageObject.url } ); + }; + + const onChangeCollidable = ( collidableSetting ) => { + setAttributes( { collidable: collidableSetting } ); + }; + + const [ enteredURL, setEnteredURL ] = useState( "" ); + + const { mediaUpload } = wp.editor; + + const ALLOWED_MEDIA_TYPES = [ + 'model/gltf-binary', + 'application/octet-stream', + ]; + + const MyDropZone = () => { + const [ hasDropped, setHasDropped ] = useState( false ); + return ( +
+ { hasDropped ? 'Dropped!' : 'Drop a glb here or' } + + mediaUpload( { + allowedTypes: ALLOWED_MEDIA_TYPES, + filesList: files, + onFileChange: ( [ images ] ) => { + onImageSelect( images ); + }, + } ) + } + /> +
+ ); + }; + + function handleClick(objectURL){ + if(objectURL){ + console.log("success good job", objectURL); + onImageSelect(objectURL); + } + console.log("fail", objectURL); + } + + + return ( +
+ + + + + + select a glb file from your media library to + render an object in the canvas: + + + + + onImageSelect( imageObject ) + } + type="image" + label="GLB File" + allowedTypes={ ALLOWED_MEDIA_TYPES } + value={ attributes.threeObjectUrl } + render={ ( { open } ) => ( + + ) } + /> + + + + + { + onChangeCollidable( e ); + } } + /> + + + + onChangeAnimations( value ) + } + /> + + + + { __( 'Position', 'three-object-viewer' ) } + + + + + onChangePositionX( value ) + } + /> + + onChangePositionY( value ) + } + /> + + onChangePositionZ( value ) + } + /> + + + + { __( 'Rotation', 'three-object-viewer' ) } + + + + + onChangeRotationX( value ) + } + /> + + onChangeRotationY( value ) + } + /> + + onChangeRotationZ( value ) + } + /> + + + + { __( 'Scale', 'three-object-viewer' ) } + + + + + onChangeScaleX( value ) + } + /> + + onChangeScaleY( value ) + } + /> + + onChangeScaleZ( value ) + } + /> + + + + + { isSelected ? ( + <> + { attributes.threeObjectUrl ? ( + + ) : ( +
+ + +
+ + Select a glb file to render in the canvas: + + {/*
+ setEnteredURL(e.target.value)}> + +
*/} + + onImageSelect( imageObject ) + } + type="image" + allowedTypes={ ALLOWED_MEDIA_TYPES } + value={ attributes.threeObjectUrl } + render={ ( { open } ) => ( + + ) } + /> +
+
+ ) } + + ) : ( + <> + { attributes.threeObjectUrl ? ( + + ) : ( +
+ +
+ + Select a glb file to render in the canvas: + + {/*
+ console.log(e.target.value) && setEnteredURL(e.target.value)}> + +
*/} +
+ + onImageSelect( imageObject ) + } + type="image" + allowedTypes={ ALLOWED_MEDIA_TYPES } + value={ attributes.threeObjectUrl } + render={ ( { open } ) => ( + + ) } + /> +
+ ) } + + ) } +
+ ); +} diff --git a/blocks/model-block/Edit.test.js b/blocks/model-block/Edit.test.js new file mode 100644 index 0000000..ab10afe --- /dev/null +++ b/blocks/model-block/Edit.test.js @@ -0,0 +1,57 @@ + +//Import React +import React from 'react'; +//Import test renderer +import { render, fireEvent, cleanup } from '@testing-library/react'; +//Import component to test +import { Editor } from './Edit'; + + +describe("Editor componet", () => { + afterEach(cleanup); + it('matches snapshot when selected', () => { + const onChange = jest.fn(); + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot when not selected', () => { + const onChange = jest.fn(); + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("Calls the onchange function", () => { + const onChange = jest.fn(); + const { getByDisplayValue } = render(); + fireEvent.change(getByDisplayValue("Salad"), { + target: { value: "New Value" } + }); + expect(onChange).toHaveBeenCalledTimes(1); + }); + + it("Passes updated value, not event to onChange callback", () => { + const onChange = jest.fn(); + const { getByDisplayValue } = render(); + fireEvent.change(getByDisplayValue("Seltzer"), { + target: { value: "Boring Water" } + }); + expect(onChange).toHaveBeenCalledWith("Boring Water"); + }); +}); \ No newline at end of file diff --git a/blocks/model-block/Save.js b/blocks/model-block/Save.js new file mode 100644 index 0000000..518ee4a --- /dev/null +++ b/blocks/model-block/Save.js @@ -0,0 +1,28 @@ +import { __ } from '@wordpress/i18n'; +import { useBlockProps } from '@wordpress/block-editor'; + +export default function save( { attributes } ) { + return ( +
+ <> +
+

+ { attributes.threeObjectUrl } +

+

{ attributes.scaleX }

+

{ attributes.scaleY }

+

{ attributes.scaleZ }

+

{ attributes.positionX }

+

{ attributes.positionY }

+

{ attributes.positionZ }

+

{ attributes.rotationX }

+

{ attributes.rotationY }

+

{ attributes.rotationZ }

+

+ { attributes.animations } +

+
+ +
+ ); +} diff --git a/blocks/model-block/block.json b/blocks/model-block/block.json new file mode 100644 index 0000000..3fe62a1 --- /dev/null +++ b/blocks/model-block/block.json @@ -0,0 +1,64 @@ +{ + "name": "three-object-viewer/model-block", + "title": "Model Block", + "description": "A 3D model for your VersePress environment", + "attributes": { + "scaleX": { + "type": "int", + "default":1 + }, + "scaleY": { + "type": "int", + "default":1 + }, + "scaleZ": { + "type": "int", + "default":1 + }, + "positionX": { + "type": "int", + "default":0 + }, + "positionY": { + "type": "int", + "default":0 + }, + "positionZ": { + "type": "int", + "default":0 + }, + "rotationX": { + "type": "int", + "default":0 + }, + "rotationY": { + "type": "int", + "default":0 + }, + "rotationZ": { + "type": "int", + "default":0 + }, + "threeObjectUrl": { + "type": "string", + "default": null + }, + "animations": { + "type": "string", + "default": "" + }, + "collidable": { + "type": "boolean", + "default": false + } + }, + "category": "3D", + "apiVersion": 2, + "supports": { + "html": false, + "multiple": true + }, + "editorScript": "file:../../build/block-model-block.js", + "editorStyle": "file:../../build/block-model-block.css", + "style": "file:../../build/block-model-block.css" +} diff --git a/blocks/model-block/components/ModelEdit.js b/blocks/model-block/components/ModelEdit.js new file mode 100644 index 0000000..c71bd1b --- /dev/null +++ b/blocks/model-block/components/ModelEdit.js @@ -0,0 +1,105 @@ +import * as THREE from 'three'; +import React, { Suspense, useRef, useState, useEffect } from 'react'; +import { Canvas, useLoader, useFrame, useThree } from '@react-three/fiber'; +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; +import { + OrthographicCamera, + PerspectiveCamera, + OrbitControls, + useAnimations, +} from '@react-three/drei'; +import { VRM, VRMUtils, VRMSchema, VRMLoaderPlugin } from '@pixiv/three-vrm' +import { GLTFAudioEmitterExtension } from 'three-omi'; + + +function ThreeObject( props ) { + const [ url, set ] = useState( props.url ); + useEffect( () => { + setTimeout( () => set( props.url ), 2000 ); + }, [] ); + const [ listener ] = useState( () => new THREE.AudioListener() ); + + useThree( ( { camera } ) => { + camera.add( listener ); + } ); + + const gltf = useLoader( GLTFLoader, url, ( loader ) => { + loader.register( + ( parser ) => new GLTFAudioEmitterExtension( parser, listener ) + ); + loader.register( ( parser ) => { + + return new VRMLoaderPlugin( parser ); + + } ); + + } ); + + const { actions } = useAnimations( gltf.animations, gltf.scene ); + + const animationList = props.animations ? props.animations.split( ',' ) : ''; + + useEffect( () => { + if ( animationList ) { + animationList.forEach( ( name ) => { + if ( Object.keys( actions ).includes( name ) ) { + actions[ name ].play(); + } + } ); + } + }, [] ); + + if(gltf?.userData?.gltfExtensions?.VRM){ + const vrm = gltf.userData.vrm; + vrm.scene.position.set( 0, props.positionY, 0 ); + VRMUtils.rotateVRM0( vrm ); + const rotationVRM = vrm.scene.rotation.y + parseFloat(props.rotationY); + vrm.scene.rotation.set( 0, rotationVRM, 0 ); + vrm.scene.scale.set( props.scale, props.scale, props.scale ); + return ; + } + gltf.scene.position.set( 0, 0, 0 ); + gltf.scene.rotation.set( 0, 0, 0 ); + gltf.scene.scale.set( 1, 1, 1 ); + return ; +} + +export default function ModelEdit( props ) { + return ( + <> + + + + + { props.url && ( + + + + ) } + + + { props.hasTip && ( +

Click and drag ^

+ ) } + + ); +} diff --git a/blocks/model-block/editor.scss b/blocks/model-block/editor.scss new file mode 100644 index 0000000..9ed5c19 --- /dev/null +++ b/blocks/model-block/editor.scss @@ -0,0 +1,55 @@ + + .wp-block-three-object-block { + border: 1px dotted #f00; +} + .glb-preview-container { + padding: 100px; + text-align: center; + align-items: center; + align-content: center; + background-color:#f2f2f2; + } + + .glb-preview-container button{ + padding: 15px; + border-radius: 30px; +} + +.three-object-viewer-button { + background-color:rgb(199, 254, 0); + color: black; + border: solid 1.5px black; +} + +.glb-preview-container button:hover{ + border-radius: 30px; + background-color:rgb(156, 199, 0); + cursor: pointer; +} +.three-object-block-tip { + overflow-wrap: break-word; + background-color: black; + color: white; + padding: 10px; + font-weight: 500; + max-width: 160px; + font-size: 12px; + margin-top: 0px; + margin: 0 auto; + text-align: center; +} + +.three-object-block-url-input { + padding-bottom: 20px; +} + +.three-object-block-url-input input{ + height: 40px; +} + +.block-editor-block-inspector .components-base-control.position-inputs:last-child { + margin-bottom: 24px !important; +} +.block-editor-block-inspector .components-base-control.position-inputs { + padding-right: 5px; +} diff --git a/blocks/model-block/index.js b/blocks/model-block/index.js new file mode 100644 index 0000000..22b14b6 --- /dev/null +++ b/blocks/model-block/index.js @@ -0,0 +1,197 @@ +import { registerBlockType } from '@wordpress/blocks'; +import Edit from './Edit'; +import Save from './Save'; +import { useBlockProps } from '@wordpress/block-editor'; + +const icon = ( + + + + + +); + +const blockConfig = require( './block.json' ); +registerBlockType( blockConfig.name, { + ...blockConfig, + icon: icon, + apiVersion: 2, + edit: Edit, + save: Save, + deprecated: [ + { + attributes: { + bg_color: { + type: 'string', + default: '#FFFFFF', + }, + zoom: { + type: 'integer', + default: 90, + }, + scale: { + type: 'integer', + default: 1, + }, + positionX: { + type: 'integer', + default: 0, + }, + positionY: { + type: 'integer', + default: 0, + }, + rotationY: { + type: 'integer', + default: 0, + }, + threeObjectUrl: { + type: 'string', + default: null, + }, + hasZoom: { + type: 'bool', + default: false, + }, + hasTip: { + type: 'bool', + default: true, + }, + deviceTarget: { + type: 'string', + default: '2d', + }, + }, + save( props ) { + return ( +
+ <> +
+

+ { props.attributes.deviceTarget } +

+

+ { props.attributes.threeObjectUrl } +

+

+ { props.attributes.scale } +

+

+ { props.attributes.bg_color } +

+

+ { props.attributes.zoom } +

+

+ { props.attributes.hasZoom ? 1 : 0 } +

+

+ { props.attributes.hasTip ? 1 : 0 } +

+

+ { props.attributes.positionY } +

+

+ { props.attributes.rotationY } +

+

+ { props.attributes.scale } +

+
+ +
+ ); + }, + }, + { + attributes: { + bg_color: { + type: 'string', + default: '#FFFFFF', + }, + zoom: { + type: 'integer', + default: 90, + }, + scale: { + type: 'integer', + default: 1, + }, + positionX: { + type: 'integer', + default: 0, + }, + positionY: { + type: 'integer', + default: 0, + }, + rotationY: { + type: 'integer', + default: 0, + }, + threeObjectUrl: { + type: 'string', + default: null, + }, + hasZoom: { + type: 'bool', + default: false, + }, + hasTip: { + type: 'bool', + default: true, + }, + deviceTarget: { + type: 'string', + default: '2d', + }, + animations: { + type: 'string', + default: '', + } + }, + save( props ) { + return ( +
+ <> +
+

+ { props.attributes.deviceTarget } +

+

+ { props.attributes.threeObjectUrl } +

+

{ props.attributes.scale }

+

+ { props.attributes.bg_color } +

+

{ props.attributes.zoom }

+

+ { props.attributes.hasZoom ? 1 : 0 } +

+

+ { props.attributes.hasTip ? 1 : 0 } +

+

+ { props.attributes.positionY } +

+

+ { props.attributes.rotationY } +

+

{ props.attributes.scale }

+

+ { props.attributes.animations } +

+
+ +
+ ); + }, + }, + ], +} ); diff --git a/blocks/model-block/init.php b/blocks/model-block/init.php new file mode 100644 index 0000000..4d16124 --- /dev/null +++ b/blocks/model-block/init.php @@ -0,0 +1,10 @@ + ( + <> + {isSelected ? ( + + ) : ( +

{value}

+ )} + +); + +export default function Edit({ attributes, setAttributes, isSelected }) { + return ( +
+ setAttributes({ content })} + /> +
+ ); +} diff --git a/blocks/npc-block/Edit.test.js b/blocks/npc-block/Edit.test.js new file mode 100644 index 0000000..ab10afe --- /dev/null +++ b/blocks/npc-block/Edit.test.js @@ -0,0 +1,57 @@ + +//Import React +import React from 'react'; +//Import test renderer +import { render, fireEvent, cleanup } from '@testing-library/react'; +//Import component to test +import { Editor } from './Edit'; + + +describe("Editor componet", () => { + afterEach(cleanup); + it('matches snapshot when selected', () => { + const onChange = jest.fn(); + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot when not selected', () => { + const onChange = jest.fn(); + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("Calls the onchange function", () => { + const onChange = jest.fn(); + const { getByDisplayValue } = render(); + fireEvent.change(getByDisplayValue("Salad"), { + target: { value: "New Value" } + }); + expect(onChange).toHaveBeenCalledTimes(1); + }); + + it("Passes updated value, not event to onChange callback", () => { + const onChange = jest.fn(); + const { getByDisplayValue } = render(); + fireEvent.change(getByDisplayValue("Seltzer"), { + target: { value: "Boring Water" } + }); + expect(onChange).toHaveBeenCalledWith("Boring Water"); + }); +}); \ No newline at end of file diff --git a/blocks/npc-block/Save.js b/blocks/npc-block/Save.js new file mode 100644 index 0000000..3a499f1 --- /dev/null +++ b/blocks/npc-block/Save.js @@ -0,0 +1,6 @@ +import { __ } from '@wordpress/i18n'; +import { useBlockProps } from '@wordpress/block-editor'; + +export default function save({ attributes }) { + return
{attributes.content}
; +} \ No newline at end of file diff --git a/blocks/npc-block/block.json b/blocks/npc-block/block.json new file mode 100644 index 0000000..e197d03 --- /dev/null +++ b/blocks/npc-block/block.json @@ -0,0 +1,20 @@ +{ + "name": "three-object-viewer/npc-block", + "title": "NPC Block", + "description": "A NPC Block to make your experience a bit more personal.", + "attributes": { + "content": { + "type": "string", + "default": "Hi Roy" + } + }, + "category": "theme", + "apiVersion": 2, + "icon": "smiley", + "supports": { + "html": false + }, + "editorScript": "file:../../build/block-npc-block.js", + "editorStyle": "file:../../build/block-npc-block.css", + "style": "file:../../build/block-npc-block.css" +} diff --git a/blocks/npc-block/editor.scss b/blocks/npc-block/editor.scss new file mode 100644 index 0000000..318f617 --- /dev/null +++ b/blocks/npc-block/editor.scss @@ -0,0 +1,4 @@ + + .wp-block-npc-block { + border: 1px dotted #f00; +} diff --git a/blocks/npc-block/index.js b/blocks/npc-block/index.js new file mode 100644 index 0000000..3539797 --- /dev/null +++ b/blocks/npc-block/index.js @@ -0,0 +1,12 @@ + +import { registerBlockType } from "@wordpress/blocks"; +import Edit from './Edit'; +import Save from './Save'; + +const blockConfig = require('./block.json'); +registerBlockType(blockConfig.name, { + ...blockConfig, + apiVersion: 2, + edit: Edit, + save: Save +}); diff --git a/blocks/npc-block/init.php b/blocks/npc-block/init.php new file mode 100644 index 0000000..93dd084 --- /dev/null +++ b/blocks/npc-block/init.php @@ -0,0 +1,10 @@ + { + setAttributes( { skyUrl: null } ); + setAttributes( { skyUrl: imageObject.url } ); + }; + + const onChangeCollidable = ( zoomSetting ) => { + setAttributes( { hasZoom: zoomSetting } ); + }; + + + const { mediaUpload } = wp.editor; + + const ALLOWED_MEDIA_TYPES = [ + 'image' + ]; + + const MyDropZone = () => { + const [ hasDropped, setHasDropped ] = useState( false ); + return ( +
+ { hasDropped ? 'Dropped!' : 'Drop an image here or' } + + mediaUpload( { + allowedTypes: ALLOWED_MEDIA_TYPES, + filesList: files, + onFileChange: ( [ images ] ) => { + onImageSelect( images ); + }, + } ) + } + /> +
+ ); + }; + + function handleClick(objectURL){ + if(objectURL){ + console.log("success good job", objectURL); + onImageSelect(objectURL); + } + console.log("fail", objectURL); + } + + + return ( +
+ + + + + + Select an image to be used as your skybox. 360 spherical panoramics recommended: + + + + + onImageSelect( imageObject ) + } + type="image" + label="Sky File" + allowedTypes={ ALLOWED_MEDIA_TYPES } + value={ attributes.skyUrl } + render={ ( { open } ) => ( + + ) } + /> + + + + + { isSelected ? ( + <> + { attributes.skyUrl ? ( + <> + + ) : ( +
+ + +
+ + Select an image to be used as your skybox. 360 spherical panoramics recommended: + + + onImageSelect( imageObject ) + } + type="image" + allowedTypes={ ALLOWED_MEDIA_TYPES } + value={ attributes.skyUrl } + render={ ( { open } ) => ( + + ) } + /> +
+
+ ) } + + ) : ( + <> + { attributes.skyUrl ? ( + <> + + ) : ( +
+ +
+ + Select an image to be used as your skybox. 360 spherical panoramics recommended: + + {/*
+ console.log(e.target.value) && setEnteredURL(e.target.value)}> + +
*/} +
+ + onImageSelect( imageObject ) + } + type="image" + allowedTypes={ ALLOWED_MEDIA_TYPES } + value={ attributes.skyUrl } + render={ ( { open } ) => ( + + ) } + /> +
+ ) } + + ) } +
+ ); +} diff --git a/blocks/sky-block/Edit.test.js b/blocks/sky-block/Edit.test.js new file mode 100644 index 0000000..ab10afe --- /dev/null +++ b/blocks/sky-block/Edit.test.js @@ -0,0 +1,57 @@ + +//Import React +import React from 'react'; +//Import test renderer +import { render, fireEvent, cleanup } from '@testing-library/react'; +//Import component to test +import { Editor } from './Edit'; + + +describe("Editor componet", () => { + afterEach(cleanup); + it('matches snapshot when selected', () => { + const onChange = jest.fn(); + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot when not selected', () => { + const onChange = jest.fn(); + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("Calls the onchange function", () => { + const onChange = jest.fn(); + const { getByDisplayValue } = render(); + fireEvent.change(getByDisplayValue("Salad"), { + target: { value: "New Value" } + }); + expect(onChange).toHaveBeenCalledTimes(1); + }); + + it("Passes updated value, not event to onChange callback", () => { + const onChange = jest.fn(); + const { getByDisplayValue } = render(); + fireEvent.change(getByDisplayValue("Seltzer"), { + target: { value: "Boring Water" } + }); + expect(onChange).toHaveBeenCalledWith("Boring Water"); + }); +}); \ No newline at end of file diff --git a/blocks/sky-block/Save.js b/blocks/sky-block/Save.js new file mode 100644 index 0000000..ee57703 --- /dev/null +++ b/blocks/sky-block/Save.js @@ -0,0 +1,16 @@ +import { __ } from '@wordpress/i18n'; +import { useBlockProps } from '@wordpress/block-editor'; + +export default function save( { attributes } ) { + return ( +
+ <> +
+

+ { attributes.skyUrl } +

+
+ +
+ ); +} diff --git a/blocks/sky-block/block.json b/blocks/sky-block/block.json new file mode 100644 index 0000000..fa63499 --- /dev/null +++ b/blocks/sky-block/block.json @@ -0,0 +1,20 @@ +{ + "name": "three-object-viewer/sky-block", + "title": "Sky Block", + "description": "A sky your environment", + "attributes": { + "skyUrl": { + "type": "string", + "default": null + } + }, + "category": "3D", + "apiVersion": 2, + "supports": { + "html": false, + "multiple": false + }, + "editorScript": "file:../../build/block-sky-block.js", + "editorStyle": "file:../../build/block-sky-block.css", + "style": "file:../../build/block-sky-block.css" +} diff --git a/blocks/sky-block/components/SkyEdit.js b/blocks/sky-block/components/SkyEdit.js new file mode 100644 index 0000000..eb2cde8 --- /dev/null +++ b/blocks/sky-block/components/SkyEdit.js @@ -0,0 +1,66 @@ +import * as THREE from 'three'; +import React, { Suspense, useRef, useState, useEffect } from 'react'; +import { Canvas, useLoader, useFrame, useThree } from '@react-three/fiber'; +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; +import { + OrthographicCamera, + PerspectiveCamera, + OrbitControls, + useAnimations, +} from '@react-three/drei'; +import { VRM, VRMUtils, VRMSchema, VRMLoaderPlugin } from '@pixiv/three-vrm' +import { GLTFAudioEmitterExtension } from 'three-omi'; + + // Geometry +function Sphere(props) { + const texture_1 = useLoader(THREE.TextureLoader, props.url); + + return ( + + + + + ); +} + +function ThreeObject( props ) { + const [ url, set ] = useState( props.src ); + useEffect( () => { + setTimeout( () => set( props.src ), 2000 ); + }, [] ); + + return Sphere(props); +} + +export default function SkyEdit( props ) { + return ( + <> + + + + { props.src && ( + + + + ) } + + + + ); +} diff --git a/blocks/sky-block/editor.scss b/blocks/sky-block/editor.scss new file mode 100644 index 0000000..a41c19e --- /dev/null +++ b/blocks/sky-block/editor.scss @@ -0,0 +1,48 @@ + + .wp-block-three-object-block { + border: 1px dotted #f00; +} + .glb-preview-container { + padding: 100px; + text-align: center; + align-items: center; + align-content: center; + background-color:#f2f2f2; + } + + .glb-preview-container button{ + padding: 15px; + border-radius: 30px; +} + +.three-object-viewer-button { + background-color:rgb(199, 254, 0); + color: black; + border: solid 1.5px black; +} + +.glb-preview-container button:hover{ + border-radius: 30px; + background-color:rgb(156, 199, 0); + cursor: pointer; +} +.three-object-block-tip { + overflow-wrap: break-word; + background-color: black; + color: white; + padding: 10px; + font-weight: 500; + max-width: 160px; + font-size: 12px; + margin-top: 0px; + margin: 0 auto; + text-align: center; +} + +.three-object-block-url-input { + padding-bottom: 20px; +} + +.three-object-block-url-input input{ + height: 40px; +} diff --git a/blocks/sky-block/index.js b/blocks/sky-block/index.js new file mode 100644 index 0000000..22b14b6 --- /dev/null +++ b/blocks/sky-block/index.js @@ -0,0 +1,197 @@ +import { registerBlockType } from '@wordpress/blocks'; +import Edit from './Edit'; +import Save from './Save'; +import { useBlockProps } from '@wordpress/block-editor'; + +const icon = ( + + + + + +); + +const blockConfig = require( './block.json' ); +registerBlockType( blockConfig.name, { + ...blockConfig, + icon: icon, + apiVersion: 2, + edit: Edit, + save: Save, + deprecated: [ + { + attributes: { + bg_color: { + type: 'string', + default: '#FFFFFF', + }, + zoom: { + type: 'integer', + default: 90, + }, + scale: { + type: 'integer', + default: 1, + }, + positionX: { + type: 'integer', + default: 0, + }, + positionY: { + type: 'integer', + default: 0, + }, + rotationY: { + type: 'integer', + default: 0, + }, + threeObjectUrl: { + type: 'string', + default: null, + }, + hasZoom: { + type: 'bool', + default: false, + }, + hasTip: { + type: 'bool', + default: true, + }, + deviceTarget: { + type: 'string', + default: '2d', + }, + }, + save( props ) { + return ( +
+ <> +
+

+ { props.attributes.deviceTarget } +

+

+ { props.attributes.threeObjectUrl } +

+

+ { props.attributes.scale } +

+

+ { props.attributes.bg_color } +

+

+ { props.attributes.zoom } +

+

+ { props.attributes.hasZoom ? 1 : 0 } +

+

+ { props.attributes.hasTip ? 1 : 0 } +

+

+ { props.attributes.positionY } +

+

+ { props.attributes.rotationY } +

+

+ { props.attributes.scale } +

+
+ +
+ ); + }, + }, + { + attributes: { + bg_color: { + type: 'string', + default: '#FFFFFF', + }, + zoom: { + type: 'integer', + default: 90, + }, + scale: { + type: 'integer', + default: 1, + }, + positionX: { + type: 'integer', + default: 0, + }, + positionY: { + type: 'integer', + default: 0, + }, + rotationY: { + type: 'integer', + default: 0, + }, + threeObjectUrl: { + type: 'string', + default: null, + }, + hasZoom: { + type: 'bool', + default: false, + }, + hasTip: { + type: 'bool', + default: true, + }, + deviceTarget: { + type: 'string', + default: '2d', + }, + animations: { + type: 'string', + default: '', + } + }, + save( props ) { + return ( +
+ <> +
+

+ { props.attributes.deviceTarget } +

+

+ { props.attributes.threeObjectUrl } +

+

{ props.attributes.scale }

+

+ { props.attributes.bg_color } +

+

{ props.attributes.zoom }

+

+ { props.attributes.hasZoom ? 1 : 0 } +

+

+ { props.attributes.hasTip ? 1 : 0 } +

+

+ { props.attributes.positionY } +

+

+ { props.attributes.rotationY } +

+

{ props.attributes.scale }

+

+ { props.attributes.animations } +

+
+ +
+ ); + }, + }, + ], +} ); diff --git a/blocks/sky-block/init.php b/blocks/sky-block/init.php new file mode 100644 index 0000000..4d16124 --- /dev/null +++ b/blocks/sky-block/init.php @@ -0,0 +1,10 @@ + { + console.log(imageObject); + setAttributes( { videoUrl: null } ); + setAttributes( { videoUrl: imageObject.url, aspectHeight: imageObject.height, aspectWidth: imageObject.width } ); + }; + const onChangePositionX = ( positionX ) => { + setAttributes( { positionX: positionX } ); + }; + const onChangePositionY = ( positionY ) => { + setAttributes( { positionY: positionY } ); + }; + const onChangePositionZ = ( positionZ ) => { + setAttributes( { positionZ: positionZ } ); + }; + + const onChangeRotationX = ( rotationX ) => { + setAttributes( { rotationX: rotationX } ); + }; + const onChangeRotationY = ( rotationY ) => { + setAttributes( { rotationY: rotationY } ); + }; + const onChangeRotationZ = ( rotationZ ) => { + setAttributes( { rotationZ: rotationZ } ); + }; + + const onChangeScaleX = ( scaleX ) => { + setAttributes( { scaleX: scaleX } ); + }; + const onChangeScaleY = ( scaleY ) => { + setAttributes( { scaleY: scaleY } ); + }; + const onChangeScaleZ = ( scaleZ ) => { + setAttributes( { scaleZ: scaleZ } ); + }; + + const onChangeAutoPlay = ( autoPlaySetting ) => { + setAttributes( { autoPlay: autoPlaySetting } ); + }; + const onChangeLoop = ( loopSetting ) => { + setAttributes( { loop: loopSetting } ); + }; + const onChangePositional = ( positionalSetting ) => { + setAttributes( { positional: positionalSetting } ); + }; + const onChangeVolume = ( volumeSetting ) => { + setAttributes( { volume: volumeSetting } ); + }; + const onChangeConeInnerAngle = ( coneInnerAngleSetting ) => { + setAttributes( { coneInnerAngle: coneInnerAngleSetting } ); + }; + const onChangeConeOuterAngle = ( coneOuterAngleSetting ) => { + setAttributes( { coneOuterAngle: coneOuterAngleSetting } ); + }; + const onChangeConeOuterGain = ( coneOuterGainSetting ) => { + setAttributes( { coneOuterGain: coneOuterGainSetting } ); + }; + const onChangeDistanceModel = ( distanceModelSetting ) => { + setAttributes( { distanceModel: distanceModelSetting } ); + }; + const onChangeMaxDistance = ( maxDistanceSetting ) => { + setAttributes( { maxDistance: maxDistanceSetting } ); + }; + const onChangeRefDistance = ( refDistanceSetting ) => { + setAttributes( { refDistance: refDistanceSetting } ); + }; + + const onChangeRolloffFactor = ( rolloffFactorSetting ) => { + setAttributes( { rolloffFactor: rolloffFactorSetting } ); + }; + + + + const { mediaUpload } = wp.editor; + + const ALLOWED_MEDIA_TYPES = [ + 'audio' + ]; + + const MyDropZone = () => { + const [ hasDropped, setHasDropped ] = useState( false ); + return ( +
+ { hasDropped ? 'Dropped!' : 'Drop an image here or' } + + mediaUpload( { + allowedTypes: ALLOWED_MEDIA_TYPES, + filesList: files, + onFileChange: ( [ images ] ) => { + onImageSelect( images ); + }, + } ) + } + /> +
+ ); + }; + + function handleClick(objectURL){ + if(objectURL){ + console.log("success good job", objectURL); + onImageSelect(objectURL); + } + console.log("fail", objectURL); + } + + + return ( +
+ + + + + + Select an image or object to attach to your audio. Leave blank for no mesh. + + + + + onImageSelect( imageObject ) + } + type="audio" + label="Audio File" + allowedTypes={ ALLOWED_MEDIA_TYPES } + value={ attributes.audioUrl } + render={ ( { open } ) => ( + + ) } + /> + + + + { __( 'Position', 'three-object-viewer' ) } + + + + + onChangePositionX( value ) + } + /> + + onChangePositionY( value ) + } + /> + + onChangePositionZ( value ) + } + /> + + + + { __( 'Rotation', 'three-object-viewer' ) } + + + + + onChangeRotationX( value ) + } + /> + + onChangeRotationY( value ) + } + /> + + onChangeRotationZ( value ) + } + /> + + + { + onChangeAutoPlay( e ); + } } + /> + + + { + onChangeLoop( e ); + } } + /> + + + { + onChangePositional( e ); + } } + /> + + + + onChangeVolume( value ) + } + /> + + + { attributes.positional && ( + + + + onChangeConeInnerAngle( value ) + } + /> + + + + onChangeConeOuterAngle( value ) + } + /> + + + + onChangeConeOuterGain( value ) + } + /> + + + + onChangeDistanceModel( target ) + } + /> + + + + onChangeMaxDistance( value ) + } + /> + + + + onChangeRefDistance( value ) + } + /> + + + + onChangeRolloffFactor( value ) + } + /> + + + )} + + + { isSelected ? ( + <> + { attributes.videoUrl ? ( + <> + + ) : ( +
+ + +
+ + Select an image: + + + onImageSelect( imageObject ) + } + type="video" + allowedTypes={ ALLOWED_MEDIA_TYPES } + value={ attributes.videoUrl } + render={ ( { open } ) => ( + + ) } + /> +
+
+ ) } + + ) : ( + <> + { attributes.videoUrl ? ( + <> + + ) : ( +
+ +
+ + Select an image to render in your environment: + + {/*
+ console.log(e.target.value) && setEnteredURL(e.target.value)}> + +
*/} +
+ + onImageSelect( imageObject ) + } + type="image" + allowedTypes={ ALLOWED_MEDIA_TYPES } + value={ attributes.videoUrl } + render={ ( { open } ) => ( + + ) } + /> +
+ ) } + + ) } +
+ ); +} diff --git a/blocks/three-audio-block/Edit.test.js b/blocks/three-audio-block/Edit.test.js new file mode 100644 index 0000000..ab10afe --- /dev/null +++ b/blocks/three-audio-block/Edit.test.js @@ -0,0 +1,57 @@ + +//Import React +import React from 'react'; +//Import test renderer +import { render, fireEvent, cleanup } from '@testing-library/react'; +//Import component to test +import { Editor } from './Edit'; + + +describe("Editor componet", () => { + afterEach(cleanup); + it('matches snapshot when selected', () => { + const onChange = jest.fn(); + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot when not selected', () => { + const onChange = jest.fn(); + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("Calls the onchange function", () => { + const onChange = jest.fn(); + const { getByDisplayValue } = render(); + fireEvent.change(getByDisplayValue("Salad"), { + target: { value: "New Value" } + }); + expect(onChange).toHaveBeenCalledTimes(1); + }); + + it("Passes updated value, not event to onChange callback", () => { + const onChange = jest.fn(); + const { getByDisplayValue } = render(); + fireEvent.change(getByDisplayValue("Seltzer"), { + target: { value: "Boring Water" } + }); + expect(onChange).toHaveBeenCalledWith("Boring Water"); + }); +}); \ No newline at end of file diff --git a/blocks/three-audio-block/Save.js b/blocks/three-audio-block/Save.js new file mode 100644 index 0000000..2cfa1de --- /dev/null +++ b/blocks/three-audio-block/Save.js @@ -0,0 +1,50 @@ +import { __ } from '@wordpress/i18n'; +import { useBlockProps } from '@wordpress/block-editor'; + +export default function save( { attributes } ) { + return ( +
+ <> +
+

+ { attributes.audioUrl } +

+

+ { attributes.scaleX } +

+

+ { attributes.scaleY } +

+

+ { attributes.scaleZ } +

+

+ { attributes.positionX } +

+

+ { attributes.positionY } +

+

+ { attributes.positionZ } +

+

+ { attributes.rotationX } +

+

+ { attributes.rotationY } +

+

+ { attributes.rotationZ } +

+

+ { attributes.aspectHeight } +

+

+ { attributes.aspectWidth } +

+ +
+ +
+ ); +} diff --git a/blocks/three-audio-block/block.json b/blocks/three-audio-block/block.json new file mode 100644 index 0000000..16cbfea --- /dev/null +++ b/blocks/three-audio-block/block.json @@ -0,0 +1,108 @@ +{ + "name": "three-object-viewer/three-audio-block", + "title": "Three Audio Block", + "description": "An audio block for your environment", + "attributes": { + "audioUrl": { + "type": "string", + "default": null + }, + "autoPlay": { + "type": "bool", + "default": true + }, + "loop": { + "type": "bool", + "default": true + }, + "volume": { + "type": "int", + "default": 1 + }, + "positional": { + "type": "bool", + "default": true + }, + "coneInnerAngle": { + "type": "int", + "default":1 + }, + "coneOuterAngle": { + "type": "int", + "default":1 + }, + "coneOuterGain": { + "type": "int", + "default":1 + }, + "distanceModel": { + "type": "string", + "default": "inverse" + }, + "maxDistance": { + "type": "int", + "default":1 + }, + "refDistance": { + "type": "int", + "default":1 + }, + "rolloffFactor": { + "type": "int", + "default":1 + }, + "scaleX": { + "type": "int", + "default":1 + }, + "scaleY": { + "type": "int", + "default":1 + }, + "scaleZ": { + "type": "int", + "default":1 + }, + "positionX": { + "type": "int", + "default":0 + }, + "positionY": { + "type": "int", + "default":0 + }, + "positionZ": { + "type": "int", + "default":0 + }, + "rotationX": { + "type": "int", + "default":0 + }, + "rotationY": { + "type": "int", + "default":0 + }, + "rotationZ": { + "type": "int", + "default":0 + }, + "aspectHeight": { + "type": "int", + "default":0 + }, + "aspectWidth": { + "type": "int", + "default":0 + } + }, + "category": "3D", + "apiVersion": 2, + "supports": { + "html": false, + "multiple": true + }, + "editorScript": "file:../../build/block-three-audio-block.js", + "editorStyle": "file:../../build/block-three-audio-block.css", + "style": "file:../../build/block-three-audio-block.css" +} diff --git a/blocks/three-audio-block/components/ImageEdit.js b/blocks/three-audio-block/components/ImageEdit.js new file mode 100644 index 0000000..16e566c --- /dev/null +++ b/blocks/three-audio-block/components/ImageEdit.js @@ -0,0 +1,75 @@ +import * as THREE from 'three'; +import React, { Suspense, useRef, useState, useEffect, useMemo } from 'react'; +import { Canvas, useLoader, useFrame, useThree } from '@react-three/fiber'; +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; +import { + OrthographicCamera, + PerspectiveCamera, + OrbitControls, + useAnimations, +} from '@react-three/drei'; +import { VRM, VRMUtils, VRMSchema, VRMLoaderPlugin } from '@pixiv/three-vrm' +import { GLTFAudioEmitterExtension } from 'three-omi'; +import { useAspect } from '@react-three/drei'; + +// Geometry +function Plane(props) { + console.log(props); + const clicked = true; + const [video] = useState(() => Object.assign(document.createElement('video'), { src: props.url, crossOrigin: 'Anonymous', loop: true, muted: true })); + + useEffect(() => void (clicked && video.play()), [video, clicked]); + + return ( + + + + + + + ); +} + +function ThreeObject( props ) { + const [ url, set ] = useState( props.src ); + useEffect( () => { + setTimeout( () => set( props.src ), 2000 ); + }, [] ); + + return Plane(props); +} + +export default function ImageEdit( props ) { + return ( + <> + + + + { props.src && ( + + + + ) } + + + + ); +} diff --git a/blocks/three-audio-block/editor.scss b/blocks/three-audio-block/editor.scss new file mode 100644 index 0000000..a41c19e --- /dev/null +++ b/blocks/three-audio-block/editor.scss @@ -0,0 +1,48 @@ + + .wp-block-three-object-block { + border: 1px dotted #f00; +} + .glb-preview-container { + padding: 100px; + text-align: center; + align-items: center; + align-content: center; + background-color:#f2f2f2; + } + + .glb-preview-container button{ + padding: 15px; + border-radius: 30px; +} + +.three-object-viewer-button { + background-color:rgb(199, 254, 0); + color: black; + border: solid 1.5px black; +} + +.glb-preview-container button:hover{ + border-radius: 30px; + background-color:rgb(156, 199, 0); + cursor: pointer; +} +.three-object-block-tip { + overflow-wrap: break-word; + background-color: black; + color: white; + padding: 10px; + font-weight: 500; + max-width: 160px; + font-size: 12px; + margin-top: 0px; + margin: 0 auto; + text-align: center; +} + +.three-object-block-url-input { + padding-bottom: 20px; +} + +.three-object-block-url-input input{ + height: 40px; +} diff --git a/blocks/three-audio-block/index.js b/blocks/three-audio-block/index.js new file mode 100644 index 0000000..22b14b6 --- /dev/null +++ b/blocks/three-audio-block/index.js @@ -0,0 +1,197 @@ +import { registerBlockType } from '@wordpress/blocks'; +import Edit from './Edit'; +import Save from './Save'; +import { useBlockProps } from '@wordpress/block-editor'; + +const icon = ( + + + + + +); + +const blockConfig = require( './block.json' ); +registerBlockType( blockConfig.name, { + ...blockConfig, + icon: icon, + apiVersion: 2, + edit: Edit, + save: Save, + deprecated: [ + { + attributes: { + bg_color: { + type: 'string', + default: '#FFFFFF', + }, + zoom: { + type: 'integer', + default: 90, + }, + scale: { + type: 'integer', + default: 1, + }, + positionX: { + type: 'integer', + default: 0, + }, + positionY: { + type: 'integer', + default: 0, + }, + rotationY: { + type: 'integer', + default: 0, + }, + threeObjectUrl: { + type: 'string', + default: null, + }, + hasZoom: { + type: 'bool', + default: false, + }, + hasTip: { + type: 'bool', + default: true, + }, + deviceTarget: { + type: 'string', + default: '2d', + }, + }, + save( props ) { + return ( +
+ <> +
+

+ { props.attributes.deviceTarget } +

+

+ { props.attributes.threeObjectUrl } +

+

+ { props.attributes.scale } +

+

+ { props.attributes.bg_color } +

+

+ { props.attributes.zoom } +

+

+ { props.attributes.hasZoom ? 1 : 0 } +

+

+ { props.attributes.hasTip ? 1 : 0 } +

+

+ { props.attributes.positionY } +

+

+ { props.attributes.rotationY } +

+

+ { props.attributes.scale } +

+
+ +
+ ); + }, + }, + { + attributes: { + bg_color: { + type: 'string', + default: '#FFFFFF', + }, + zoom: { + type: 'integer', + default: 90, + }, + scale: { + type: 'integer', + default: 1, + }, + positionX: { + type: 'integer', + default: 0, + }, + positionY: { + type: 'integer', + default: 0, + }, + rotationY: { + type: 'integer', + default: 0, + }, + threeObjectUrl: { + type: 'string', + default: null, + }, + hasZoom: { + type: 'bool', + default: false, + }, + hasTip: { + type: 'bool', + default: true, + }, + deviceTarget: { + type: 'string', + default: '2d', + }, + animations: { + type: 'string', + default: '', + } + }, + save( props ) { + return ( +
+ <> +
+

+ { props.attributes.deviceTarget } +

+

+ { props.attributes.threeObjectUrl } +

+

{ props.attributes.scale }

+

+ { props.attributes.bg_color } +

+

{ props.attributes.zoom }

+

+ { props.attributes.hasZoom ? 1 : 0 } +

+

+ { props.attributes.hasTip ? 1 : 0 } +

+

+ { props.attributes.positionY } +

+

+ { props.attributes.rotationY } +

+

{ props.attributes.scale }

+

+ { props.attributes.animations } +

+
+ +
+ ); + }, + }, + ], +} ); diff --git a/blocks/three-audio-block/init.php b/blocks/three-audio-block/init.php new file mode 100644 index 0000000..4d16124 --- /dev/null +++ b/blocks/three-audio-block/init.php @@ -0,0 +1,10 @@ + { + console.log(imageObject); + setAttributes( { imageUrl: null } ); + setAttributes( { imageUrl: imageObject.url, aspectHeight: imageObject.height, aspectWidth: imageObject.width } ); + }; + const onChangePositionX = ( positionX ) => { + setAttributes( { positionX: positionX } ); + }; + const onChangePositionY = ( positionY ) => { + setAttributes( { positionY: positionY } ); + }; + const onChangePositionZ = ( positionZ ) => { + setAttributes( { positionZ: positionZ } ); + }; + + const onChangeRotationX = ( rotationX ) => { + setAttributes( { rotationX: rotationX } ); + }; + const onChangeRotationY = ( rotationY ) => { + setAttributes( { rotationY: rotationY } ); + }; + const onChangeRotationZ = ( rotationZ ) => { + setAttributes( { rotationZ: rotationZ } ); + }; + + const onChangeScaleX = ( scaleX ) => { + setAttributes( { scaleX: scaleX } ); + }; + const onChangeScaleY = ( scaleY ) => { + setAttributes( { scaleY: scaleY } ); + }; + const onChangeScaleZ = ( scaleZ ) => { + setAttributes( { scaleZ: scaleZ } ); + }; + + const onChangeCollidable = ( zoomSetting ) => { + setAttributes( { hasZoom: zoomSetting } ); + }; + + + const { mediaUpload } = wp.editor; + + const ALLOWED_MEDIA_TYPES = [ + 'image' + ]; + + const MyDropZone = () => { + const [ hasDropped, setHasDropped ] = useState( false ); + return ( +
+ { hasDropped ? 'Dropped!' : 'Drop an image here or' } + + mediaUpload( { + allowedTypes: ALLOWED_MEDIA_TYPES, + filesList: files, + onFileChange: ( [ images ] ) => { + onImageSelect( images ); + }, + } ) + } + /> +
+ ); + }; + + function handleClick(objectURL){ + if(objectURL){ + console.log("success good job", objectURL); + onImageSelect(objectURL); + } + console.log("fail", objectURL); + } + + + return ( +
+ + + + + + Select an image to render in your environment: + + + + + onImageSelect( imageObject ) + } + type="image" + label="Image File" + allowedTypes={ ALLOWED_MEDIA_TYPES } + value={ attributes.imageUrl } + render={ ( { open } ) => ( + + ) } + /> + + + + onChangePositionX( value ) + } + /> + + onChangePositionY( value ) + } + /> + + onChangePositionZ( value ) + } + /> + + + + { __( 'Rotation', 'three-object-viewer' ) } + + + + + onChangeRotationX( value ) + } + /> + + onChangeRotationY( value ) + } + /> + + onChangeRotationZ( value ) + } + /> + + + + { __( 'Scale', 'three-object-viewer' ) } + + + + + onChangeScaleX( value ) + } + /> + + onChangeScaleY( value ) + } + /> + + onChangeScaleZ( value ) + } + /> + + + + + { isSelected ? ( + <> + { attributes.imageUrl ? ( + <> + + ) : ( +
+ + +
+ + Select an image: + + + onImageSelect( imageObject ) + } + type="image" + allowedTypes={ ALLOWED_MEDIA_TYPES } + value={ attributes.imageUrl } + render={ ( { open } ) => ( + + ) } + /> +
+
+ ) } + + ) : ( + <> + { attributes.imageUrl ? ( + <> + + ) : ( +
+ +
+ + Select an image to render in your environment: + + {/*
+ console.log(e.target.value) && setEnteredURL(e.target.value)}> + +
*/} +
+ + onImageSelect( imageObject ) + } + type="image" + allowedTypes={ ALLOWED_MEDIA_TYPES } + value={ attributes.imageUrl } + render={ ( { open } ) => ( + + ) } + /> +
+ ) } + + ) } +
+ ); +} diff --git a/blocks/three-image-block/Edit.test.js b/blocks/three-image-block/Edit.test.js new file mode 100644 index 0000000..ab10afe --- /dev/null +++ b/blocks/three-image-block/Edit.test.js @@ -0,0 +1,57 @@ + +//Import React +import React from 'react'; +//Import test renderer +import { render, fireEvent, cleanup } from '@testing-library/react'; +//Import component to test +import { Editor } from './Edit'; + + +describe("Editor componet", () => { + afterEach(cleanup); + it('matches snapshot when selected', () => { + const onChange = jest.fn(); + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot when not selected', () => { + const onChange = jest.fn(); + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("Calls the onchange function", () => { + const onChange = jest.fn(); + const { getByDisplayValue } = render(); + fireEvent.change(getByDisplayValue("Salad"), { + target: { value: "New Value" } + }); + expect(onChange).toHaveBeenCalledTimes(1); + }); + + it("Passes updated value, not event to onChange callback", () => { + const onChange = jest.fn(); + const { getByDisplayValue } = render(); + fireEvent.change(getByDisplayValue("Seltzer"), { + target: { value: "Boring Water" } + }); + expect(onChange).toHaveBeenCalledWith("Boring Water"); + }); +}); \ No newline at end of file diff --git a/blocks/three-image-block/Save.js b/blocks/three-image-block/Save.js new file mode 100644 index 0000000..6c7a9ad --- /dev/null +++ b/blocks/three-image-block/Save.js @@ -0,0 +1,50 @@ +import { __ } from '@wordpress/i18n'; +import { useBlockProps } from '@wordpress/block-editor'; + +export default function save( { attributes } ) { + return ( +
+ <> +
+

+ { attributes.imageUrl } +

+

+ { attributes.scaleX } +

+

+ { attributes.scaleY } +

+

+ { attributes.scaleZ } +

+

+ { attributes.positionX } +

+

+ { attributes.positionY } +

+

+ { attributes.positionZ } +

+

+ { attributes.rotationX } +

+

+ { attributes.rotationY } +

+

+ { attributes.rotationZ } +

+

+ { attributes.aspectHeight } +

+

+ { attributes.aspectWidth } +

+ +
+ +
+ ); +} diff --git a/blocks/three-image-block/block.json b/blocks/three-image-block/block.json new file mode 100644 index 0000000..b3e904f --- /dev/null +++ b/blocks/three-image-block/block.json @@ -0,0 +1,64 @@ +{ + "name": "three-object-viewer/three-image-block", + "title": "Three Image Block", + "description": "An image block for your environment", + "attributes": { + "imageUrl": { + "type": "string", + "default": null + }, + "scaleX": { + "type": "int", + "default":1 + }, + "scaleY": { + "type": "int", + "default":1 + }, + "scaleZ": { + "type": "int", + "default":1 + }, + "positionX": { + "type": "int", + "default":0 + }, + "positionY": { + "type": "int", + "default":0 + }, + "positionZ": { + "type": "int", + "default":0 + }, + "rotationX": { + "type": "int", + "default":0 + }, + "rotationY": { + "type": "int", + "default":0 + }, + "rotationZ": { + "type": "int", + "default":0 + }, + "aspectHeight": { + "type": "int", + "default":0 + }, + "aspectWidth": { + "type": "int", + "default":0 + } + }, + "category": "3D", + "apiVersion": 2, + "supports": { + "html": false, + "multiple": true + }, + "editorScript": "file:../../build/block-three-image-block.js", + "editorStyle": "file:../../build/block-three-image-block.css", + "style": "file:../../build/block-three-image-block.css" +} diff --git a/blocks/three-image-block/components/ImageEdit.js b/blocks/three-image-block/components/ImageEdit.js new file mode 100644 index 0000000..f1d475a --- /dev/null +++ b/blocks/three-image-block/components/ImageEdit.js @@ -0,0 +1,69 @@ +import * as THREE from 'three'; +import React, { Suspense, useRef, useState, useEffect } from 'react'; +import { Canvas, useLoader, useFrame, useThree } from '@react-three/fiber'; +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; +import { + OrthographicCamera, + PerspectiveCamera, + OrbitControls, + useAnimations, +} from '@react-three/drei'; +import { VRM, VRMUtils, VRMSchema, VRMLoaderPlugin } from '@pixiv/three-vrm' +import { GLTFAudioEmitterExtension } from 'three-omi'; +import { useAspect } from '@react-three/drei' + + // Geometry +function Plane(props) { + const texture_1 = useLoader(THREE.TextureLoader, props.url); + + return ( + + + + + ); +} + +function ThreeObject( props ) { + const [ url, set ] = useState( props.src ); + useEffect( () => { + setTimeout( () => set( props.src ), 2000 ); + }, [] ); + + return Plane(props); +} + +export default function ImageEdit( props ) { + return ( + <> + + + + { props.src && ( + + + + ) } + + + + ); +} diff --git a/blocks/three-image-block/editor.scss b/blocks/three-image-block/editor.scss new file mode 100644 index 0000000..a41c19e --- /dev/null +++ b/blocks/three-image-block/editor.scss @@ -0,0 +1,48 @@ + + .wp-block-three-object-block { + border: 1px dotted #f00; +} + .glb-preview-container { + padding: 100px; + text-align: center; + align-items: center; + align-content: center; + background-color:#f2f2f2; + } + + .glb-preview-container button{ + padding: 15px; + border-radius: 30px; +} + +.three-object-viewer-button { + background-color:rgb(199, 254, 0); + color: black; + border: solid 1.5px black; +} + +.glb-preview-container button:hover{ + border-radius: 30px; + background-color:rgb(156, 199, 0); + cursor: pointer; +} +.three-object-block-tip { + overflow-wrap: break-word; + background-color: black; + color: white; + padding: 10px; + font-weight: 500; + max-width: 160px; + font-size: 12px; + margin-top: 0px; + margin: 0 auto; + text-align: center; +} + +.three-object-block-url-input { + padding-bottom: 20px; +} + +.three-object-block-url-input input{ + height: 40px; +} diff --git a/blocks/three-image-block/index.js b/blocks/three-image-block/index.js new file mode 100644 index 0000000..22b14b6 --- /dev/null +++ b/blocks/three-image-block/index.js @@ -0,0 +1,197 @@ +import { registerBlockType } from '@wordpress/blocks'; +import Edit from './Edit'; +import Save from './Save'; +import { useBlockProps } from '@wordpress/block-editor'; + +const icon = ( + + + + + +); + +const blockConfig = require( './block.json' ); +registerBlockType( blockConfig.name, { + ...blockConfig, + icon: icon, + apiVersion: 2, + edit: Edit, + save: Save, + deprecated: [ + { + attributes: { + bg_color: { + type: 'string', + default: '#FFFFFF', + }, + zoom: { + type: 'integer', + default: 90, + }, + scale: { + type: 'integer', + default: 1, + }, + positionX: { + type: 'integer', + default: 0, + }, + positionY: { + type: 'integer', + default: 0, + }, + rotationY: { + type: 'integer', + default: 0, + }, + threeObjectUrl: { + type: 'string', + default: null, + }, + hasZoom: { + type: 'bool', + default: false, + }, + hasTip: { + type: 'bool', + default: true, + }, + deviceTarget: { + type: 'string', + default: '2d', + }, + }, + save( props ) { + return ( +
+ <> +
+

+ { props.attributes.deviceTarget } +

+

+ { props.attributes.threeObjectUrl } +

+

+ { props.attributes.scale } +

+

+ { props.attributes.bg_color } +

+

+ { props.attributes.zoom } +

+

+ { props.attributes.hasZoom ? 1 : 0 } +

+

+ { props.attributes.hasTip ? 1 : 0 } +

+

+ { props.attributes.positionY } +

+

+ { props.attributes.rotationY } +

+

+ { props.attributes.scale } +

+
+ +
+ ); + }, + }, + { + attributes: { + bg_color: { + type: 'string', + default: '#FFFFFF', + }, + zoom: { + type: 'integer', + default: 90, + }, + scale: { + type: 'integer', + default: 1, + }, + positionX: { + type: 'integer', + default: 0, + }, + positionY: { + type: 'integer', + default: 0, + }, + rotationY: { + type: 'integer', + default: 0, + }, + threeObjectUrl: { + type: 'string', + default: null, + }, + hasZoom: { + type: 'bool', + default: false, + }, + hasTip: { + type: 'bool', + default: true, + }, + deviceTarget: { + type: 'string', + default: '2d', + }, + animations: { + type: 'string', + default: '', + } + }, + save( props ) { + return ( +
+ <> +
+

+ { props.attributes.deviceTarget } +

+

+ { props.attributes.threeObjectUrl } +

+

{ props.attributes.scale }

+

+ { props.attributes.bg_color } +

+

{ props.attributes.zoom }

+

+ { props.attributes.hasZoom ? 1 : 0 } +

+

+ { props.attributes.hasTip ? 1 : 0 } +

+

+ { props.attributes.positionY } +

+

+ { props.attributes.rotationY } +

+

{ props.attributes.scale }

+

+ { props.attributes.animations } +

+
+ +
+ ); + }, + }, + ], +} ); diff --git a/blocks/three-image-block/init.php b/blocks/three-image-block/init.php new file mode 100644 index 0000000..4d16124 --- /dev/null +++ b/blocks/three-image-block/init.php @@ -0,0 +1,10 @@ + new GLTFAudioEmitterExtension( parser, listener ) ); loader.register( ( parser ) => { + return new VRMLoaderPlugin( parser ); + } ); + } ); + const fallbackURL = threeObjectPlugin + defaultVRM; + const playerURL = props.playerData.vrm ? props.playerData.vrm : fallbackURL + const someSceneState = useLoader( GLTFLoader, playerURL, ( loader ) => { + loader.register( + ( parser ) => new GLTFAudioEmitterExtension( parser, listener ) + ); + loader.register( ( parser ) => { return new VRMLoaderPlugin( parser ); } ); } ); @@ -52,6 +63,23 @@ function SavedObject( props ) { } ); } }, [] ); + if(someSceneState?.userData?.gltfExtensions?.VRM){ + const playerController = someSceneState.userData.vrm; + const { camera } = useThree(); + useFrame(() => { + const offset = camera.position.z - 3; + playerController.scene.position.set( camera.position.x, camera.position.y, offset ); + playerController.scene.rotation.set( camera.rotation.x, camera.rotation.y, camera.rotation.z ); + }); + VRMUtils.rotateVRM0( playerController ); + const rotationVRM = playerController.scene.rotation.y; + playerController.scene.rotation.set( 0, rotationVRM, 0 ); + playerController.scene.scale.set( 1, 1, 1 ); + gltf.scene.position.set( 0, props.positionY, 0 ); + gltf.scene.rotation.set( 0, props.rotationY, 0 ); + gltf.scene.scale.set( props.scale, props.scale, props.scale ); + return <>; + } if(gltf?.userData?.gltfExtensions?.VRM){ const vrm = gltf.userData.vrm; vrm.scene.position.set( 0, props.positionY, 0 ); @@ -64,7 +92,7 @@ function SavedObject( props ) { gltf.scene.position.set( 0, props.positionY, 0 ); gltf.scene.rotation.set( 0, props.rotationY, 0 ); gltf.scene.scale.set( props.scale, props.scale, props.scale ); - return ; + return <>; } function Floor( props ) { @@ -119,6 +147,7 @@ export default function ThreeObjectFront( props ) { scale={ props.scale } hasTip={ props.hasTip } animations={ props.animations } + playerData={ props.userData } /> diff --git a/blocks/three-object-block/frontend.js b/blocks/three-object-block/frontend.js index 96bec21..02a7a97 100644 --- a/blocks/three-object-block/frontend.js +++ b/blocks/three-object-block/frontend.js @@ -4,6 +4,7 @@ import ThreeObjectFront from './components/ThreeObjectFront'; const threeApp = document.querySelectorAll( '.three-object-three-app' ); + threeApp.forEach( ( threeApp ) => { if ( threeApp ) { const threeUrl = threeApp.querySelector( 'p.three-object-block-url' ) @@ -54,6 +55,7 @@ threeApp.forEach( ( threeApp ) => { rotationY={ rotationY } animations={ animations } backgroundColor={ backgroundColor } + userData={userData} />, threeApp ); diff --git a/blocks/three-video-block/Edit.js b/blocks/three-video-block/Edit.js new file mode 100644 index 0000000..f2de848 --- /dev/null +++ b/blocks/three-video-block/Edit.js @@ -0,0 +1,328 @@ +import { __ } from '@wordpress/i18n'; +import React, { useState } from 'react'; +import { DropZone } from '@wordpress/components'; +import './editor.scss'; +import { + useBlockProps, + ColorPalette, + InspectorControls, + MediaUpload, +} from '@wordpress/block-editor'; +import { + Panel, + PanelBody, + PanelRow, + RangeControl, + ToggleControl, + SelectControl, + TextControl, +} from '@wordpress/components'; +import { more } from '@wordpress/icons'; + +import VideoEdit from './components/VideoEdit'; + +export default function Edit( { attributes, setAttributes, isSelected } ) { + + const onImageSelect = ( imageObject ) => { + console.log(imageObject); + setAttributes( { audioUrl: null } ); + setAttributes( { audioUrl: imageObject.url, aspectHeight: imageObject.height, aspectWidth: imageObject.width } ); + }; + const onChangePositionX = ( positionX ) => { + setAttributes( { positionX: positionX } ); + }; + const onChangePositionY = ( positionY ) => { + setAttributes( { positionY: positionY } ); + }; + const onChangePositionZ = ( positionZ ) => { + setAttributes( { positionZ: positionZ } ); + }; + + const onChangeRotationX = ( rotationX ) => { + setAttributes( { rotationX: rotationX } ); + }; + const onChangeRotationY = ( rotationY ) => { + setAttributes( { rotationY: rotationY } ); + }; + const onChangeRotationZ = ( rotationZ ) => { + setAttributes( { rotationZ: rotationZ } ); + }; + + const onChangeScaleX = ( scaleX ) => { + setAttributes( { scaleX: scaleX } ); + }; + const onChangeScaleY = ( scaleY ) => { + setAttributes( { scaleY: scaleY } ); + }; + const onChangeScaleZ = ( scaleZ ) => { + setAttributes( { scaleZ: scaleZ } ); + }; + + const onChangeAutoPlay = ( autoPlaySetting ) => { + setAttributes( { autoPlay: autoPlaySetting } ); + }; + + + const { mediaUpload } = wp.editor; + + const ALLOWED_MEDIA_TYPES = [ + 'video' + ]; + + const MyDropZone = () => { + const [ hasDropped, setHasDropped ] = useState( false ); + return ( +
+ { hasDropped ? 'Dropped!' : 'Drop an image here or' } + + mediaUpload( { + allowedTypes: ALLOWED_MEDIA_TYPES, + filesList: files, + onFileChange: ( [ images ] ) => { + onImageSelect( images ); + }, + } ) + } + /> +
+ ); + }; + + function handleClick(objectURL){ + if(objectURL){ + console.log("success good job", objectURL); + onImageSelect(objectURL); + } + console.log("fail", objectURL); + } + + + return ( +
+ + + + + + Select an image to render in your environment: + + + + + onImageSelect( imageObject ) + } + type="image" + label="Image File" + allowedTypes={ ALLOWED_MEDIA_TYPES } + value={ attributes.audioUrl } + render={ ( { open } ) => ( + + ) } + /> + + + { + onChangeAutoPlay( e ); + } } + /> + + + + onChangePositionX( value ) + } + /> + + onChangePositionY( value ) + } + /> + + onChangePositionZ( value ) + } + /> + + + + { __( 'Rotation', 'three-object-viewer' ) } + + + + + onChangeRotationX( value ) + } + /> + + onChangeRotationY( value ) + } + /> + + onChangeRotationZ( value ) + } + /> + + + + { __( 'Scale', 'three-object-viewer' ) } + + + + + onChangeScaleX( value ) + } + /> + + onChangeScaleY( value ) + } + /> + + onChangeScaleZ( value ) + } + /> + + + + + { isSelected ? ( + <> + { attributes.audioUrl ? ( + <> + + ) : ( +
+ + +
+ + Select an image: + + + onImageSelect( imageObject ) + } + type="video" + allowedTypes={ ALLOWED_MEDIA_TYPES } + value={ attributes.audioUrl } + render={ ( { open } ) => ( + + ) } + /> +
+
+ ) } + + ) : ( + <> + { attributes.audioUrl ? ( + <> + + ) : ( +
+ +
+ + Select an image to render in your environment: + + {/*
+ console.log(e.target.value) && setEnteredURL(e.target.value)}> + +
*/} +
+ + onImageSelect( imageObject ) + } + type="image" + allowedTypes={ ALLOWED_MEDIA_TYPES } + value={ attributes.audioUrl } + render={ ( { open } ) => ( + + ) } + /> +
+ ) } + + ) } +
+ ); +} diff --git a/blocks/three-video-block/Edit.test.js b/blocks/three-video-block/Edit.test.js new file mode 100644 index 0000000..ab10afe --- /dev/null +++ b/blocks/three-video-block/Edit.test.js @@ -0,0 +1,57 @@ + +//Import React +import React from 'react'; +//Import test renderer +import { render, fireEvent, cleanup } from '@testing-library/react'; +//Import component to test +import { Editor } from './Edit'; + + +describe("Editor componet", () => { + afterEach(cleanup); + it('matches snapshot when selected', () => { + const onChange = jest.fn(); + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it('matches snapshot when not selected', () => { + const onChange = jest.fn(); + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("Calls the onchange function", () => { + const onChange = jest.fn(); + const { getByDisplayValue } = render(); + fireEvent.change(getByDisplayValue("Salad"), { + target: { value: "New Value" } + }); + expect(onChange).toHaveBeenCalledTimes(1); + }); + + it("Passes updated value, not event to onChange callback", () => { + const onChange = jest.fn(); + const { getByDisplayValue } = render(); + fireEvent.change(getByDisplayValue("Seltzer"), { + target: { value: "Boring Water" } + }); + expect(onChange).toHaveBeenCalledWith("Boring Water"); + }); +}); \ No newline at end of file diff --git a/blocks/three-video-block/Save.js b/blocks/three-video-block/Save.js new file mode 100644 index 0000000..adbc7b7 --- /dev/null +++ b/blocks/three-video-block/Save.js @@ -0,0 +1,50 @@ +import { __ } from '@wordpress/i18n'; +import { useBlockProps } from '@wordpress/block-editor'; + +export default function save( { attributes } ) { + return ( +
+ <> +
+

+ { attributes.videoUrl } +

+

+ { attributes.scaleX } +

+

+ { attributes.scaleY } +

+

+ { attributes.scaleZ } +

+

+ { attributes.positionX } +

+

+ { attributes.positionY } +

+

+ { attributes.positionZ } +

+

+ { attributes.rotationX } +

+

+ { attributes.rotationY } +

+

+ { attributes.rotationZ } +

+

+ { attributes.aspectHeight } +

+

+ { attributes.aspectWidth } +

+ +
+ +
+ ); +} diff --git a/blocks/three-video-block/block.json b/blocks/three-video-block/block.json new file mode 100644 index 0000000..cde1c93 --- /dev/null +++ b/blocks/three-video-block/block.json @@ -0,0 +1,68 @@ +{ + "name": "three-object-viewer/three-video-block", + "title": "Three Video Block", + "description": "A video block for your environment", + "attributes": { + "videoUrl": { + "type": "string", + "default": null + }, + "autoPlay": { + "type": "bool", + "default": true + }, + "scaleX": { + "type": "int", + "default":1 + }, + "scaleY": { + "type": "int", + "default":1 + }, + "scaleZ": { + "type": "int", + "default":1 + }, + "positionX": { + "type": "int", + "default":0 + }, + "positionY": { + "type": "int", + "default":0 + }, + "positionZ": { + "type": "int", + "default":0 + }, + "rotationX": { + "type": "int", + "default":0 + }, + "rotationY": { + "type": "int", + "default":0 + }, + "rotationZ": { + "type": "int", + "default":0 + }, + "aspectHeight": { + "type": "int", + "default":0 + }, + "aspectWidth": { + "type": "int", + "default":0 + } + }, + "category": "3D", + "apiVersion": 2, + "supports": { + "html": false, + "multiple": true + }, + "editorScript": "file:../../build/block-three-video-block.js", + "editorStyle": "file:../../build/block-three-video-block.css", + "style": "file:../../build/block-three-video-block.css" +} diff --git a/blocks/three-video-block/components/VideoEdit.js b/blocks/three-video-block/components/VideoEdit.js new file mode 100644 index 0000000..7e3cfad --- /dev/null +++ b/blocks/three-video-block/components/VideoEdit.js @@ -0,0 +1,75 @@ +import * as THREE from 'three'; +import React, { Suspense, useRef, useState, useEffect, useMemo } from 'react'; +import { Canvas, useLoader, useFrame, useThree } from '@react-three/fiber'; +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; +import { + OrthographicCamera, + PerspectiveCamera, + OrbitControls, + useAnimations, +} from '@react-three/drei'; +import { VRM, VRMUtils, VRMSchema, VRMLoaderPlugin } from '@pixiv/three-vrm' +import { GLTFAudioEmitterExtension } from 'three-omi'; +import { useAspect } from '@react-three/drei'; + +// Geometry +function Plane(props) { + console.log(props); + const clicked = true; + const [video] = useState(() => Object.assign(document.createElement('video'), { src: props.url, crossOrigin: 'Anonymous', loop: true, muted: true })); + + useEffect(() => void (clicked && video.play()), [video, clicked]); + + return ( + + + + + + + ); +} + +function ThreeObject( props ) { + const [ url, set ] = useState( props.src ); + useEffect( () => { + setTimeout( () => set( props.src ), 2000 ); + }, [] ); + + return Plane(props); +} + +export default function VideoEdit( props ) { + return ( + <> + + + + { props.src && ( + + + + ) } + + + + ); +} diff --git a/blocks/three-video-block/editor.scss b/blocks/three-video-block/editor.scss new file mode 100644 index 0000000..a41c19e --- /dev/null +++ b/blocks/three-video-block/editor.scss @@ -0,0 +1,48 @@ + + .wp-block-three-object-block { + border: 1px dotted #f00; +} + .glb-preview-container { + padding: 100px; + text-align: center; + align-items: center; + align-content: center; + background-color:#f2f2f2; + } + + .glb-preview-container button{ + padding: 15px; + border-radius: 30px; +} + +.three-object-viewer-button { + background-color:rgb(199, 254, 0); + color: black; + border: solid 1.5px black; +} + +.glb-preview-container button:hover{ + border-radius: 30px; + background-color:rgb(156, 199, 0); + cursor: pointer; +} +.three-object-block-tip { + overflow-wrap: break-word; + background-color: black; + color: white; + padding: 10px; + font-weight: 500; + max-width: 160px; + font-size: 12px; + margin-top: 0px; + margin: 0 auto; + text-align: center; +} + +.three-object-block-url-input { + padding-bottom: 20px; +} + +.three-object-block-url-input input{ + height: 40px; +} diff --git a/blocks/three-video-block/index.js b/blocks/three-video-block/index.js new file mode 100644 index 0000000..22b14b6 --- /dev/null +++ b/blocks/three-video-block/index.js @@ -0,0 +1,197 @@ +import { registerBlockType } from '@wordpress/blocks'; +import Edit from './Edit'; +import Save from './Save'; +import { useBlockProps } from '@wordpress/block-editor'; + +const icon = ( + + + + + +); + +const blockConfig = require( './block.json' ); +registerBlockType( blockConfig.name, { + ...blockConfig, + icon: icon, + apiVersion: 2, + edit: Edit, + save: Save, + deprecated: [ + { + attributes: { + bg_color: { + type: 'string', + default: '#FFFFFF', + }, + zoom: { + type: 'integer', + default: 90, + }, + scale: { + type: 'integer', + default: 1, + }, + positionX: { + type: 'integer', + default: 0, + }, + positionY: { + type: 'integer', + default: 0, + }, + rotationY: { + type: 'integer', + default: 0, + }, + threeObjectUrl: { + type: 'string', + default: null, + }, + hasZoom: { + type: 'bool', + default: false, + }, + hasTip: { + type: 'bool', + default: true, + }, + deviceTarget: { + type: 'string', + default: '2d', + }, + }, + save( props ) { + return ( +
+ <> +
+

+ { props.attributes.deviceTarget } +

+

+ { props.attributes.threeObjectUrl } +

+

+ { props.attributes.scale } +

+

+ { props.attributes.bg_color } +

+

+ { props.attributes.zoom } +

+

+ { props.attributes.hasZoom ? 1 : 0 } +

+

+ { props.attributes.hasTip ? 1 : 0 } +

+

+ { props.attributes.positionY } +

+

+ { props.attributes.rotationY } +

+

+ { props.attributes.scale } +

+
+ +
+ ); + }, + }, + { + attributes: { + bg_color: { + type: 'string', + default: '#FFFFFF', + }, + zoom: { + type: 'integer', + default: 90, + }, + scale: { + type: 'integer', + default: 1, + }, + positionX: { + type: 'integer', + default: 0, + }, + positionY: { + type: 'integer', + default: 0, + }, + rotationY: { + type: 'integer', + default: 0, + }, + threeObjectUrl: { + type: 'string', + default: null, + }, + hasZoom: { + type: 'bool', + default: false, + }, + hasTip: { + type: 'bool', + default: true, + }, + deviceTarget: { + type: 'string', + default: '2d', + }, + animations: { + type: 'string', + default: '', + } + }, + save( props ) { + return ( +
+ <> +
+

+ { props.attributes.deviceTarget } +

+

+ { props.attributes.threeObjectUrl } +

+

{ props.attributes.scale }

+

+ { props.attributes.bg_color } +

+

{ props.attributes.zoom }

+

+ { props.attributes.hasZoom ? 1 : 0 } +

+

+ { props.attributes.hasTip ? 1 : 0 } +

+

+ { props.attributes.positionY } +

+

+ { props.attributes.rotationY } +

+

{ props.attributes.scale }

+

+ { props.attributes.animations } +

+
+ +
+ ); + }, + }, + ], +} ); diff --git a/blocks/three-video-block/init.php b/blocks/three-video-block/init.php new file mode 100644 index 0000000..4d16124 --- /dev/null +++ b/blocks/three-video-block/init.php @@ -0,0 +1,10 @@ +avatar); + $user_data_passed = array( + 'userId' => $current_user->user_login, + 'inWorldName' => $current_user->in_world_name, + 'banner' => $current_user->custom_banner, + 'vrm' => $vrm, + ); + + $three_object_plugin = plugins_url() . '/three-object-viewer/build/'; + + // $user_data_passed = array( + // 'userId' => 'something', + // 'userName' => 'someone', + // 'vrm' => 'somefile.vrm', + // ); + + wp_register_script( 'threeobjectloader-frontend', plugin_dir_url( __FILE__ ) . '../build/assets/js/blocks.frontend.js', ['wp-element', 'wp-data', 'wp-hooks'], '', true ); + wp_localize_script( 'threeobjectloader-frontend', 'userData', $user_data_passed ); + wp_localize_script( 'threeobjectloader-frontend', 'threeObjectPlugin', $three_object_plugin ); + wp_enqueue_script( - "threeobjectloader-frontend", - plugin_dir_url( __FILE__ ) . '../build/assets/js/blocks.frontend.js', - ['wp-element'], - '', - true + "threeobjectloader-frontend" ); + + wp_register_script( 'versepress-frontend', plugin_dir_url( __FILE__ ) . '../build/assets/js/blocks.frontend-versepress.js', ['wp-element', 'wp-data', 'wp-hooks'], '', true ); + wp_localize_script( 'versepress-frontend', 'userData', $user_data_passed ); + wp_localize_script( 'versepress-frontend', 'threeObjectPlugin', $three_object_plugin ); + + wp_enqueue_script( + "versepress-frontend" + ); + } diff --git a/pluginMachine.json b/pluginMachine.json index 8cc14cb..dfcca36 100644 --- a/pluginMachine.json +++ b/pluginMachine.json @@ -4,10 +4,16 @@ "buildId": 172, "entryPoints": { "adminPages": [ - "three-model-viewer-settings" + "three-object-viewer-settings" ], "blocks": [ - "three-object-block" + "three-object-block", + "environment", + "model-block", + "sky-block", + "three-image-block", + "three-video-block", + "three-audio-block" ] }, "buildIncludes": [ @@ -17,6 +23,11 @@ "vendor", "build", "blocks/three-object-block/init.php", - "admin/three-model-viewer-settings/init.php" + "blocks/environment/init.php", + "blocks/model-block/init.php", + "blocks/sky-block/init.php", + "blocks/three-image-block/init.php", + "blocks/three-video-block/init.php", + "admin/three-object-viewer-settings/init.php" ] } \ No newline at end of file diff --git a/three-object-viewer.php b/three-object-viewer.php index e9f7d4e..e296379 100644 --- a/three-object-viewer.php +++ b/three-object-viewer.php @@ -17,6 +17,24 @@ // Include three-object-block include_once dirname( __FILE__ ) . '/blocks/three-object-block/init.php'; +// Include environment +include_once dirname( __FILE__ ) . '/blocks/environment/init.php'; + +// Include model +include_once dirname( __FILE__ ) . '/blocks/model-block/init.php'; + +// Include sky +include_once dirname( __FILE__ ) . '/blocks/sky-block/init.php'; + +// Include image +include_once dirname( __FILE__ ) . '/blocks/three-image-block/init.php'; + +// Include image +include_once dirname( __FILE__ ) . '/blocks/three-video-block/init.php'; + +// Include audio +include_once dirname( __FILE__ ) . '/blocks/three-audio-block/init.php'; + /** * Include the autoloader */ @@ -28,4 +46,4 @@ include_once dirname( __FILE__ ). '/inc/functions.php'; include_once dirname( __FILE__ ). '/inc/hooks.php'; -include_once dirname( __FILE__ ) . '/admin/three-model-viewer-settings/init.php'; +include_once dirname( __FILE__ ) . '/admin/three-object-viewer-settings/init.php'; diff --git a/webpack.config.js b/webpack.config.js index 18e259d..b830e14 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -25,6 +25,9 @@ if ( entryPoints.hasOwnProperty( 'adminPages' ) ) { entry[ `./assets/js/blocks.frontend` ] = './blocks/three-object-block/frontend.js'; +entry[ `./assets/js/blocks.frontend-versepress` ] = +'./blocks/environment/frontend.js'; + module.exports = { mode: isProduction ? 'production' : 'development', ...defaultConfig, @@ -36,7 +39,15 @@ module.exports = { test: /\.css$/, use: [ 'style-loader', 'css-loader' ], }, - ], + { + test: /\.vrm$/, + use: [ + { + loader: 'file-loader', + }, + ], + }, + ], }, entry, output: {