import { AFRAME } from '../systems/brush'; import { uuidv4 } from '../util'; import { FontLoader } from 'three/examples/jsm/loaders/FontLoader.js'; import { GetBackgroundColor, GetColor } from '../systems/ui'; import InteractionMixin from './interaction-mixin'; import { PAINTER_CONSTANTS } from '../constants'; import mixin from '../../gui/components/mixin'; import { updateAttribute, updateAttributeEl, watchAttribute } from '../useEventListenerOn'; const THREE: any = (window as any).THREE; function loadFontAtlas(path) { const promise = new Promise((resolve, reject) => { const loader = new THREE.TextureLoader(); loader.load(path, resolve); }); return promise; } function loadFont(path) { const promise = new Promise((resolve, reject) => { const loader = new FontLoader(); loader.load(path, resolve); }); return promise; } export default function () { AFRAME.registerComponent('infinity-list', { schema: { selectionevent: { type: 'string' }, closeevent: { type: 'string' }, hideClose: { type: 'boolean', default: false }, columns: { type: 'number', default: 4 }, width: { type: 'number', default: 3 }, height: { type: 'number', default: 2.2 }, boxMargin: { type: 'number', default: .2 }, itemtemplate: { type: 'string' }, itemSize: { type: 'number', default: .5 }, itemHeight: { type: 'number', default: 0 }, itemWidth: { type: 'number', default: 0 }, iconFont: { type: 'string', default: '' }, icon: { type: 'string', default: '' }, direct: { type: 'boolean', default: false }, speed: { type: 'string', default: 0 }, iconFontSize: { type: 'string', default: '' }, options: { parse: (val) => { console.log(val); if (typeof val !== `string`) { return val; } return JSON.parse(val); }, default: [] }, selected: { type: 'string' }, textValueMap: { type: 'string', default: '' } }, setup: function () { let me = this; let itemHeight = this.data.itemHeight || this.data.itemSize || .5; let itemWidth = this.data.itemWidth || this.data.itemSize || .5; let outerBoxWidth = this.data.width || 3; let boxHeight = this.data.height || 2.2; let boxMargin = this.data.boxMargin || .2 let windowHeight = boxHeight - boxMargin; this.boxHeight = boxHeight; this.outerBoxWidth = outerBoxWidth; this.guiItem = { width: itemWidth, height: itemHeight } this.stepSize = .1; me.radius = .25; // Radius of the circle me.fontSize = .001; me.currentRotation = 0; me.visible_items = getNumberOfVisibleItems(itemHeight + me.itemMargin, windowHeight, this.data.columns); // Number of items visible at a time me.imageMargin = .01 me.itemMargin = .1 me.currentIndex = 0; me.currentIndexScroll = 0; me.targetScroll = 0 me.visibleObjects = {}; let options = me.getOptions(); me.options = options; let sprocket = me.sprocket || document.createElement('a-entity'); me.sprocket = sprocket; sprocket.setAttribute('position', `0 ${-windowHeight / 2 + .1} 0`) me.el.appendChild(sprocket); me.itemHeight = itemHeight; me.itemWidth = itemWidth; me.windowHeight = windowHeight; me.windowMargin = { y: .3 } }, updateSubComponents: function () { let outerBoxWidth = this.outerBoxWidth; let boxHeight = this.boxHeight; let windowHeight = this.windowHeight; this.slider.setAttribute('width', `${boxHeight}`); this.outerBox.setAttribute('width', `${outerBoxWidth - this.sliderWidth - this.sliderHorizontalMargin}`); this.outerBox.setAttribute('height', `${boxHeight}`); this.outerBox.setAttribute('position', `${-this.sliderWidth + -this.sliderHorizontalMargin} 0 -.02`); this.sliderContainer.setAttribute('position', `${outerBoxWidth / 2 - this.sliderWidth / 2} 0 .0`); this.closeButton.setAttribute('position', `${-outerBoxWidth / 2 - .1} ${windowHeight / 2 - .1} 0`); this.updateElementSize(this, this.el); }, init: function () { let me = this; this.onInit(); var system = this.el.sceneEl.systems.ui; // Access by system name this.setup(); this.system = system; let outerBox: any = document.createElement('frame-rounded'); outerBox.setAttribute("gui-interactable", {}) outerBox.setAttribute('color', GetBackgroundColor()); outerBox.setAttribute('gui-interactable', {}) outerBox.setAttribute('position', '0 0 .01') outerBox.setAttribute('opacity', ".25") this.outerBox = outerBox; me.el.appendChild(outerBox); let slider = document.createElement('a-gui-slider'); slider.setAttribute('percent', `${0}`); if (this.data.textValueMap) { slider.setAttribute('text-value-map', this.data.textValueMap); } this.sliderWidth = 0.2; this.sliderHorizontalMargin = .2 this.el.addEventListener('update-attribute', (evt: any) => { let { detail } = evt; let { attribute, value } = detail; me.el.setAttribute(attribute, value) }) slider.setAttribute('targetbarsize', this.sliderWidth); slider.addEventListener('change', (evt: any) => { let { value } = evt.detail; me.targetScroll = (me.itemHeight * (me.options?.length || 0) / me.data.columns) * value; handleCheckForMore(value); }); let sliderContainer = document.createElement('a-entity'); sliderContainer.setAttribute('rotation', `0 0 90`); this.sliderContainer = sliderContainer; sliderContainer.appendChild(slider); this.slider = slider; outerBox.appendChild(sliderContainer); let closeButton = document.createElement('frame-gui-icon-label-button'); closeButton.setAttribute('width', '0.25') closeButton.setAttribute('height', '0.25') closeButton.addEventListener('click', () => { me.el.emit(me.data.closeevent, {}); me.targetScroll += me.itemHeight * (me.visible_items + .5); me.targetScroll = Math.min(me.targetScroll, me.itemHeight * me.options.length); }); closeButton.setAttribute('value', '') if (this.data.iconFont) { closeButton.setAttribute('icon-font', this.data.iconFont) closeButton.setAttribute('icon', this.data.icon || 'f128') closeButton.setAttribute('icon-font-size', this.data.iconFontSize || '0.1') } closeButton.setAttribute('margin', '0 0 0.05 0.02') if (!this.data.hideClose) { outerBox.appendChild(closeButton); } this.closeButton = closeButton; outerBox.classList.add('raycastable'); const canvas = this.el.sceneEl.canvas; function handleCheckForMore(value: number) { if (value > .99) { me.el.emit('request-more-data', {}); } else if (value < .01) { me.el.emit('request-more-data', { atStart: true }); } } canvas.addEventListener('wheel', (event) => { if (me.sprocketHovering || me.sliderHovering || me.elHovering || me.outerboxHovering || me.sliderContainerHovering || me.raycastersIntersecting || me.mouseIntersecting) { const deltaY = event.deltaY / 400; let maxHeight = (me.itemHeight * (me.options?.length || 0) / me.data.columns); me.targetScroll = Math.min(Math.max(me.targetScroll + deltaY, 0), (me.itemHeight * me.options.length / me.data.columns)); if (maxHeight) { slider.setAttribute('percent', `${Math.max(0, Math.min(me.targetScroll / maxHeight, 1))}`) handleCheckForMore(Math.max(0, Math.min(me.targetScroll / maxHeight, 1))); } } }); canvas.addEventListener('axismove', (event) => { if (event?.detail?.axis) { if (me.sprocketHovering || me.sliderHovering || me.elHovering || me.outerboxHovering || me.sliderContainerHovering || me.raycastersIntersecting || me.mouseIntersecting) { const deltaY = event?.detail?.axis / 4; let maxHeight = (me.itemHeight * (me.options?.length || 0) / me.data.columns); me.targetScroll = Math.min(Math.max(me.targetScroll + deltaY, 0), (me.itemHeight * me.options.length / me.data.columns)); if (maxHeight) { slider.setAttribute('percent', `${Math.max(0, Math.min(me.targetScroll / maxHeight, 1))}`) handleCheckForMore(Math.max(0, Math.min(me.targetScroll / maxHeight, 1))); } } } }); this.setupRayIntersectionListener(outerBox, (over) => { me.outerboxHovering = over; }) this.setupRayIntersectionListener(sliderContainer, (over) => { me.sliderContainerHovering = over; }) this.setupRayIntersectionListener(me.sprocket, (over) => { me.sprocketHovering = over; }) this.setupRayIntersectionListener(slider, (over) => { me.sliderHovering = over; }) this.setupRayIntersectionListener(this.el, (over) => { me.elHovering = over; }) this.setupRayListener(outerBox, 'interactive', this.handleInifniteListInteractions); this.updateSubComponents(); me.loadedFont = true; watchAttribute(me.el, { options: (name: string, value: any, el: any) => { if (JSON.stringify(value) !== JSON.stringify(me.oldOptions || '')) { me.directOptions = value; let options = me.getOptions(); me.options = options; let keys = Object.keys(me.visibleObjects); for (let k = 0; k < keys.length; k++) { let key = parseInt(keys[k]); let vo = me.visibleObjects[key]; try { if (vo?.parentNode) vo.parentNode.removeChild(vo); } catch (e) { console.log(e) } delete me.visibleObjects[key] } } } }) }, ...mixin, getWidth: function () { return parseFloat(`${this.data.width}`); }, getHeight: function () { return parseFloat(`${this.data.height}`); }, ...InteractionMixin, update: function (oldData) { let me = this; if (!this.data.direct) { if (JSON.stringify(this.data.options) !== JSON.stringify(oldData.options || '')) { let options = me.getOptions(); me.options = options; let keys = Object.keys(me.visibleObjects); for (let k = 0; k < keys.length; k++) { let key = parseInt(keys[k]); let vo = me.visibleObjects[key]; try { if (vo?.parentNode) vo.parentNode.removeChild(vo); } catch (e) { console.log(e) } delete me.visibleObjects[key] } } } if (this.data.width !== oldData?.width || this.data.height !== oldData?.height) { this.setup(); this.updateSubComponents(); } if (this.slider && this.data.textValueMap && this.data.textValueMap !== oldData.textValueMap) { this.slider.setAttribute('text-value-map', this.data.textValueMap); } }, handleInifniteListInteractions: function (args) { if (this.system) { console.log('handleInifniteListInteractions') let step = this.stepSize; let me = this; const { uv, x, y, raycaster, entity } = args; let raycastid = raycaster?.components?.raycaster?.el?.id; if (this.system.getButtonState(raycastid, 'thumbsticktouch') === 'down') { let detail = this.system.getButtonStateDetail(raycastid, 'thumbstickmoved'); console.log('thumbstickmoved') console.log(JSON.stringify(detail)) if (detail.y > 0.95) { console.log("DOWN"); this.setRaycastStates(raycastid, 'direction', 'down') } else if (detail.y < -0.95) { console.log("UP"); this.setRaycastStates(raycastid, 'direction', 'up') } else if (detail.x < -0.95) { console.log("LEFT"); this.setRaycastStates(raycastid, 'direction', 'left') } else if (detail.x > 0.95) { console.log("RIGHT"); this.setRaycastStates(raycastid, 'direction', 'right') } else { this.setRaycastStates(raycastid, 'direction', '') } this.setRaycastStates(raycastid, 'pressed', this.system.getButtonState(raycastid, 'trigger') === 'down') } else if (this.system.getButtonState(raycastid, 'thumbsticktouch') === 'up') { console.log('thumbsticktouch') this.setRaycastStates(raycastid, 'direction', '') } if (this.system && args) { const { x, y, raycaster } = args; let raycastid = raycaster?.components?.raycaster?.el?.id; if (raycastid) { switch (this.getRaycastState(raycastid, 'direction')) { case 'down': me.currentIndexScroll += step; break; case 'up': me.currentIndexScroll -= step; break; case 'left': break; case 'up': break; default: break; } } console.log(`me.currentIndexScroll: ${me.currentIndexScroll}`) } } }, tick: function () { let me = this; let columns = this.data.columns; this.onInit(); let itemHeight = me.itemHeight + me.itemMargin; let itemWidth = me.itemWidth + me.itemMargin; let windowHeight = me.windowHeight; let scrollPosition = me.currentIndexScroll; if (me.loadedFont) { let to_add = []; let to_remove = []; let is_adding = {}; let range = getVisibleItemRange(itemHeight, windowHeight - me.windowMargin.y, scrollPosition, columns) for (let i = range[0]; i <= range[1]; i++) { if (i >= 0 && me.options.length > i) { let optionIndex = i; if (!me.visibleObjects[optionIndex] && !is_adding[optionIndex]) { to_add.push(me.options[optionIndex]); is_adding[optionIndex] = true; } } } let keys = Object.keys(me.visibleObjects); for (let k = 0; k < keys.length; k++) { let key = parseInt(keys[k]); if (!(range[0] <= key && range[1] >= key)) { to_remove.push(key); } } to_remove.forEach((key) => { let vo = me.visibleObjects[key]; try { if (vo?.parentNode) { vo.parentNode.removeChild(vo); } } catch (e) { console.log(e) } delete me.visibleObjects[key] }); to_add.forEach((option: { id: number, value: number, text: string }) => { let { entity, onAppend } = me.createOptionEl(option, (Math.PI / me.visible_items / 2) * option.id); me.sprocket.appendChild(entity); me.visibleObjects[option.id] = entity; if (onAppend) { onAppend(); } }) // console.log(`me.currentIndexScroll: ${me.currentIndexScroll}`) // this.currentIndex = Math.floor(me.currentIndexScroll % me.visible_items); this.currentRotation = me.currentIndexScroll; for (let key in me.visibleObjects) { let indexKey = parseInt(key); let posY = calculateItemPosition(indexKey, itemHeight, me.currentIndexScroll, columns) + (itemHeight / 2) let posX = calculateColumn(indexKey, itemWidth, columns) - (this.outerBoxWidth / 2) + (itemWidth / 2); me.visibleObjects[key].setAttribute('position', `${posX} ${posY} 0`); if (indexKey > (range[1] - columns) || indexKey < (range[0] + columns)) { me.visibleObjects[key].setAttribute('opacity', 0); } else { me.visibleObjects[key].setAttribute('opacity', 1); } } // me.time = me.time || 0 // me.time += .001; // me.currentIndexScroll -= Math.sin(me.time) * .001 if (Math.abs(me.currentIndexScroll - me.targetScroll) > .0001) { if (Math.abs(me.currentIndexScroll - me.targetScroll) > this.outerBoxWidth) { if (me.currentIndexScroll > me.targetScroll) { me.currentIndexScroll = me.currentIndexScroll - this.outerBoxWidth; } else { me.currentIndexScroll = me.currentIndexScroll + this.outerBoxWidth; } } me.currentIndexScroll = lerp(me.currentIndexScroll, me.targetScroll, this.data.speed || .3) } } // me.sprocket.setAttribute('rotation', `${currentRotation % 360} 0 0`) // this.currentRotation += .1 }, createOptionEl: function (options) { let me = this; let { value, id, text, url } = options; if (me?.data?.itemtemplate) { let item = document.createElement(me.data.itemtemplate); if (!me.data.direct) { item.setAttribute('options', JSON.stringify({ ...options, imageMargin: me.imageMargin, selectionevent: me.data.selectionevent, guiItem: me.guiItem, value, id, text, url, })); } item.setAttribute('selectionevent', this.data.selectionevent); let entity: any = document.createElement('a-entity'); entity.setAttribute('rotation', `0 0 0`) entity.appendChild(item); return { entity, onAppend: () => { if (me.data.direct) { updateAttributeEl(item, 'options', { ...options, imageMargin: me.imageMargin, selectionevent: me.data.selectionevent, guiItem: me.guiItem, value, id, text, url, }) } } }; } let entity: any = document.createElement('a-entity'); entity.setAttribute('rotation', `0 0 0`) let imageEntity: any = document.createElement('frame-ar-image'); imageEntity.setAttribute('maxwidth', `${me.guiItem.height - me.imageMargin}`); imageEntity.setAttribute('maxheight', `${me.guiItem.height - me.imageMargin}`); imageEntity.setAttribute('position', `${(-me.guiItem.width / 2) + (me.guiItem.height / 2)} 0 .03`) imageEntity.setAttribute('url', url); var buttonContainer = document.createElement("frame-gui-button"); buttonContainer.setAttribute('geometry', `primitive: plane; width: ${me.guiItem.width}; height: ${me.guiItem.height}; depth: 0.02;`); buttonContainer.setAttribute('material', `shader: flat; opacity: 1; side:double; color: ${GetColor(1)}`); buttonContainer.setAttribute('rotation', '0 0 0'); buttonContainer.setAttribute('panel-color', GetColor(4)) buttonContainer.setAttribute('panel-rounded', "0.1"); buttonContainer.setAttribute('gap', '0.01') buttonContainer.setAttribute('base-depth', '0.01') buttonContainer.setAttribute('value', ``) buttonContainer.setAttribute('font-color', '#ffffff') buttonContainer.setAttribute('font-size', '.05') buttonContainer.setAttribute('position', `0 0 0.01`); buttonContainer.setAttribute('text-position', `0 ${(-me.guiItem.height / 2) + 0.04} .04`) buttonContainer.addEventListener('click', () => { }) buttonContainer.setAttribute('height', me.guiItem.height); buttonContainer.setAttribute('margin', `0 0 0.01 0`); buttonContainer.setAttribute('width', me.guiItem.width); buttonContainer.classList.add('raycastable') buttonContainer.addEventListener('click', function () { me.el.emit(me.data.selectionevent, { value, id, text, url }); }) let textEntity = me.createText(text || value || ``); let textBackground = document.createElement('a-entity'); textBackground.setAttribute('geometry', `primitive: plane; width: ${me.guiItem.width}; height: ${me.guiItem.height / 10}; depth: 0.02;`); textBackground.setAttribute('material', `shader: flat; opacity: 1; side:double; color: ${GetColor(0)}`); textBackground.setAttribute('position', `0 ${(-me.guiItem.height / 2) + 0.04} .04`) textBackground.appendChild(textEntity); buttonContainer.appendChild(textBackground); buttonContainer.appendChild(imageEntity); entity.appendChild(buttonContainer); return { entity } }, createText: function (newText) { var textEntity = document.createElement("a-entity"); textEntity.setAttribute('troika-text', `value: ${newText}; align:center; anchor:center; baseline:center; letterSpacing:0; color:${this.data.fontColor || '#ffffff'}; font:${this.data.fontFamily}; fontSize:${'14px'}; depthOffset:1; maxWidth:611; `); textEntity.setAttribute('position', `0 0 0.01`); let scle = 0.004; textEntity.setAttribute('scale', `${scle} ${scle} ${scle}`); // textEntity.setAttribute('troika-text-material', `shader: flat;`); return (textEntity); }, getOptions: function () { let me = this; if (this.data.direct) { if (me?.directOptions?.length) { return me?.directOptions; } } else { if (this?.data?.options?.length) { return this.data.options; } } return [ "Apple", ].map((text, index) => { return { id: index, value: index, text, url: 'https://cdn.midjourney.com/7acac49c-096e-41db-9ddb-9e592e0dd31b/0_2.webp' } }) } }); AFRAME.registerComponent('ar-image', { schema: { url: { type: 'string' }, maxwidth: { type: 'number' }, maxheight: { type: 'number' } }, init: function () { this.initImage(); }, ...mixin, getWidth: function () { return parseFloat(`${this?.size?.width || 0}`); }, getHeight: function () { return parseFloat(`${this?.size?.height || 0}`); }, update: function (oldData) { if (oldData.url && oldData.url !== this.data.url) { // this.el.setAttribute('material', { src: this.data.url }); this.initImage(); } else if (!oldData.url && oldData.url !== this.data.url) { this.initImage(); } }, initImage: function () { if (this.data.url) { this.el.setAttribute('visible', 'true') const image = new Image(); image.crossOrigin = 'anonymous'; // Use this if your image is hosted on a different domain image.src = this.data.url; image.onload = () => { const naturalAspectRatio = image.naturalWidth / image.naturalHeight; let finalWidth, finalHeight; // Calculate width and height based on aspect ratio and maximum dimensions if (image.naturalWidth > image.naturalHeight) { finalWidth = Math.min(this.data.maxwidth, image.naturalWidth); finalHeight = finalWidth / naturalAspectRatio; if (finalHeight > this.data.maxheight) { finalHeight = this.data.maxheight; finalWidth = finalHeight * naturalAspectRatio; } } else { finalHeight = Math.min(this.data.maxheight, image.naturalHeight); finalWidth = finalHeight * naturalAspectRatio; if (finalWidth > this.data.maxwidth) { finalWidth = this.data.maxwidth; finalHeight = finalWidth / naturalAspectRatio; } } this.size = { width: finalWidth, height: finalHeight } this.updateElementSize(this, this.el); // Set the geometry and material of the entity this.el.setAttribute('geometry', { primitive: 'plane', width: finalWidth, height: finalHeight }); // Create a texture from the loaded image const texture = new THREE.Texture(image); texture.needsUpdate = true; // Get the mesh of the A-Frame entity const mesh = this.el.getObject3D('mesh'); if (mesh) { // Apply the texture to the material mesh.material = new THREE.MeshBasicMaterial({ map: texture }); mesh.material.needsUpdate = true; } }; } else { this.el.setAttribute('visible', 'false') } } }); AFRAME.registerPrimitive('frame-ar-image', { defaultComponents: { 'ar-image': {} }, mappings: { url: 'ar-image.url', maxwidth: 'ar-image.maxwidth', maxheight: 'ar-image.maxheight' } }); AFRAME.registerPrimitive('frame-infinite-list', { defaultComponents: { 'infinity-list': {} }, mappings: { itemtemplate: 'infinity-list.itemtemplate', selectionevent: 'infinity-list.selectionevent', closeevent: 'infinity-list.closeevent', 'icon-font': 'infinity-list.iconFont', 'icon-font-size': 'infinity-list.iconFontSize', 'icon': 'infinity-list.icon', 'text-value-map': 'infinite-list.textValueMap', speed: 'infinity-list.speed', sourceId: 'infinity-list.sourceId', options: 'infinity-list.options', width: 'infinity-list.width', height: 'infinity-list.height', margin: 'infinity-list.boxMargin', columns: 'infinity-list.columns', itemsize: 'infinity-list.itemSize', itemheight: 'infinity-list.itemHeight', itemwidth: 'infinity-list.itemWidth', selected: 'infinity-list.selected', hideclose: 'infinity-list.hideClose', 'direct': 'infinity-list.direct', } }); // Example usage in an A-Frame scene // // // // Example usage in an A-Frame scene // // // } function isIndexOutsideRange(rangeStart, rangeEnd, index) { if (rangeStart > rangeEnd) { return !( (rangeStart <= index && rangeEnd <= index) || (rangeEnd >= index && rangeStart >= index) ); } return !(rangeStart <= index && rangeEnd >= index) } function isIndexOutsideRange_bad(totalItems, rangeStart, rangeEnd, index) { // Normalize indices to handle wrap-around rangeEnd = (rangeEnd >= totalItems) ? rangeEnd % totalItems : rangeEnd; rangeStart = (rangeStart >= totalItems) ? rangeStart % totalItems : rangeStart; index = (index >= totalItems) ? index % totalItems : index; if (rangeStart <= rangeEnd) { // Normal scenario: start index is less than or equal to end index return index < rangeStart || index > rangeEnd; } else { // Wrap-around scenario: start index is greater than end index // The index is outside the range if it's not between rangeEnd and rangeStart return !(index > rangeEnd || index < rangeStart); } } function calculateItemPosition(index: number, itemHeight: number, scrollPosition: number, columns: number = 1): number { // Calculate the row number where the item is located const row = Math.floor(index / columns); // Calculate the starting position of the item based on its row const itemStartPosition = row * itemHeight; // Adjust by the scroll position to get its position within the current view const relativePosition = itemStartPosition - scrollPosition; return relativePosition; } function calculateColumn(index: number, itemWidth: number, columns: number = 1): number { // Ensure the number of columns is at least 1 columns = Math.max(columns, 1); // The column is the remainder of the index divided by the number of columns return (index % columns) * itemWidth; } function getNumberOfVisibleItems(itemHeight: number, windowHeight: number, column: number) { return (windowHeight / itemHeight) * column; } function getVisibleItemRange(itemHeight: number, windowHeight: number, scrollPosition: number, columns: number = 1): [number, number] { // Ensure that the first index is not negative const firstIndex = Math.max(0, Math.floor(scrollPosition / itemHeight) * columns); // Calculate the last index, ensuring it doesn't go negative const lastIndex = Math.max(0, Math.ceil((scrollPosition + windowHeight) / itemHeight) * columns) - 1; return [firstIndex, lastIndex]; } function lerp(start: number, end: number, t: number): number { return start * (1 - t) + end * t; }