Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions packages/editor/src/components/BPHierarchyComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ import {EditModePlugin} from "../utils/EditModePlugin.ts";

interface BPHierarchyComponentPropsExtras extends HandleContextMenuCallback<IObject3D>{
/**
* A double click on an asset row opens that asset's own document. The wrapper resolves the path
* and answers true when it opened one, because then focusing the camera would be wasted work.
* The document path a row's object opens as, or null for a row with no file of its own. A row
* that answers a path gets "Open in New Tab" on its menu.
*/
onOpenAsset?: (object: IObject3D) => boolean
documentPath?: (object: IObject3D) => string | null
}

const referenceKindIcons: Record<Exclude<ObjectReference['kind'], 'material'>, IconName> = {
Expand Down Expand Up @@ -236,15 +236,12 @@ export class BPHierarchyComponent<T extends IObject3D = IObject3D> extends BPTre
if(!node) return
const obj = node.nodeData!
const isScene = obj === this.context.viewer.scene as any
// An asset row opens its own document, the way the Inspector's Edit Asset button does.
// Focusing the camera on a row whose document replaces the viewport would be wasted.
const opened = !isScene && (this.props.onOpenAsset?.(obj) ?? false)
obj.dispatchEvent({
type: 'select',
value: obj,
object: obj,
ui: true,
focusCamera: !isScene && !opened,
focusCamera: !isScene,
bubbleToParent: true,
})
if(isScene){
Expand All @@ -265,6 +262,17 @@ export class BPHierarchyComponent<T extends IObject3D = IObject3D> extends BPTre
if(!node) return
const obj = node.nodeData!

// A row that came from a file opens that file as a tab, the way the Files panel does.
const path = this.props.documentPath?.(obj)
if (path) {
items.push({
props: {text: 'Open in New Tab'},
key: 'openInNewTab',
action: 'openDocument',
data: {path},
})
}

const editMode = this.context.viewer.getPlugin(EditModePlugin)
const selected = this.context.viewer.getPlugin(PickingPlugin)?.getSelectedObjects<IObject3D>() ?? []
// Isolating a node that is not in the selection isolates that node alone.
Expand Down
45 changes: 25 additions & 20 deletions packages/editor/src/components/FilesPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ const menuItemsFiles: MenuItem2[] = [{
props: {text: 'Reveal in System File Explorer', icon: 'folder-shared'},
}*/]

/**
* The item a file the viewport has a view for gets, above the rest. It stands outside the list
* above because no extension tag names its files: the document kinds are a list of their own.
*/
const menuItemOpenInNewTab: MenuItem2 = {
action: 'openInNewTab',
key: 'openInNewTab',
props: {text: 'Open in New Tab', icon: 'open-application'},
}

export function FilesPanelGrid({}: {
}){
const {selectedFiles, setSelectedFiles, currentPath, setCurrentPath, fileManifest, refreshManifest }= useAssets()
Expand All @@ -149,6 +159,16 @@ export function FilesPanelGrid({}: {
if (res && 'error' in res) showSuccessErrorToast('', 'Unable to open file', res as ErrorRes)
}

/** What a double click and the Open in New Tab item both do: walk into a folder, or open a file. */
const openFile = (f: FileManifestEntry) => {
if (f.type === 'directory') {
setCurrentPath(f.path)
setSelectedFiles([])
} else if (documentKind(f.path)) {
updateLoading(f.path, loadFile(f.path))
}
}

let items = fileManifest
const path = currentPath.replace(/^\//, '')
let dirHandle = !path ? project?.handle : undefined
Expand Down Expand Up @@ -244,6 +264,7 @@ export function FilesPanelGrid({}: {
})
}
const actions: Record<string, MenuItemAction> = {
openInNewTab: (data: { file: FileManifestEntry })=> openFile(data.file),
createEmptyScene: async ()=>{
if(!dirHandle || !project?.handle) return
await whileExistsPrompt({
Expand Down Expand Up @@ -487,21 +508,6 @@ export class MyComponent extends Object3DComponent {
useEffect(() => {
selectedFilesRef.current = selectedFiles
}, [selectedFiles])
const openFile = (f: FileManifestEntry, newTab: boolean) => {
if (f.type === 'directory') {
setCurrentPath(f.path)
setSelectedFiles([])
} else {
if (documentKind(f.path)) {
if (newTab) {
// todo
// window.open(window.location.pathname + '?project=' + encodeURIComponent(f.path) + (project?.path ? '&base=' + encodeURIComponent(project.path) : ''), '_blank')
return
}
updateLoading(f.path, loadFile(f.path))
}
}
}
const selectFiles = async (files: FileManifestEntry[], e: React.MouseEvent|React.KeyboardEvent)=>{
selectedFilesRef.current = files
setSelectedFiles(files)
Expand Down Expand Up @@ -562,9 +568,7 @@ export class MyComponent extends Object3DComponent {
if(e.key === 'Enter' && selectedFiles.length === 1) {
e.preventDefault()
e.stopPropagation()
const f = selectedFiles[0]
const newTab = e.metaKey || e.ctrlKey
openFile(f, newTab);
openFile(selectedFiles[0]);
}
if(e.key === 'ArrowRight' || e.key === 'ArrowLeft'){
e.preventDefault()
Expand Down Expand Up @@ -610,13 +614,14 @@ export class MyComponent extends Object3DComponent {
return false
}))

if(f.type !== 'directory' && documentKind(f.path)) menuItemsFiles2 = [menuItemOpenInNewTab, ...menuItemsFiles2]

handleContextMenu(e, menuItemsFiles2.map(i=>({...i, data: {file: f}})), f)
}}
onDoubleClick={(e)=>{
e.preventDefault()
e.stopPropagation()
const newTab = e.metaKey || e.ctrlKey
openFile(f, newTab);
openFile(f);
}}
onClick={(e)=>{
// if(f.type === 'directory') setCurrentPath(f.path)
Expand Down
23 changes: 12 additions & 11 deletions packages/editor/src/components/ObjectHierarchyComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,13 @@ export function ExtraMenuItems(props: {
export function ObjectHierarchyComponent({className}: { className: string }) {
const manager = useManager()
const viewer = manager.get()
const {store} = useDocuments()

const {makeAsset} = useMakeAsset()
const actions = {makeAsset,
openDocument: (data: { path: string }) => {
store.open(data.path).catch(e => showErrorToast(`Unable to open ${data.path}`, e))
},
moveInParent: (data: { obj: IObject3D, delta: number }) => {
const parent = data.obj.parent
if (!parent) return
Expand All @@ -58,20 +62,17 @@ export function ObjectHierarchyComponent({className}: { className: string }) {
// })

const contextMenu = useContextMenu()
const {store} = useDocuments()

/**
* A placed asset names the file it came from in userData.rootPath. A double click on that row
* opens the file as a document, the same call the Inspector's Edit Asset button makes. A clone
* of an asset child carries _tpRootPath instead, and that one stays a plain row.
* A placed asset names the file it came from in userData.rootPath, and that file is what its row
* opens as a tab. An object inside an asset has no rootPath of its own, so it answers nothing.
*/
const onOpenAsset = (obj: IObject3D) => {
const rootPath = (obj as {_tpRootPath?: string})._tpRootPath ? null : obj.userData?.rootPath
if (!rootPath || !rootPath.startsWith(assetUrlPrefix)) return false
const documentPath = (obj: IObject3D) => {
const rootPath = obj.userData?.rootPath
if (!rootPath || !rootPath.startsWith(assetUrlPrefix)) return null
const path = manager.resolveAssetIdPath(rootPath)
if (!path || path.startsWith(assetUrlPrefix) || !documentKind(path)) return false
store.open(path).catch(e => showErrorToast(`Unable to open ${path}`, e))
return true
if (!path || path.startsWith(assetUrlPrefix) || !documentKind(path)) return null
return path
}

const config: UiObjectConfig = useMemo(() => ({
Expand All @@ -90,7 +91,7 @@ export function ObjectHierarchyComponent({className}: { className: string }) {
<BPHierarchyComponent
config={config}
key={viewer.scene.modelRoot.uuid} // this is required because viewer can be destroyed and recreated
onOpenAsset={onOpenAsset}
documentPath={documentPath}
handleContextMenu={(e, items, obj) => {
contextMenu.handleContextMenu({
event: e,
Expand Down
Loading