Merge branch 'main' of github.com:sudhir9297/editor into feat/wall-room-creation
This commit is contained in:
@@ -147,6 +147,7 @@ export function FloatingActionMenu() {
|
|||||||
duplicate.start = [duplicate.start[0] + 1, duplicate.start[1] + 1]
|
duplicate.start = [duplicate.start[0] + 1, duplicate.start[1] + 1]
|
||||||
duplicate.end = [duplicate.end[0] + 1, duplicate.end[1] + 1]
|
duplicate.end = [duplicate.end[0] + 1, duplicate.end[1] + 1]
|
||||||
} else if (node.type === 'roof') {
|
} else if (node.type === 'roof') {
|
||||||
|
duplicateInfo.children = []
|
||||||
duplicate = RoofNode.parse(duplicateInfo)
|
duplicate = RoofNode.parse(duplicateInfo)
|
||||||
} else if (node.type === 'roof-segment') {
|
} else if (node.type === 'roof-segment') {
|
||||||
duplicate = RoofSegmentNode.parse(duplicateInfo)
|
duplicate = RoofSegmentNode.parse(duplicateInfo)
|
||||||
@@ -160,6 +161,12 @@ export function FloatingActionMenu() {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to parse duplicate', error)
|
console.error('Failed to parse duplicate', error)
|
||||||
|
useScene.temporal.getState().resume()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!duplicate) {
|
||||||
|
useScene.temporal.getState().resume()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -920,13 +920,13 @@ const EditorOutlinerSync = () => {
|
|||||||
outliner.selectedObjects.length = 0
|
outliner.selectedObjects.length = 0
|
||||||
for (const id of idsToHighlight) {
|
for (const id of idsToHighlight) {
|
||||||
const obj = sceneRegistry.nodes.get(id)
|
const obj = sceneRegistry.nodes.get(id)
|
||||||
if (obj) outliner.selectedObjects.push(obj)
|
if (obj?.parent) outliner.selectedObjects.push(obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
outliner.hoveredObjects.length = 0
|
outliner.hoveredObjects.length = 0
|
||||||
if (hoveredId) {
|
if (hoveredId) {
|
||||||
const obj = sceneRegistry.nodes.get(hoveredId)
|
const obj = sceneRegistry.nodes.get(hoveredId)
|
||||||
if (obj) outliner.hoveredObjects.push(obj)
|
if (obj?.parent) outliner.hoveredObjects.push(obj)
|
||||||
}
|
}
|
||||||
}, [phase, previewSelectedIds, selection, hoveredId, outliner])
|
}, [phase, previewSelectedIds, selection, hoveredId, outliner])
|
||||||
|
|
||||||
|
|||||||
@@ -1578,3 +1578,8 @@ export const CATALOG_ITEMS: AssetInput[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export function getDefaultCatalogItem(category: string | null | undefined): AssetInput | null {
|
||||||
|
if (!category) return null
|
||||||
|
return CATALOG_ITEMS.find((item) => item.category === category) ?? null
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import { persist } from 'zustand/middleware'
|
import { persist } from 'zustand/middleware'
|
||||||
|
import { getDefaultCatalogItem } from '../components/ui/item-catalog/catalog-items'
|
||||||
|
|
||||||
const DEFAULT_ACTIVE_SIDEBAR_PANEL = 'site'
|
const DEFAULT_ACTIVE_SIDEBAR_PANEL = 'site'
|
||||||
const DEFAULT_FLOORPLAN_PANE_RATIO = 0.5
|
const DEFAULT_FLOORPLAN_PANE_RATIO = 0.5
|
||||||
@@ -344,6 +345,10 @@ export function selectDefaultBuildingAndLevel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDefaultSelectedItemForCategory(category: CatalogCategory | null): AssetInput | null {
|
||||||
|
return getDefaultCatalogItem(category)
|
||||||
|
}
|
||||||
|
|
||||||
const useEditor = create<EditorState>()(
|
const useEditor = create<EditorState>()(
|
||||||
persist(
|
persist(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
@@ -365,7 +370,11 @@ const useEditor = create<EditorState>()(
|
|||||||
} else if (phase === 'structure') {
|
} else if (phase === 'structure') {
|
||||||
set({ tool: 'wall', catalogCategory: null })
|
set({ tool: 'wall', catalogCategory: null })
|
||||||
} else if (phase === 'furnish') {
|
} else if (phase === 'furnish') {
|
||||||
set({ tool: 'item', catalogCategory: 'furniture' })
|
set({
|
||||||
|
tool: 'item',
|
||||||
|
catalogCategory: 'furniture',
|
||||||
|
selectedItem: getDefaultSelectedItemForCategory('furniture'),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Reset to select mode and clear tool/catalog when switching phases
|
// Reset to select mode and clear tool/catalog when switching phases
|
||||||
@@ -405,8 +414,15 @@ const useEditor = create<EditorState>()(
|
|||||||
} else if (phase === 'structure' && structureLayer === 'elements') {
|
} else if (phase === 'structure' && structureLayer === 'elements') {
|
||||||
set({ tool: 'wall' })
|
set({ tool: 'wall' })
|
||||||
} else if (phase === 'furnish') {
|
} else if (phase === 'furnish') {
|
||||||
set({ tool: 'item', catalogCategory: 'furniture' })
|
set({
|
||||||
|
tool: 'item',
|
||||||
|
catalogCategory: 'furniture',
|
||||||
|
selectedItem: getDefaultSelectedItemForCategory('furniture'),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
} else if (phase === 'furnish' && tool === 'item' && !get().selectedItem) {
|
||||||
|
const category = get().catalogCategory ?? 'furniture'
|
||||||
|
set({ selectedItem: getDefaultSelectedItemForCategory(category) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// When leaving build mode, clear tool
|
// When leaving build mode, clear tool
|
||||||
@@ -434,7 +450,17 @@ const useEditor = create<EditorState>()(
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
catalogCategory: DEFAULT_PERSISTED_EDITOR_UI_STATE.catalogCategory,
|
catalogCategory: DEFAULT_PERSISTED_EDITOR_UI_STATE.catalogCategory,
|
||||||
setCatalogCategory: (category) => set({ catalogCategory: category }),
|
setCatalogCategory: (category) =>
|
||||||
|
set((state) => ({
|
||||||
|
catalogCategory: category,
|
||||||
|
selectedItem:
|
||||||
|
category !== null &&
|
||||||
|
state.phase === 'furnish' &&
|
||||||
|
state.mode === 'build' &&
|
||||||
|
state.tool === 'item'
|
||||||
|
? getDefaultSelectedItemForCategory(category)
|
||||||
|
: state.selectedItem,
|
||||||
|
})),
|
||||||
selectedItem: null,
|
selectedItem: null,
|
||||||
setSelectedItem: (item) => set({ selectedItem: item }),
|
setSelectedItem: (item) => set({ selectedItem: item }),
|
||||||
movingNode: null as
|
movingNode: null as
|
||||||
@@ -517,11 +543,23 @@ const useEditor = create<EditorState>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'pascal-editor-ui-preferences',
|
name: 'pascal-editor-ui-preferences',
|
||||||
merge: (persistedState, currentState) => ({
|
merge: (persistedState, currentState) => {
|
||||||
|
const mergedState = {
|
||||||
...currentState,
|
...currentState,
|
||||||
...normalizePersistedEditorUiState(persistedState as Partial<PersistedEditorState>),
|
...normalizePersistedEditorUiState(persistedState as Partial<PersistedEditorState>),
|
||||||
...normalizePersistedEditorLayoutState(persistedState as Partial<PersistedEditorState>),
|
...normalizePersistedEditorLayoutState(persistedState as Partial<PersistedEditorState>),
|
||||||
}),
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...mergedState,
|
||||||
|
selectedItem:
|
||||||
|
mergedState.phase === 'furnish' &&
|
||||||
|
mergedState.mode === 'build' &&
|
||||||
|
mergedState.tool === 'item'
|
||||||
|
? getDefaultSelectedItemForCategory(mergedState.catalogCategory ?? 'furniture')
|
||||||
|
: currentState.selectedItem,
|
||||||
|
}
|
||||||
|
},
|
||||||
partialize: (state) => ({
|
partialize: (state) => ({
|
||||||
phase: state.phase,
|
phase: state.phase,
|
||||||
mode: state.mode,
|
mode: state.mode,
|
||||||
|
|||||||
@@ -110,6 +110,12 @@ const Viewer: React.FC<ViewerProps> = ({
|
|||||||
const renderer = new THREE.WebGPURenderer(props as any)
|
const renderer = new THREE.WebGPURenderer(props as any)
|
||||||
renderer.toneMapping = THREE.ACESFilmicToneMapping
|
renderer.toneMapping = THREE.ACESFilmicToneMapping
|
||||||
renderer.toneMappingExposure = 0.9
|
renderer.toneMappingExposure = 0.9
|
||||||
|
// Awaiting init() is required when the browser falls back to the
|
||||||
|
// WebGL2 backend (Safari without the WebGPU flag, older Chrome on
|
||||||
|
// machines without a WebGPU device). In native WebGPU mode the
|
||||||
|
// init resolves almost instantly. Without this await, the first
|
||||||
|
// render throws "Renderer: .render() called before the backend is
|
||||||
|
// initialized" from the post-processing fallback path.
|
||||||
await renderer.init()
|
await renderer.init()
|
||||||
return renderer
|
return renderer
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -117,6 +117,24 @@ const PostProcessingPasses = () => {
|
|||||||
|
|
||||||
hasPipelineErrorRef.current = false
|
hasPipelineErrorRef.current = false
|
||||||
|
|
||||||
|
// WebGPU availability check: SSGI, denoise, and RenderPipeline are all
|
||||||
|
// WebGPU-only APIs. When the browser falls back to WebGL2 (no
|
||||||
|
// `navigator.gpu`, or the device couldn't be created), building the
|
||||||
|
// pipeline either throws silently or produces a broken output where
|
||||||
|
// the scene renders for a few frames and then goes black as the retry
|
||||||
|
// loop fights the direct-render fallback path. Short-circuit here so
|
||||||
|
// `useFrame` uses the direct `renderer.render(scene, camera)` path
|
||||||
|
// exclusively and never attempts the TSL pipeline.
|
||||||
|
const hasWebGPU = typeof navigator !== 'undefined' && typeof navigator.gpu !== 'undefined'
|
||||||
|
if (!hasWebGPU) {
|
||||||
|
console.warn(
|
||||||
|
'[viewer] WebGPU unavailable — rendering without post-processing (SSGI, outlines, denoise).',
|
||||||
|
)
|
||||||
|
hasPipelineErrorRef.current = true
|
||||||
|
renderPipelineRef.current = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Clear outliner arrays synchronously to prevent stale Object3D refs
|
// Clear outliner arrays synchronously to prevent stale Object3D refs
|
||||||
// from the previous project leaking into the new pipeline's outline passes.
|
// from the previous project leaking into the new pipeline's outline passes.
|
||||||
const outliner = useViewer.getState().outliner
|
const outliner = useViewer.getState().outliner
|
||||||
@@ -263,15 +281,7 @@ const PostProcessingPasses = () => {
|
|||||||
}
|
}
|
||||||
renderPipelineRef.current = null
|
renderPipelineRef.current = null
|
||||||
}
|
}
|
||||||
}, [
|
}, [renderer, scene, camera, hoverHighlightMode, zoneLayers, projectId, pipelineVersion])
|
||||||
renderer,
|
|
||||||
scene,
|
|
||||||
camera,
|
|
||||||
hoverHighlightMode,
|
|
||||||
zoneLayers,
|
|
||||||
projectId,
|
|
||||||
pipelineVersion,
|
|
||||||
])
|
|
||||||
|
|
||||||
useFrame((_, delta) => {
|
useFrame((_, delta) => {
|
||||||
// Animate background colour toward the current theme target (same lerp as AnimatedBackground)
|
// Animate background colour toward the current theme target (same lerp as AnimatedBackground)
|
||||||
|
|||||||
@@ -600,9 +600,14 @@ export class MergedOutlineNode extends TempNode {
|
|||||||
|
|
||||||
private _buildCache(objects: Object3D[], cache: Set<Object3D>) {
|
private _buildCache(objects: Object3D[], cache: Set<Object3D>) {
|
||||||
for (const obj of objects) {
|
for (const obj of objects) {
|
||||||
|
if (!obj || !obj.traverse) continue
|
||||||
|
try {
|
||||||
obj.traverse((child: any) => {
|
obj.traverse((child: any) => {
|
||||||
if (child.isMesh || child.isSprite) cache.add(child)
|
if (child.isMesh || child.isSprite) cache.add(child)
|
||||||
})
|
})
|
||||||
|
} catch {
|
||||||
|
// Skip objects that were disposed or removed from the scene graph
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user