diff --git a/packages/scene-composer/src/augmentations/components/three-fiber/anchor/AnchorWidget.tsx b/packages/scene-composer/src/augmentations/components/three-fiber/anchor/AnchorWidget.tsx index 0d9997f73..a6390c600 100644 --- a/packages/scene-composer/src/augmentations/components/three-fiber/anchor/AnchorWidget.tsx +++ b/packages/scene-composer/src/augmentations/components/three-fiber/anchor/AnchorWidget.tsx @@ -15,6 +15,7 @@ import { DefaultAnchorStatus, INavLink, IRuleBasedMap, + ITagSettings, IValueDataBinding, KnownComponentType, SceneResourceType, @@ -28,6 +29,7 @@ import { getSceneResourceInfo } from '../../../../utils/sceneResourceUtils'; import svgIconToWidgetSprite from '../common/SvgIconToWidgetSprite'; import { findComponentByType } from '../../../../utils/nodeUtils'; import { Layers } from '../../../../common/constants'; +import { componentSettingsSelector } from '../../../../utils/componentSettingsUtils'; export interface AnchorWidgetProps { node: ISceneNodeInternal; @@ -59,6 +61,12 @@ export function AsyncLoadedAnchorWidget({ dataBindingTemplate, } = useStore(sceneComposerId)((state) => state); const isViewing = useStore(sceneComposerId)((state) => state.isViewing()); + const tagSettings: ITagSettings = useStore(sceneComposerId)((state) => + componentSettingsSelector(state, KnownComponentType.Tag), + ); + const autoRescale = useMemo(() => { + return tagSettings.autoRescale; + }, [tagSettings.autoRescale]); const onWidgetClick = useStore(sceneComposerId)((state) => state.getEditorConfig().onWidgetClick); const getObject3DFromSceneNodeRef = useStore(sceneComposerId)((state) => state.getObject3DBySceneNodeRef); @@ -77,8 +85,11 @@ export function AsyncLoadedAnchorWidget({ const baseScale = useMemo(() => { // NOTE: For Fixed Size value was [0.05, 0.05, 1] - return new THREE.Vector3(0.5, 0.5, 1).multiply(new THREE.Vector3(...node.transform.scale)); - }, [node.transform.scale]); + const defaultScale = autoRescale ? [0.05, 0.05, 1] : [0.5, 0.5, 1]; + return new THREE.Vector3(...defaultScale).multiply( + new THREE.Vector3(tagSettings.scale, tagSettings.scale, tagSettings.scale), + ); + }, [autoRescale, tagSettings.scale]); useEffect(() => { setParent(node.parentRef ? getObject3DFromSceneNodeRef(node.parentRef) : undefined); @@ -115,9 +126,9 @@ export function AsyncLoadedAnchorWidget({ VideoIconSvgString, ]; return iconStrings.map((iconString, index) => { - return svgIconToWidgetSprite(iconString, keys[index], isAlwaysVisible); + return svgIconToWidgetSprite(iconString, keys[index], isAlwaysVisible, !autoRescale); }); - }, []); + }, [autoRescale]); const isAnchor = (nodeRef?: string) => { const node = getSceneNodeByRef(nodeRef); diff --git a/packages/scene-composer/src/augmentations/components/three-fiber/anchor/__tests__/AnchorWidget.spec.tsx b/packages/scene-composer/src/augmentations/components/three-fiber/anchor/__tests__/AnchorWidget.spec.tsx index 22f0925aa..62d22ac81 100644 --- a/packages/scene-composer/src/augmentations/components/three-fiber/anchor/__tests__/AnchorWidget.spec.tsx +++ b/packages/scene-composer/src/augmentations/components/three-fiber/anchor/__tests__/AnchorWidget.spec.tsx @@ -27,6 +27,7 @@ describe('AnchorWidget', () => { const setHighlightedSceneNodeRef = jest.fn(); const setSelectedSceneNodeRef = jest.fn(); const getObject3DBySceneNodeRef = jest.fn(); + const getSceneProperty = jest.fn(); const node = { ref: 'test-ref', @@ -51,11 +52,13 @@ describe('AnchorWidget', () => { getEditorConfig: () => ({ onWidgetClick }), dataInput: 'dataInput' as any, getObject3DBySceneNodeRef, + getSceneProperty, } as any); }; beforeEach(() => { (useLoader as unknown as jest.Mock).mockReturnValue(['TestSvgData']); + getSceneProperty.mockReturnValue(undefined); jest.clearAllMocks(); }); @@ -93,6 +96,14 @@ describe('AnchorWidget', () => { expect(container).toMatchSnapshot(); }); + it('should render correctly with non default tag settings', () => { + setStore('test-ref', 'test-ref'); + getSceneProperty.mockReturnValue({ Tag: { scale: 3, autoRescale: true } }); + const container = renderer.create(); + + expect(container).toMatchSnapshot(); + }); + it('should render correctly with an offset', () => { setStore('test-ref', 'test-ref'); const node = { diff --git a/packages/scene-composer/src/augmentations/components/three-fiber/anchor/__tests__/__snapshots__/AnchorWidget.spec.tsx.snap b/packages/scene-composer/src/augmentations/components/three-fiber/anchor/__tests__/__snapshots__/AnchorWidget.spec.tsx.snap index b699006b4..fe3a04b3b 100644 --- a/packages/scene-composer/src/augmentations/components/three-fiber/anchor/__tests__/__snapshots__/AnchorWidget.spec.tsx.snap +++ b/packages/scene-composer/src/augmentations/components/three-fiber/anchor/__tests__/__snapshots__/AnchorWidget.spec.tsx.snap @@ -116,6 +116,64 @@ exports[`AnchorWidget should render correctly with an offset 1`] = ` `; +exports[`AnchorWidget should render correctly with non default tag settings 1`] = ` + + + + + + + +
+
+
+
+
+ + + +`; + exports[`AnchorWidget should render with a countered size when parent is scaled 1`] = ` { + const icons = [ + ['Selected', { key: SelectedAnchor, icon: SelectedIconSvgString }], + ['Info', { key: DefaultAnchorStatus.Info, icon: InfoIconSvgString }], + ['Warning', { key: DefaultAnchorStatus.Warning, icon: WarningIconSvgString }], + ['Error', { key: DefaultAnchorStatus.Error, icon: ErrorIconSvgString }], + ['Video', { key: DefaultAnchorStatus.Video, icon: VideoIconSvgString }], + ]; + + icons.forEach((value) => { + it(`it should render the ${value[0]} correctly`, () => { + jest.spyOn(window.Math, 'random').mockReturnValue(0.1); + const { key, icon } = value[1] as any; + const container = renderer.create(svgIconToWidgetSprite(icon, key, false, true)); + + expect(container).toMatchSnapshot(); + }); + }); + + icons.forEach((value) => { + it(`it should render the always visible ${value[0]} correctly`, () => { + jest.spyOn(window.Math, 'random').mockReturnValue(0.1); + const { key, icon } = value[1] as any; + const container = renderer.create(svgIconToWidgetSprite(icon, key, true, true)); + + expect(container).toMatchSnapshot(); + }); + }); + + icons.forEach((value) => { + it(`it should render the constant sized ${value[0]} correctly`, () => { + jest.spyOn(window.Math, 'random').mockReturnValue(0.1); + const { key, icon } = value[1] as any; + const container = renderer.create(svgIconToWidgetSprite(icon, key, false, false)); + + expect(container).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/scene-composer/src/augmentations/components/three-fiber/common/SvgIconToWidgetSprite.tsx b/packages/scene-composer/src/augmentations/components/three-fiber/common/SvgIconToWidgetSprite.tsx index 31f426a59..0a7128657 100644 --- a/packages/scene-composer/src/augmentations/components/three-fiber/common/SvgIconToWidgetSprite.tsx +++ b/packages/scene-composer/src/augmentations/components/three-fiber/common/SvgIconToWidgetSprite.tsx @@ -9,6 +9,7 @@ export default function svgIconToWidgetSprite( svg: string, key: DefaultAnchorStatus | string, alwaysVisible, + sizeAttenuation: boolean, // when true, tag size changes when zooming props?: WidgetSpriteProps, ) { if (!THREE.Cache.get(key)) { @@ -21,29 +22,30 @@ export default function svgIconToWidgetSprite( THREE.Cache.add(key, texture); } - const group = new THREE.Group(); - const texture = THREE.Cache.get(key); - // NOTE: sizeAttenuation is true by default, but I am leaving this as setting it to false is what maintains a constant size. - const spriteMaterial = new THREE.SpriteMaterial({ map: texture, sizeAttenuation: true }); - spriteMaterial.color.convertSRGBToLinear(); - const sprite = new THREE.Sprite(spriteMaterial); - sprite.renderOrder = RenderOrder.DrawLate; - - group.add(sprite); - - if (alwaysVisible) { - const altSpriteMaterial = spriteMaterial.clone(); - altSpriteMaterial.depthFunc = THREE.GreaterDepth; - altSpriteMaterial.opacity = 0.5; - const altSprite = new THREE.Sprite(altSpriteMaterial); - altSprite.renderOrder = RenderOrder.DrawLate; - group.add(altSprite); - } return ( - + + + + + {alwaysVisible && ( + + + + )} + ); } diff --git a/packages/scene-composer/src/augmentations/components/three-fiber/common/__snapshots__/SvgIconToWidgetSprite.spec.tsx.snap b/packages/scene-composer/src/augmentations/components/three-fiber/common/__snapshots__/SvgIconToWidgetSprite.spec.tsx.snap new file mode 100644 index 000000000..921335e51 --- /dev/null +++ b/packages/scene-composer/src/augmentations/components/three-fiber/common/__snapshots__/SvgIconToWidgetSprite.spec.tsx.snap @@ -0,0 +1,316 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`svgIconToWidgetSprite it should render the Error correctly 1`] = ` + + + + + + + +`; + +exports[`svgIconToWidgetSprite it should render the Info correctly 1`] = ` + + + + + + + +`; + +exports[`svgIconToWidgetSprite it should render the Selected correctly 1`] = ` + + + + + + + +`; + +exports[`svgIconToWidgetSprite it should render the Video correctly 1`] = ` + + + + + + + +`; + +exports[`svgIconToWidgetSprite it should render the Warning correctly 1`] = ` + + + + + + + +`; + +exports[`svgIconToWidgetSprite it should render the always visible Error correctly 1`] = ` + + + + + + + + + + +`; + +exports[`svgIconToWidgetSprite it should render the always visible Info correctly 1`] = ` + + + + + + + + + + +`; + +exports[`svgIconToWidgetSprite it should render the always visible Selected correctly 1`] = ` + + + + + + + + + + +`; + +exports[`svgIconToWidgetSprite it should render the always visible Video correctly 1`] = ` + + + + + + + + + + +`; + +exports[`svgIconToWidgetSprite it should render the always visible Warning correctly 1`] = ` + + + + + + + + + + +`; + +exports[`svgIconToWidgetSprite it should render the constant sized Error correctly 1`] = ` + + + + + + + +`; + +exports[`svgIconToWidgetSprite it should render the constant sized Info correctly 1`] = ` + + + + + + + +`; + +exports[`svgIconToWidgetSprite it should render the constant sized Selected correctly 1`] = ` + + + + + + + +`; + +exports[`svgIconToWidgetSprite it should render the constant sized Video correctly 1`] = ` + + + + + + + +`; + +exports[`svgIconToWidgetSprite it should render the constant sized Warning correctly 1`] = ` + + + + + + + +`; diff --git a/packages/scene-composer/src/common/constants.ts b/packages/scene-composer/src/common/constants.ts index fe6b478ed..06f6a78b0 100644 --- a/packages/scene-composer/src/common/constants.ts +++ b/packages/scene-composer/src/common/constants.ts @@ -2,7 +2,13 @@ import * as THREE from 'three'; import { Component, LightType } from '../models/SceneModels'; import { InfoIconSvgString, WarningIconSvgString, ErrorIconSvgString, VideoIconSvgString } from '../assets'; -import { IValueDataBindingProviderState, DefaultAnchorStatus, DistanceUnit, Vector3 } from '../interfaces'; +import { + IValueDataBindingProviderState, + DefaultAnchorStatus, + DistanceUnit, + Vector3, + ITagSettings, +} from '../interfaces'; import { CameraControlImpl } from '../store/internalInterfaces'; /****************************************************************************** @@ -115,6 +121,11 @@ export const SCENE_BODY_CLASS = 'twinmaker_scene_container'; export const DRACO_PATH = 'https://www.gstatic.com/draco/versioned/decoders/1.4.1/'; +export const DEFAULT_TAG_GLOBAL_SETTINGS: ITagSettings = { + autoRescale: false, + scale: 1, +}; + /****************************************************************************** * Camera Constants ******************************************************************************/ diff --git a/packages/scene-composer/src/components/panels/SceneNodeInspectorPanel.tsx b/packages/scene-composer/src/components/panels/SceneNodeInspectorPanel.tsx index 9eae49e01..8cd796e07 100644 --- a/packages/scene-composer/src/components/panels/SceneNodeInspectorPanel.tsx +++ b/packages/scene-composer/src/components/panels/SceneNodeInspectorPanel.tsx @@ -28,7 +28,6 @@ export const SceneNodeInspectorPanel: React.FC = () => { const selectedSceneNode = getSceneNodeByRef(selectedSceneNodeRef); const intl = useIntl(); - const tagResizeEnabled = getGlobalSettings().featureConfig[COMPOSER_FEATURES.TagResize]; const subModelMovementEnabled = getGlobalSettings().featureConfig[COMPOSER_FEATURES.SubModelMovement]; const i18nKnownComponentTypesStrings = defineMessages({ @@ -89,7 +88,7 @@ export const SceneNodeInspectorPanel: React.FC = () => { const transformVisible = !isSubModelComponent || subModelMovementEnabled; - const shouldShowScale = !((isTagComponent && !tagResizeEnabled) || isCameraComponent); + const shouldShowScale = !(isTagComponent || isCameraComponent); const readonly: Triplet = [false, false, false]; diff --git a/packages/scene-composer/src/components/panels/SettingsPanel.tsx b/packages/scene-composer/src/components/panels/SettingsPanel.tsx index 105bd2975..557f62b95 100644 --- a/packages/scene-composer/src/components/panels/SettingsPanel.tsx +++ b/packages/scene-composer/src/components/panels/SettingsPanel.tsx @@ -6,11 +6,12 @@ import useLifecycleLogging from '../../logger/react-logger/hooks/useLifecycleLog import { presets } from '../three-fiber/Environment'; import { sceneComposerIdContext } from '../../common/sceneComposerIdContext'; import { useStore } from '../../store'; -import { IValueDataBindingProvider, KnownSceneProperty } from '../../interfaces'; +import { COMPOSER_FEATURES, IValueDataBindingProvider, KnownSceneProperty } from '../../interfaces'; import { pascalCase } from '../../utils/stringUtils'; +import { getGlobalSettings } from '../../common/GlobalSettings'; import { ExpandableInfoSection } from './CommonPanelComponents'; -import { SceneDataBindingTemplateEditor } from './scene-settings'; +import { SceneDataBindingTemplateEditor, SceneTagSettingsEditor } from './scene-settings'; export interface SettingsPanelProps { valueDataBindingProvider?: IValueDataBindingProvider; @@ -22,6 +23,8 @@ export const SettingsPanel: React.FC = ({ valueDataBindingPr const setSceneProperty = useStore(sceneComposerId)((state) => state.setSceneProperty); const intl = useIntl(); + const tagResizeEnabled = getGlobalSettings().featureConfig[COMPOSER_FEATURES.TagResize]; + const selectedEnvPreset = useStore(sceneComposerId)((state) => state.getSceneProperty(KnownSceneProperty.EnvironmentPreset), ); @@ -92,6 +95,16 @@ export const SettingsPanel: React.FC = ({ valueDataBindingPr + + {tagResizeEnabled && ( + + + + )} + {valueDataBindingProvider && ( , HTMLInputElement> {} diff --git a/packages/scene-composer/tests/components/panels/SettingsPanel.spec.tsx b/packages/scene-composer/src/components/panels/__tests__/SettingsPanel.spec.tsx similarity index 56% rename from packages/scene-composer/tests/components/panels/SettingsPanel.spec.tsx rename to packages/scene-composer/src/components/panels/__tests__/SettingsPanel.spec.tsx index badcf9cd2..b92d50503 100644 --- a/packages/scene-composer/tests/components/panels/SettingsPanel.spec.tsx +++ b/packages/scene-composer/src/components/panels/__tests__/SettingsPanel.spec.tsx @@ -4,9 +4,11 @@ import { shallow, configure } from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; import { Select } from '@awsui/components-react'; -import { SettingsPanel } from '../../../src/components/panels'; -import { ExpandableInfoSection } from '../../../src/components/panels/CommonPanelComponents'; -import { useStore } from '../../../src/store'; +import { SettingsPanel } from '..'; +import { ExpandableInfoSection } from '../CommonPanelComponents'; +import { useStore } from '../../../store'; +import { setFeatureConfig } from '../../../common/GlobalSettings'; +import { COMPOSER_FEATURES } from '../../../interfaces'; jest.spyOn(React, 'useContext').mockReturnValue('sceneComponserId' as any); @@ -22,7 +24,8 @@ describe('SettingsPanel contains expected elements.', () => { const wrapper = shallow(); const expandableInfoSection = wrapper.find(ExpandableInfoSection); - expect(expandableInfoSection.props().title).toEqual('Scene Settings'); + expect(expandableInfoSection.length).toEqual(1); + expect(expandableInfoSection.at(0).props().title).toEqual('Scene Settings'); const selectProps = expandableInfoSection.find(Select).props(); @@ -41,4 +44,20 @@ describe('SettingsPanel contains expected elements.', () => { expect(setSceneProperty.mock.calls[0][1]).toEqual(undefined); setSceneProperty.mockReset(); }); + + it('SettingsPanel contains tag settings element.', async () => { + setFeatureConfig({ [COMPOSER_FEATURES.TagResize]: true }); + const setSceneProperty = jest.fn(); + useStore('sceneComponserId').setState({ + setSceneProperty: setSceneProperty, + getSceneProperty: jest.fn().mockReturnValue('neutral'), + }); + + const wrapper = shallow(); + + const expandableInfoSections = wrapper.find(ExpandableInfoSection); + expect(expandableInfoSections.length).toEqual(2); + expect(expandableInfoSections.at(0).props().title).toEqual('Scene Settings'); + expect(expandableInfoSections.at(1).props().title).toEqual('Tag Settings'); + }); }); diff --git a/packages/scene-composer/tests/components/panels/scene-components/motion-indicator/Slider.spec.tsx b/packages/scene-composer/src/components/panels/__tests__/Slider.spec.tsx similarity index 86% rename from packages/scene-composer/tests/components/panels/scene-components/motion-indicator/Slider.spec.tsx rename to packages/scene-composer/src/components/panels/__tests__/Slider.spec.tsx index 6377dd222..da01cf8c6 100644 --- a/packages/scene-composer/tests/components/panels/scene-components/motion-indicator/Slider.spec.tsx +++ b/packages/scene-composer/src/components/panels/__tests__/Slider.spec.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { fireEvent, render, screen } from '@testing-library/react'; -import { Slider } from '../../../../../src/components/panels/scene-components/motion-indicator/Slider'; +import { Slider } from '../Slider'; describe('Slider', () => { it('should render correctly', () => { diff --git a/packages/scene-composer/tests/components/panels/scene-components/motion-indicator/__snapshots__/Slider.spec.tsx.snap b/packages/scene-composer/src/components/panels/__tests__/__snapshots__/Slider.spec.tsx.snap similarity index 100% rename from packages/scene-composer/tests/components/panels/scene-components/motion-indicator/__snapshots__/Slider.spec.tsx.snap rename to packages/scene-composer/src/components/panels/__tests__/__snapshots__/Slider.spec.tsx.snap diff --git a/packages/scene-composer/src/components/panels/scene-components/motion-indicator/ColorEditor.spec.tsx b/packages/scene-composer/src/components/panels/scene-components/motion-indicator/ColorEditor.spec.tsx index 3ccde93ee..683a1a7d3 100644 --- a/packages/scene-composer/src/components/panels/scene-components/motion-indicator/ColorEditor.spec.tsx +++ b/packages/scene-composer/src/components/panels/scene-components/motion-indicator/ColorEditor.spec.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { fireEvent, render } from '@testing-library/react'; -import wrapper from '@awsui/components-react/test-utils/dom'; import { mockComponent, mockProvider } from '../../../../../tests/components/panels/scene-components/MockComponents'; import { IMotionIndicatorComponentInternal, useStore } from '../../../../store'; @@ -18,8 +17,8 @@ jest.mock('./DataBindingEditor', () => { }); let sliderOnChangeCb; -jest.mock('./Slider', () => { - const originalModule = jest.requireActual('./Slider'); +jest.mock('../../Slider', () => { + const originalModule = jest.requireActual('../../Slider'); return { ...originalModule, Slider: (...props: any[]) => { diff --git a/packages/scene-composer/src/components/panels/scene-components/motion-indicator/ColorEditor.tsx b/packages/scene-composer/src/components/panels/scene-components/motion-indicator/ColorEditor.tsx index c7e3140fe..e7db64a1c 100644 --- a/packages/scene-composer/src/components/panels/scene-components/motion-indicator/ColorEditor.tsx +++ b/packages/scene-composer/src/components/panels/scene-components/motion-indicator/ColorEditor.tsx @@ -9,9 +9,9 @@ import { IMotionIndicatorComponentInternal, useStore } from '../../../../store'; import { sceneComposerIdContext } from '../../../../common/sceneComposerIdContext'; import { Component } from '../../../../models/SceneModels'; import { colors } from '../../../../utils/styleUtils'; +import { Slider } from '../../Slider'; import { DataBindingEditor } from './DataBindingEditor'; -import { Slider } from './Slider'; const ColorSwatch = styled.div<{ backgroundColor: string }>` background-color: ${(props) => props.backgroundColor}; diff --git a/packages/scene-composer/src/components/panels/scene-components/motion-indicator/SpeedEditor.tsx b/packages/scene-composer/src/components/panels/scene-components/motion-indicator/SpeedEditor.tsx index e908eac2b..f5991b1e5 100644 --- a/packages/scene-composer/src/components/panels/scene-components/motion-indicator/SpeedEditor.tsx +++ b/packages/scene-composer/src/components/panels/scene-components/motion-indicator/SpeedEditor.tsx @@ -6,9 +6,9 @@ import { useIntl } from 'react-intl'; import { IMotionIndicatorComponentInternal, useStore } from '../../../../store'; import { sceneComposerIdContext } from '../../../../common/sceneComposerIdContext'; import { Component } from '../../../../models/SceneModels'; +import { Slider } from '../../Slider'; import { DataBindingEditor } from './DataBindingEditor'; -import { Slider } from './Slider'; interface ISpeedEditorProps { component: IMotionIndicatorComponentInternal; diff --git a/packages/scene-composer/src/components/panels/scene-settings/SceneTagSettingsEditor.spec.tsx b/packages/scene-composer/src/components/panels/scene-settings/SceneTagSettingsEditor.spec.tsx new file mode 100644 index 000000000..c96052f80 --- /dev/null +++ b/packages/scene-composer/src/components/panels/scene-settings/SceneTagSettingsEditor.spec.tsx @@ -0,0 +1,135 @@ +/* eslint-disable import/first */ +import React from 'react'; +import { act, fireEvent, render, screen } from '@testing-library/react'; +import wrapper from '@awsui/components-react/test-utils/dom'; + +import { useStore } from '../../../store'; +import { KnownComponentType, KnownSceneProperty } from '../../../interfaces'; +import { DEFAULT_TAG_GLOBAL_SETTINGS } from '../../../common/constants'; + +import { SceneTagSettingsEditor } from './SceneTagSettingsEditor'; + +jest.mock('@awsui/components-react', () => ({ + ...jest.requireActual('@awsui/components-react'), +})); + +const sleep = async (timeout: number) => { + return new Promise((resolve) => setTimeout(resolve, timeout)); +}; + +describe('SceneTagSettingsEditor', () => { + const setScenePropertyMock = jest.fn(); + const getScenePropertyMock = jest.fn(); + const baseState = { + setSceneProperty: setScenePropertyMock, + getSceneProperty: getScenePropertyMock, + }; + + beforeEach(() => { + jest.useRealTimers(); + getScenePropertyMock.mockReturnValue({ + [KnownComponentType.Tag]: DEFAULT_TAG_GLOBAL_SETTINGS, + }); + jest.clearAllMocks(); + }); + + it('should update store when input value changed', async () => { + useStore('default').setState(baseState); + const { container } = render(); + const polarisWrapper = wrapper(container); + const input = polarisWrapper.findInput(); + + expect(input).toBeDefined(); + + // change input should update store when value is valid + input?.setInputValue('11'); + expect(setScenePropertyMock).toBeCalledTimes(1); + expect(setScenePropertyMock).toBeCalledWith(KnownSceneProperty.ComponentSettings, { + [KnownComponentType.Tag]: { ...DEFAULT_TAG_GLOBAL_SETTINGS, scale: 11 }, + }); + + // change input should update store with 0 when value is invalid + input?.setInputValue('-11'); + expect(setScenePropertyMock).toBeCalledTimes(2); + expect(setScenePropertyMock).toBeCalledWith(KnownSceneProperty.ComponentSettings, { + [KnownComponentType.Tag]: { ...DEFAULT_TAG_GLOBAL_SETTINGS, scale: 0 }, + }); + }); + + it('should update input value when store value changed', async () => { + useStore('default').setState(baseState); + const { container } = render(); + const polarisWrapper = wrapper(container); + const input = polarisWrapper.findInput(); + + expect(input).toBeDefined(); + expect(input?.findNativeInput().getElement().value).toEqual(String(DEFAULT_TAG_GLOBAL_SETTINGS.scale)); + + getScenePropertyMock.mockReturnValue({ + [KnownComponentType.Tag]: { ...DEFAULT_TAG_GLOBAL_SETTINGS, scale: 6 }, + }); + act(() => { + useStore('default').setState({ ...baseState }); + }); + + expect(polarisWrapper.findInput()?.findNativeInput().getElement().value).toEqual('6'); + }); + + it('should update store when checkbox clicked', async () => { + useStore('default').setState(baseState); + const { container } = render(); + const polarisWrapper = wrapper(container); + const checkbox = polarisWrapper.findCheckbox(); + + expect(checkbox).toBeDefined(); + + // click checkbox should update store + checkbox?.findNativeInput().click(); + expect(setScenePropertyMock).toBeCalledTimes(1); + expect(setScenePropertyMock).toBeCalledWith(KnownSceneProperty.ComponentSettings, { + [KnownComponentType.Tag]: { + ...DEFAULT_TAG_GLOBAL_SETTINGS, + autoRescale: !DEFAULT_TAG_GLOBAL_SETTINGS.autoRescale, + }, + }); + }); + + it('should update store when slider value changed', async () => { + useStore('default').setState(baseState); + const { container } = render(); + const polarisWrapper = wrapper(container); + + // slider is hidden initially + expect(screen.queryAllByTestId('slider').length).toBe(0); + + // show slider when input gets focus + const input = polarisWrapper.findInput(); + act(() => { + input?.focus(); + }); + const slider = screen.queryAllByTestId('slider'); + expect(slider.length).toBe(1); + + await act(async () => { + slider[0].focus(); + + input?.blur(); + // wait for setTimeOut in code + await sleep(2); + + // update slider value should update store + fireEvent.change(slider[0], { target: { value: '3' } }); + }); + + expect(setScenePropertyMock).toBeCalledTimes(1); + expect(setScenePropertyMock).toBeCalledWith(KnownSceneProperty.ComponentSettings, { + [KnownComponentType.Tag]: { ...DEFAULT_TAG_GLOBAL_SETTINGS, scale: 3 }, + }); + + // hide slider when slider lost focus + act(() => { + slider[0].blur(); + }); + expect(screen.queryAllByTestId('slider').length).toBe(0); + }); +}); diff --git a/packages/scene-composer/src/components/panels/scene-settings/SceneTagSettingsEditor.tsx b/packages/scene-composer/src/components/panels/scene-settings/SceneTagSettingsEditor.tsx new file mode 100644 index 000000000..0bc054abb --- /dev/null +++ b/packages/scene-composer/src/components/panels/scene-settings/SceneTagSettingsEditor.tsx @@ -0,0 +1,136 @@ +import React, { useCallback, useContext, useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; +import { Checkbox, FormField, Grid, Input } from '@awsui/components-react'; + +import useLifecycleLogging from '../../../logger/react-logger/hooks/useLifecycleLogging'; +import { useStore } from '../../../store'; +import { sceneComposerIdContext } from '../../../common/sceneComposerIdContext'; +import { IComponentSettingsMap, ITagSettings, KnownComponentType, KnownSceneProperty } from '../../../interfaces'; +import { componentSettingsSelector } from '../../../utils/componentSettingsUtils'; +import { Slider } from '../Slider'; + +export const SceneTagSettingsEditor: React.FC = () => { + useLifecycleLogging('SceneTagSettingsEditor'); + + const sceneComposerId = useContext(sceneComposerIdContext); + const intl = useIntl(); + const setSceneProperty = useStore(sceneComposerId)((state) => state.setSceneProperty); + const getSceneProperty = useStore(sceneComposerId)((state) => state.getSceneProperty); + const tagSettings: ITagSettings = useStore(sceneComposerId)((state) => + componentSettingsSelector(state, KnownComponentType.Tag), + ); + const [dirty, setDirty] = useState(false); + const [focusInput, setFocusInput] = useState(false); + const [focusSlider, setFocusSlider] = useState(false); + const [draggingSlider, setDraggingSlider] = useState(false); + + const [internalScale, setInternalScale] = useState(tagSettings.scale); + + const updateSettings = useCallback( + (settingsPartial: Partial) => { + const newTagSettings: ITagSettings = { + ...tagSettings, + ...settingsPartial, + }; + const newComponentSettings: IComponentSettingsMap = { + ...getSceneProperty(KnownSceneProperty.ComponentSettings), + [KnownComponentType.Tag]: newTagSettings, + }; + + setSceneProperty(KnownSceneProperty.ComponentSettings, newComponentSettings); + }, + [tagSettings, getSceneProperty, setSceneProperty], + ); + + const onSliderChange = useCallback( + (event) => { + setInternalScale(Number(event.target.value)); + setDirty(true); + }, + [setInternalScale, setDirty], + ); + + const onInputBlur = useCallback(() => { + // If slider is getting focus, this makes sure to execute setFocusSlider(true) first to keep showing it. + const id = setTimeout(() => { + setFocusInput(false); + }, 1); + return () => { + clearTimeout(id); + }; + }, [setFocusInput]); + + const onInputChange = useCallback( + (event) => { + let value = event.detail.value; + if (value < 0) { + value = 0; + } + + if (value !== internalScale) { + setInternalScale(Number(value)); + setDirty(true); + } + }, + [setInternalScale, setDirty], + ); + + // Save scale changes to scene file + useEffect(() => { + if (dirty && !focusInput && !draggingSlider) { + updateSettings({ scale: internalScale }); + setDirty(false); + } + }, [updateSettings, setDirty, dirty, focusInput, internalScale, draggingSlider]); + + // Update internal when scale in store is changed + useEffect(() => { + if (!dirty && tagSettings.scale !== internalScale) { + setInternalScale(tagSettings.scale); + } + }, [dirty, draggingSlider, tagSettings.scale, internalScale]); + + return ( + + + + setFocusInput(true)} + onBlur={onInputBlur} + onChange={onInputChange} + /> + updateSettings({ autoRescale: e.detail.checked })} + > + {intl.formatMessage({ defaultMessage: 'Auto rescale', description: 'checkbox label' })} + + + + {(focusInput || focusSlider) && ( + { + setFocusSlider(true); + }} + onBlur={() => { + setFocusSlider(false); + }} + onMouseUp={() => { + setDraggingSlider(false); + }} + onMouseDown={() => { + setDraggingSlider(true); + }} + onChange={onSliderChange} + /> + )} + + + ); +}; diff --git a/packages/scene-composer/src/components/panels/scene-settings/index.ts b/packages/scene-composer/src/components/panels/scene-settings/index.ts index 28405fd4f..d0f255486 100644 --- a/packages/scene-composer/src/components/panels/scene-settings/index.ts +++ b/packages/scene-composer/src/components/panels/scene-settings/index.ts @@ -1 +1,2 @@ export * from './SceneDataBindingTemplateEditor'; +export * from './SceneTagSettingsEditor'; diff --git a/packages/scene-composer/src/components/three-fiber/EditorTransformControls.tsx b/packages/scene-composer/src/components/three-fiber/EditorTransformControls.tsx index f3f71df4e..9b3ef03b3 100644 --- a/packages/scene-composer/src/components/three-fiber/EditorTransformControls.tsx +++ b/packages/scene-composer/src/components/three-fiber/EditorTransformControls.tsx @@ -26,7 +26,6 @@ export function EditorTransformControls() { const addingWidget = useStore(sceneComposerId)((state) => state.addingWidget); const [transformControls] = useState(() => new TransformControlsImpl(camera, domElement)); - const tagResizeEnabled = getGlobalSettings().featureConfig[COMPOSER_FEATURES.TagResize]; const subModelMovementEnabled = getGlobalSettings().featureConfig[COMPOSER_FEATURES.SubModelMovement]; const isTagComponent = useMemo( @@ -59,7 +58,7 @@ export function EditorTransformControls() { useEffect(() => { if (selectedSceneNode) { - if (isTagComponent && !tagResizeEnabled && transformControlMode === 'scale') { + if (isTagComponent && transformControlMode === 'scale') { // Prevent the scale from being enabled setTransformControlsMode('translate'); } diff --git a/packages/scene-composer/src/components/toolbars/floatingToolbar/items/ObjectItemGroup.tsx b/packages/scene-composer/src/components/toolbars/floatingToolbar/items/ObjectItemGroup.tsx index 8c11cf966..96a7985e9 100644 --- a/packages/scene-composer/src/components/toolbars/floatingToolbar/items/ObjectItemGroup.tsx +++ b/packages/scene-composer/src/components/toolbars/floatingToolbar/items/ObjectItemGroup.tsx @@ -19,7 +19,7 @@ enum TransformTypes { const labelStrings = defineMessages({ [TransformTypes.Translate]: { defaultMessage: 'Translate', description: 'Menu label' }, [TransformTypes.Rotate]: { defaultMessage: 'Rotate', description: 'Menu label' }, - [TransformTypes.Scale]: { defaultMessage: 'Translate', description: 'Menu label' }, + [TransformTypes.Scale]: { defaultMessage: 'Scale', description: 'Menu label' }, }); const textStrings = defineMessages({ @@ -35,8 +35,6 @@ export function ObjectItemGroup() { const { getSceneNodeByRef, removeSceneNode } = useSceneDocument(sceneComposerId); const { formatMessage } = useIntl(); - const tagResizeEnabled = getGlobalSettings().featureConfig[COMPOSER_FEATURES.TagResize]; - const isTagComponent = useMemo(() => { const selectedSceneNode = getSceneNodeByRef(selectedSceneNodeRef); return selectedSceneNode?.components.some((component) => component.type === KnownComponentType.Tag) === true; @@ -57,7 +55,7 @@ export function ObjectItemGroup() { icon: { scale: 1.06, svg: ScaleIconSvg }, uuid: TransformTypes.Scale, mode: 'scale', - isDisabled: isTagComponent && !tagResizeEnabled, + isDisabled: isTagComponent, }, ].map( (item) => diff --git a/packages/scene-composer/src/interfaces/componentSettings.ts b/packages/scene-composer/src/interfaces/componentSettings.ts new file mode 100644 index 000000000..2c41ac5ac --- /dev/null +++ b/packages/scene-composer/src/interfaces/componentSettings.ts @@ -0,0 +1,9 @@ +import { KnownComponentType } from './components'; + +export interface ITagSettings { + scale: number; + autoRescale: boolean; +} + +export type IComponentSettings = ITagSettings | any; +export type IComponentSettingsMap = Record; diff --git a/packages/scene-composer/src/interfaces/index.ts b/packages/scene-composer/src/interfaces/index.ts index cce7036d4..fcad1a7a2 100644 --- a/packages/scene-composer/src/interfaces/index.ts +++ b/packages/scene-composer/src/interfaces/index.ts @@ -6,3 +6,4 @@ export * from './metricRecorder'; export * from './feature'; export * from './sceneComposerInternal'; export * from './sceneViewer'; +export * from './componentSettings'; diff --git a/packages/scene-composer/src/interfaces/interfaces.tsx b/packages/scene-composer/src/interfaces/interfaces.tsx index 1f1061291..d67d2569c 100644 --- a/packages/scene-composer/src/interfaces/interfaces.tsx +++ b/packages/scene-composer/src/interfaces/interfaces.tsx @@ -68,6 +68,7 @@ export enum KnownSceneProperty { BaseUrl = 'baseUrl', EnvironmentPreset = 'environmentPreset', DataBindingConfig = 'dataBindingConfig', + ComponentSettings = 'componentSettings', } /************************************************ diff --git a/packages/scene-composer/src/utils/componentSettingsUtils.spec.ts b/packages/scene-composer/src/utils/componentSettingsUtils.spec.ts new file mode 100644 index 000000000..79a60a9e0 --- /dev/null +++ b/packages/scene-composer/src/utils/componentSettingsUtils.spec.ts @@ -0,0 +1,27 @@ +import { DEFAULT_TAG_GLOBAL_SETTINGS } from '../common/constants'; +import { KnownComponentType } from '../interfaces'; + +import { componentSettingsSelector } from './componentSettingsUtils'; + +describe('componentSettingsUtils', () => { + describe('componentSettingsSelector', () => { + it('should return default tag settings', () => { + const state = { getSceneProperty: jest.fn() } as any; + const tagSettings = componentSettingsSelector(state, KnownComponentType.Tag); + expect(tagSettings).toEqual(DEFAULT_TAG_GLOBAL_SETTINGS); + }); + + it('should return tag settings from scene', () => { + const expected = { scale: 1.6, autoRescale: true }; + const state = { getSceneProperty: jest.fn().mockReturnValue({ Tag: expected }) } as any; + const tagSettings = componentSettingsSelector(state, KnownComponentType.Tag); + expect(tagSettings).toEqual(expected); + }); + + it('should return empty settings for unknown component type', () => { + const state = { getSceneProperty: jest.fn() } as any; + const settings = componentSettingsSelector(state, 'Random' as any); + expect(settings).toEqual({}); + }); + }); +}); diff --git a/packages/scene-composer/src/utils/componentSettingsUtils.ts b/packages/scene-composer/src/utils/componentSettingsUtils.ts new file mode 100644 index 000000000..cedaa39f5 --- /dev/null +++ b/packages/scene-composer/src/utils/componentSettingsUtils.ts @@ -0,0 +1,18 @@ +import { RootState } from '../store'; +import { KnownSceneProperty, IComponentSettings, KnownComponentType } from '../interfaces'; +import { DEFAULT_TAG_GLOBAL_SETTINGS } from '../common/constants'; + +export const componentSettingsSelector = (state: RootState, componentType: KnownComponentType): IComponentSettings => { + const settings = state.getSceneProperty(KnownSceneProperty.ComponentSettings)?.[componentType]; + if (settings) { + return settings; + } else { + // When the settings is not in scene file, return the default setting for each component type. + switch (componentType) { + case KnownComponentType.Tag: + return DEFAULT_TAG_GLOBAL_SETTINGS; + default: + return {}; + } + } +}; diff --git a/packages/scene-composer/stories/SceneComposer.stories.tsx b/packages/scene-composer/stories/SceneComposer.stories.tsx index e142d3f55..6d0ec7f14 100644 --- a/packages/scene-composer/stories/SceneComposer.stories.tsx +++ b/packages/scene-composer/stories/SceneComposer.stories.tsx @@ -151,7 +151,7 @@ const knobsConfigurationDecorator = [ [COMPOSER_FEATURES.ENHANCED_EDITING]: true, [COMPOSER_FEATURES.CameraView]: true, [COMPOSER_FEATURES.EnvironmentModel]: false, - [COMPOSER_FEATURES.TagResize]: false, + [COMPOSER_FEATURES.TagResize]: true, [COMPOSER_FEATURES.SubModelMovement]: false, ...args.config.featureConfig, }, diff --git a/packages/scene-composer/tests/augmentations/components/three-fiber/common/SvgIconToWidgetSprite.spec.tsx b/packages/scene-composer/tests/augmentations/components/three-fiber/common/SvgIconToWidgetSprite.spec.tsx deleted file mode 100644 index 633e0d200..000000000 --- a/packages/scene-composer/tests/augmentations/components/three-fiber/common/SvgIconToWidgetSprite.spec.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import renderer from 'react-test-renderer'; -import React from 'react'; - -import { DefaultAnchorStatus, SelectedAnchor } from '../../../../../src'; -import { - ErrorIconSvgString, - InfoIconSvgString, - SelectedIconSvgString, - VideoIconSvgString, - WarningIconSvgString, -} from '../../../../../src/assets'; -import svgIconToWidgetSprite from '../../../../../src/augmentations/components/three-fiber/common/SvgIconToWidgetSprite'; - -describe('svgIconToWidgetSprite', () => { - [ - ['Selected', { key: SelectedAnchor, icon: SelectedIconSvgString }, false], - ['Info', { key: DefaultAnchorStatus.Info, icon: InfoIconSvgString }, false], - ['Warning', { key: DefaultAnchorStatus.Warning, icon: WarningIconSvgString }, false], - ['Error', { key: DefaultAnchorStatus.Error, icon: ErrorIconSvgString }, false], - ['Video', { key: DefaultAnchorStatus.Video, icon: VideoIconSvgString }, false], - ['Selected', { key: SelectedAnchor, icon: SelectedIconSvgString }, true], - ['Info', { key: DefaultAnchorStatus.Info, icon: InfoIconSvgString }, true], - ['Warning', { key: DefaultAnchorStatus.Warning, icon: WarningIconSvgString }, true], - ['Error', { key: DefaultAnchorStatus.Error, icon: ErrorIconSvgString }, true], - ['Video', { key: DefaultAnchorStatus.Video, icon: VideoIconSvgString }, true], - ].forEach((value) => { - it(`it should render the ${value[2] ? 'always visible' : ''} ${value[0]} correctly`, () => { - jest.spyOn(window.Math, 'random').mockReturnValue(0.1); - const { key, icon } = value[1] as any; - const container = renderer.create(svgIconToWidgetSprite(icon, key, value[2] as boolean)); - - expect(container).toMatchSnapshot(); - }); - }); -}); diff --git a/packages/scene-composer/tests/augmentations/components/three-fiber/common/__snapshots__/SvgIconToWidgetSprite.spec.tsx.snap b/packages/scene-composer/tests/augmentations/components/three-fiber/common/__snapshots__/SvgIconToWidgetSprite.spec.tsx.snap deleted file mode 100644 index b82386261..000000000 --- a/packages/scene-composer/tests/augmentations/components/three-fiber/common/__snapshots__/SvgIconToWidgetSprite.spec.tsx.snap +++ /dev/null @@ -1,1046 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`svgIconToWidgetSprite it should render the Error correctly 1`] = ` - - - -`; - -exports[`svgIconToWidgetSprite it should render the Info correctly 1`] = ` - - - -`; - -exports[`svgIconToWidgetSprite it should render the Selected correctly 1`] = ` - - - -`; - -exports[`svgIconToWidgetSprite it should render the Video correctly 1`] = ` - - - -`; - -exports[`svgIconToWidgetSprite it should render the Warning correctly 1`] = ` - - - -`; - -exports[`svgIconToWidgetSprite it should render the always visible Error correctly 1`] = ` - - - -`; - -exports[`svgIconToWidgetSprite it should render the always visible Info correctly 1`] = ` - - - -`; - -exports[`svgIconToWidgetSprite it should render the always visible Selected correctly 1`] = ` - - - -`; - -exports[`svgIconToWidgetSprite it should render the always visible Video correctly 1`] = ` - - - -`; - -exports[`svgIconToWidgetSprite it should render the always visible Warning correctly 1`] = ` - - - -`; diff --git a/packages/scene-composer/tests/components/panels/scene-components/motion-indicator/SpeedEditor.spec.tsx b/packages/scene-composer/tests/components/panels/scene-components/motion-indicator/SpeedEditor.spec.tsx index 624377037..917399df9 100644 --- a/packages/scene-composer/tests/components/panels/scene-components/motion-indicator/SpeedEditor.spec.tsx +++ b/packages/scene-composer/tests/components/panels/scene-components/motion-indicator/SpeedEditor.spec.tsx @@ -19,10 +19,8 @@ jest.mock('../../../../../src/components/panels/scene-components/motion-indicato }); let sliderOnChangeCb; -jest.mock('../../../../../src/components/panels/scene-components/motion-indicator/Slider', () => { - const originalModule = jest.requireActual( - '../../../../../src/components/panels/scene-components/motion-indicator/Slider', - ); +jest.mock('../../../../../src/components/panels/Slider', () => { + const originalModule = jest.requireActual('../../../../../src/components/panels/Slider'); return { ...originalModule, Slider: (...props: any[]) => { diff --git a/packages/scene-composer/tests/components/panels/scene-components/motion-indicator/SpeedEditorSnap.spec.tsx b/packages/scene-composer/tests/components/panels/scene-components/motion-indicator/SpeedEditorSnap.spec.tsx index da70cd626..10f81378d 100644 --- a/packages/scene-composer/tests/components/panels/scene-components/motion-indicator/SpeedEditorSnap.spec.tsx +++ b/packages/scene-composer/tests/components/panels/scene-components/motion-indicator/SpeedEditorSnap.spec.tsx @@ -17,10 +17,8 @@ jest.mock('../../../../../src/components/panels/scene-components/motion-indicato }; }); -jest.mock('../../../../../src/components/panels/scene-components/motion-indicator/Slider', () => { - const originalModule = jest.requireActual( - '../../../../../src/components/panels/scene-components/motion-indicator/Slider', - ); +jest.mock('../../../../../src/components/panels/Slider', () => { + const originalModule = jest.requireActual('../../../../../src/components/panels/Slider'); return { ...originalModule, Slider: (...props: any[]) =>
{JSON.stringify(props)}
, diff --git a/packages/scene-composer/tests/scenes/scene_2.json b/packages/scene-composer/tests/scenes/scene_2.json index b7a179f71..a2d063691 100644 --- a/packages/scene-composer/tests/scenes/scene_2.json +++ b/packages/scene-composer/tests/scenes/scene_2.json @@ -225,6 +225,12 @@ }, "defaultCameraIndex": 0, "properties": { - "environmentPreset": "neutral" + "environmentPreset": "neutral", + "componentSettings": { + "Tag": { + "scale": 2, + "autoRescale": true + } + } } } diff --git a/packages/scene-composer/translations/IotAppKitSceneComposer.en_US.json b/packages/scene-composer/translations/IotAppKitSceneComposer.en_US.json index eab4b174d..2806794c4 100644 --- a/packages/scene-composer/translations/IotAppKitSceneComposer.en_US.json +++ b/packages/scene-composer/translations/IotAppKitSceneComposer.en_US.json @@ -27,6 +27,10 @@ "note": "Menu Item", "text": "Rotate object" }, + "0awbFz": { + "note": "ExpandableInfoSection Title", + "text": "Tag Settings" + }, "1cVQu5": { "note": "label for an input component selecting number of arrows", "text": "# of arrows" @@ -259,6 +263,10 @@ "note": "Placeholder", "text": "Choose a speed value" }, + "JvqIHJ": { + "note": "checkbox label", + "text": "Auto rescale" + }, "Kchdc4": { "note": "Placeholder", "text": "Choose a rule" @@ -303,6 +311,10 @@ "note": "label", "text": "Add color rule" }, + "LztKh8": { + "note": "Menu label", + "text": "Scale" + }, "MZtQw3": { "note": "75mm lens", "text": "75mm" @@ -679,6 +691,10 @@ "note": "Form field label", "text": "Zoom" }, + "qU9dRt": { + "note": "Form Field label", + "text": "Scale" + }, "r0JN3A": { "note": "Menu Item", "text": "Orbit"