Skip to content

Commit

Permalink
feat: gizmos alignment local/world (#572)
Browse files Browse the repository at this point in the history
* refactor: rename GizmoType enum to match babylon naming

* feat: added ability to toggle gizmo alignment to world

* chore: fix test

* chore: remove console.logs

* chore: added tests for useSdk and useGizmoAlignment

* fix: build

* chore: fix test

* feat: change rotation gizmo alignment to world while the mesh is not scaled proportionally

* fix: use the right engine, store if rotation gizmo is disabled in state

* chore: fix tests
  • Loading branch information
cazala authored May 11, 2023
1 parent 330e3c6 commit 65d68ce
Show file tree
Hide file tree
Showing 23 changed files with 446 additions and 72 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ test:
make test-inspector

test-inspector:
cd ./packages/@dcl/inspector/; ./../../../node_modules/.bin/jest --coverage --detectOpenHandles --colors --config ./jest.config.js $(FILES)
cd ./packages/@dcl/inspector/; TS_JEST_TRANSFORMER=true ./../../../node_modules/.bin/jest --coverage --detectOpenHandles --colors --config ./jest.config.js $(FILES)

test-cli:
@rm -rf tmp
Expand Down
7 changes: 1 addition & 6 deletions packages/@dcl/inspector/src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@ import { Props } from './types'

import './Modal.css'

const Modal = ({
children,
className = '',
overlayClassName = '',
...props
}: Props) => {
const Modal = ({ children, className = '', overlayClassName = '', ...props }: Props) => {
return (
<_Modal
ariaHideApp={false}
Expand Down
21 changes: 17 additions & 4 deletions packages/@dcl/inspector/src/components/Toolbar/Gizmos/Gizmos.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@
height: 100%;
}

.Gizmos .gizmo.translate {
.Gizmos .gizmo.position {
margin-right: 1px;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
background-image: url(./icons/translate.svg);
background-image: url(./icons/position.svg);
}

.Gizmos .gizmo.rotate {
.Gizmos .gizmo.rotation {
margin: 0px;
border-radius: 0px;
background-image: url(./icons/rotate.svg);
background-image: url(./icons/rotation.svg);
}

.Gizmos .gizmo.scale {
Expand Down Expand Up @@ -68,4 +68,17 @@
position: absolute;
right: -5px;
top: -5px;
}

.Gizmos .panel .snaps {
margin-bottom: 10px;
}

.Gizmos .panel .alignment {
position: relative;
}

.Gizmos .panel .alignment.disabled .icon {
cursor: not-allowed;
opacity: 0.5;
}
50 changes: 40 additions & 10 deletions packages/@dcl/inspector/src/components/Toolbar/Gizmos/Gizmos.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ToolbarButton } from '../ToolbarButton'
import { Snap } from './Snap'

import './Gizmos.css'
import { useGizmoAlignment } from '../../../hooks/editor/useGizmoAlignment'

export const Gizmos = withSdk(({ sdk }) => {
const [showPanel, setShowPanel] = useState(false)
Expand All @@ -26,42 +27,71 @@ export const Gizmos = withSdk(({ sdk }) => {

const [selection, setSelection] = useComponentValue(entity || ROOT, sdk.components.Selection)

const handleTranslateGizmo = useCallback(() => setSelection({ gizmo: GizmoType.TRANSLATE }), [setSelection])
const handleRotateGizmo = useCallback(() => setSelection({ gizmo: GizmoType.ROTATE }), [setSelection])
const handlePositionGizmo = useCallback(() => setSelection({ gizmo: GizmoType.POSITION }), [setSelection])
const handleRotationGizmo = useCallback(() => setSelection({ gizmo: GizmoType.ROTATION }), [setSelection])
const handleScaleGizmo = useCallback(() => setSelection({ gizmo: GizmoType.SCALE }), [setSelection])

const {
isPositionGizmoWorldAligned,
isRotationGizmoWorldAligned,
setPositionGizmoWorldAligned,
setRotationGizmoWorldAligned,
isRotationGizmoAlignmentDisabled
} = useGizmoAlignment()

const disableGizmos = !entity

const SnapToggleIcon = isEnabled ? BiCheckboxChecked : BiCheckbox
const PositionAlignmentIcon = isPositionGizmoWorldAligned ? BiCheckboxChecked : BiCheckbox
const RotationAlignmentIcon = isRotationGizmoWorldAligned ? BiCheckboxChecked : BiCheckbox

const ref = useOutsideClick(handleClosePanel)

return (
<div className="Gizmos">
<ToolbarButton
className={cx('gizmo translate', { active: selection?.gizmo === GizmoType.TRANSLATE })}
className={cx('gizmo position', { active: selection?.gizmo === GizmoType.POSITION })}
disabled={disableGizmos}
onClick={handleTranslateGizmo}
onClick={handlePositionGizmo}
/>
<ToolbarButton
className={cx('gizmo rotate', { active: selection?.gizmo === GizmoType.ROTATE })}
className={cx('gizmo rotation', { active: selection?.gizmo === GizmoType.ROTATION })}
disabled={disableGizmos}
onClick={handleRotateGizmo}
onClick={handleRotationGizmo}
/>
<ToolbarButton
className={cx('gizmo scale', { active: selection?.gizmo === GizmoType.SCALE })}
disabled={disableGizmos}
onClick={handleScaleGizmo}
/>
<BsCaretDown className="open-panel" onClick={handleTogglePanel} />
<div ref={ref} className={cx('panel', { visible: showPanel })}>
<div ref={ref} className={cx('panel', { visible: true })}>
<div className="title">
<label>Snap</label>
<SnapToggleIcon className="icon" onClick={toggle} />
</div>
<Snap gizmo={GizmoType.TRANSLATE} />
<Snap gizmo={GizmoType.ROTATE} />
<Snap gizmo={GizmoType.SCALE} />
<div className="snaps">
<Snap gizmo={GizmoType.POSITION} />
<Snap gizmo={GizmoType.ROTATION} />
<Snap gizmo={GizmoType.SCALE} />
</div>
<div className="title">
<label>Align to world</label>
</div>
<div className="alignment">
<label>Position</label>
<PositionAlignmentIcon
className="icon"
onClick={() => setPositionGizmoWorldAligned(!isPositionGizmoWorldAligned)}
/>
</div>
<div className={cx('alignment', { disabled: isRotationGizmoAlignmentDisabled })}>
<label>Rotation</label>
<RotationAlignmentIcon
className="icon"
onClick={() => setRotationGizmoWorldAligned(!isRotationGizmoWorldAligned)}
/>
</div>
</div>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ const Snap: React.FC<Props> = ({ gizmo }) => {

let label = ''
switch (gizmo) {
case GizmoType.TRANSLATE:
case GizmoType.POSITION:
label = 'Position'
break
case GizmoType.ROTATE:
case GizmoType.ROTATION:
label = 'Rotation'
break
case GizmoType.SCALE:
Expand Down
5 changes: 2 additions & 3 deletions packages/@dcl/inspector/src/hooks/catalog/useFileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,13 @@ export const fileSystemEvent = mitt<FileSystemEvent>()
export const useFileSystem = (): [AssetCatalogResponse, boolean] => {
const [files, setFiles] = useState<AssetCatalogResponse>({ basePath: '', assets: [] })
const [init, setInit] = useState(false)
useSdk(async ({ dataLayer }) => {
useSdk(({ dataLayer }) => {
async function fetchFiles() {
const assets = await dataLayer.getAssetCatalog({})
setFiles(assets)
}
fileSystemEvent.on('change', fetchFiles)
await fetchFiles()
setInit(true)
void fetchFiles().then(() => setInit(true))
})

return [files, init]
Expand Down
127 changes: 127 additions & 0 deletions packages/@dcl/inspector/src/hooks/editor/useGizmoAlignment.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { SdkContextValue } from '../../lib/sdk/context'
import { useGizmoAlignment } from './useGizmoAlignment'

// gizmoManager mock
import { createGizmoManager } from '../../lib/babylon/decentraland/gizmo-manager'
import mitt from 'mitt'
import { renderHook } from '@testing-library/react'
import { act } from 'react-dom/test-utils'
jest.mock('../../lib/babylon/decentraland/gizmo-manager')
const createGizmoManagerMock = createGizmoManager as jest.MockedFn<typeof createGizmoManager>
const gizmoManagerEvents = mitt()
const mockEntity = 0 as Entity
const gizmoManagerMock = {
getEntity: jest.fn().mockReturnValue({ entityId: mockEntity } as EcsEntity),
isPositionGizmoWorldAligned: jest.fn().mockReturnValue(true),
isRotationGizmoWorldAligned: jest.fn().mockReturnValue(true),
isRotationGizmoAlignmentDisabled: jest.fn().mockReturnValue(false),
setPositionGizmoWorldAligned: jest.fn(),
setRotationGizmoWorldAligned: jest.fn(),
fixRotationGizmoAlignment: jest.fn(),
onChange: jest.fn().mockImplementation((cb) => gizmoManagerEvents.on('*', cb))
}
createGizmoManagerMock.mockReturnValue(gizmoManagerMock as unknown as ReturnType<typeof createGizmoManager>)

// useSdk mock
import { useSdk } from '../sdk/useSdk'
jest.mock('../sdk/useSdk')
const useSdkMock = useSdk as jest.MockedFunction<typeof useSdk>
const sdkMock = {
scene: {},
components: { Transform: { componentId: 'core::Transform' } },
gizmos: gizmoManagerMock
} as unknown as SdkContextValue
useSdkMock.mockImplementation((cb) => {
cb && cb(sdkMock)
return sdkMock
})

// useChange mock
import { useChange } from '../sdk/useChange'
import { CrdtMessageType, Entity } from '@dcl/ecs'
import { EcsEntity } from '../../lib/babylon/decentraland/EcsEntity'
jest.mock('../sdk/useChange')
const engineEvents = mitt()
const useChangeMock = useChange as jest.MockedFunction<typeof useChange>
const mockEvent = {
entity: mockEntity,
component: sdkMock.components.Transform,
operation: CrdtMessageType.PUT_COMPONENT,
value: {}
}
useChangeMock.mockImplementation((cb) => {
cb && engineEvents.on('*', () => cb(mockEvent, sdkMock))
})

describe('useGizmoAlignment', () => {
afterEach(() => {
useSdkMock.mockClear()
createGizmoManagerMock.mockClear()
gizmoManagerMock.isPositionGizmoWorldAligned.mockClear()
gizmoManagerMock.isRotationGizmoWorldAligned.mockClear()
gizmoManagerMock.setPositionGizmoWorldAligned.mockClear()
gizmoManagerMock.setRotationGizmoWorldAligned.mockClear()
gizmoManagerMock.onChange.mockClear()
gizmoManagerEvents.all.clear()
engineEvents.all.clear()
})
describe('When the hook is mounted ', () => {
it('should sync the state with the gizmo manager', () => {
const { result } = renderHook(() => useGizmoAlignment())
const { isPositionGizmoWorldAligned, isRotationGizmoWorldAligned } = result.current
expect(isPositionGizmoWorldAligned).toBe(true)
expect(isRotationGizmoWorldAligned).toBe(true)
expect(gizmoManagerMock.isPositionGizmoWorldAligned).toHaveBeenCalled()
expect(gizmoManagerMock.isRotationGizmoWorldAligned).toHaveBeenCalled()
})
it('should add a listener for the onChange event of the gizmoManager', () => {
renderHook(() => useGizmoAlignment())
expect(gizmoManagerMock.onChange).toHaveBeenCalled()
})
it('should not update the renderer', () => {
renderHook(() => useGizmoAlignment())
expect(gizmoManagerMock.setPositionGizmoWorldAligned).not.toHaveBeenCalled()
expect(gizmoManagerMock.setRotationGizmoWorldAligned).not.toHaveBeenCalled()
})
})
describe('When the hook state is changed ', () => {
it('should update the renderer', () => {
const { result } = renderHook(() => useGizmoAlignment())
const { setPositionGizmoWorldAligned, setRotationGizmoWorldAligned } = result.current
expect(result.current.isPositionGizmoWorldAligned).toBe(true)
expect(result.current.isRotationGizmoWorldAligned).toBe(true)
gizmoManagerMock.isPositionGizmoWorldAligned.mockReturnValue(true)
gizmoManagerMock.isRotationGizmoWorldAligned.mockReturnValue(true)
act(() => {
setPositionGizmoWorldAligned(false)
setRotationGizmoWorldAligned(false)
})
expect(result.current.isPositionGizmoWorldAligned).toBe(false)
expect(result.current.isRotationGizmoWorldAligned).toBe(false)
expect(gizmoManagerMock.setPositionGizmoWorldAligned).toHaveBeenCalledWith(false)
expect(gizmoManagerMock.setRotationGizmoWorldAligned).toHaveBeenCalledWith(false)
})
})
describe('When a change happens in the renderer', () => {
it('should update the hook state', () => {
renderHook(() => useGizmoAlignment())
gizmoManagerMock.isPositionGizmoWorldAligned.mockClear()
gizmoManagerMock.isRotationGizmoWorldAligned.mockClear()
gizmoManagerMock.isRotationGizmoAlignmentDisabled.mockReset()
gizmoManagerMock.isRotationGizmoAlignmentDisabled.mockReturnValue(true)
gizmoManagerEvents.emit('*')
expect(gizmoManagerMock.isPositionGizmoWorldAligned).toHaveBeenCalled()
expect(gizmoManagerMock.isRotationGizmoWorldAligned).toHaveBeenCalled()
expect(gizmoManagerMock.isRotationGizmoAlignmentDisabled).toHaveBeenCalled()
})
})
describe('When a change happens in the engine', () => {
it('should update the renderer', () => {
engineEvents.all.clear()
renderHook(() => useGizmoAlignment())
expect(gizmoManagerMock.fixRotationGizmoAlignment).not.toHaveBeenCalled()
engineEvents.emit('*')
expect(gizmoManagerMock.fixRotationGizmoAlignment).toHaveBeenCalledWith(mockEvent.value)
})
})
})
Loading

0 comments on commit 65d68ce

Please sign in to comment.