From 4f3bda62a9dfc8feebd40f72de5087b19ff40912 Mon Sep 17 00:00:00 2001 From: wolf10drc Date: Mon, 3 Aug 2026 03:00:46 +0400 Subject: [PATCH 1/6] editor: light WebGPU preview and /editor scene redirect Redirect MCP-style /editor/:id URLs to /scene/:id, open scenes with a lighter post-FX path for stable local WebGPU, and clarify the home blank-canvas banner vs the saved scenes list. --- apps/editor/app/page.tsx | 12 ++++-------- apps/editor/app/scenes/page.tsx | 3 ++- apps/editor/components/scene-loader.tsx | 25 +++++++++++++++++++++++++ apps/editor/next.config.ts | 11 +++++++++++ 4 files changed, 42 insertions(+), 9 deletions(-) diff --git a/apps/editor/app/page.tsx b/apps/editor/app/page.tsx index e4ccf7ed3..1e3d0933a 100644 --- a/apps/editor/app/page.tsx +++ b/apps/editor/app/page.tsx @@ -91,16 +91,12 @@ export default function Home() {
{PROJECT_ID === 'local-editor' && (
-
- Local editor — scenes are not saved. - - Open recent scenes - - - · +
+ + Blank canvas — saved scenes are under Scenes (not this page). - Create new + Open saved scenes
diff --git a/apps/editor/app/scenes/page.tsx b/apps/editor/app/scenes/page.tsx index 39b8dbf07..9e35f622f 100644 --- a/apps/editor/app/scenes/page.tsx +++ b/apps/editor/app/scenes/page.tsx @@ -83,7 +83,8 @@ export default async function ScenesPage() {
  • {scene.thumbnailUrl ? ( diff --git a/apps/editor/components/scene-loader.tsx b/apps/editor/components/scene-loader.tsx index 9b3b6797b..1275839cf 100644 --- a/apps/editor/components/scene-loader.tsx +++ b/apps/editor/components/scene-loader.tsx @@ -9,6 +9,7 @@ import { type SceneGraph, type SidebarTab, } from '@pascal-app/editor' +import { useViewer } from '@pascal-app/viewer' import { Hammer, Layers } from 'lucide-react' import Image from 'next/image' import Link from 'next/link' @@ -17,6 +18,9 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { BuildTab } from './build-tab' import { CommunityViewerToolbarLeft, CommunityViewerToolbarRight } from './viewer-toolbar' +/** Lighter preview path: skips post-FX and outline passes (viewer `?disable=` flags). */ +export const LIGHT_PREVIEW_QUERY = 'disable=postFx,outline' + export interface SceneMeta { id: string name: string @@ -100,6 +104,21 @@ export function SceneLoader({ initialScene, meta }: SceneLoaderProps) { const [conflict, setConflict] = useState(false) const [saveError, setSaveError] = useState(null) + // Light preview: solid shading pairs with `?disable=postFx` (viewer post-processing flags). + useEffect(() => { + if (typeof window === 'undefined') return + const params = new URLSearchParams(window.location.search) + const disable = params.get('disable') ?? '' + const light = + disable.split(',').some((p) => p.trim() === 'postFx') || params.get('safe') === '1' + if (!light) return + try { + useViewer.getState().setShading('solid') + } catch { + // Viewer store may not be ready yet on first paint. + } + }, []) + const handleLoad = useCallback(async () => initialScene, [initialScene]) const handleSave = useCallback( @@ -224,6 +243,12 @@ export function SceneLoader({ initialScene, meta }: SceneLoaderProps) {
    )}
    + + Light preview + ` (hosted route). This open-source + // app serves saved scenes at `/scene/` — redirect so links and bookmarks work. + async redirects() { + return [ + { + source: '/editor/:id', + destination: '/scene/:id', + permanent: false, + }, + ] + }, transpilePackages: [ 'three', '@pascal-app/viewer', From 267cc6fe762624bffc5ff6539da7dc2616dceeea Mon Sep 17 00:00:00 2001 From: wolf10drc Date: Mon, 3 Aug 2026 03:29:59 +0400 Subject: [PATCH 2/6] editor: re-apply light preview when search params change SceneLoader stayed mounted on same-route Light preview navigation, so shading never updated. Depend on useSearchParams so solid mode applies. --- apps/editor/components/scene-loader.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/editor/components/scene-loader.tsx b/apps/editor/components/scene-loader.tsx index 1275839cf..c700505fd 100644 --- a/apps/editor/components/scene-loader.tsx +++ b/apps/editor/components/scene-loader.tsx @@ -13,7 +13,7 @@ import { useViewer } from '@pascal-app/viewer' import { Hammer, Layers } from 'lucide-react' import Image from 'next/image' import Link from 'next/link' -import { useRouter } from 'next/navigation' +import { useRouter, useSearchParams } from 'next/navigation' import { useCallback, useEffect, useRef, useState } from 'react' import { BuildTab } from './build-tab' import { CommunityViewerToolbarLeft, CommunityViewerToolbarRight } from './viewer-toolbar' @@ -98,26 +98,26 @@ function sceneGraphSignature(graph: SceneGraphWithCollections): string { export function SceneLoader({ initialScene, meta }: SceneLoaderProps) { const router = useRouter() + const searchParams = useSearchParams() const versionRef = useRef(meta.version) const lastRemoteGraphJsonRef = useRef(null) const suppressRemoteSaveUntilRef = useRef(0) const [conflict, setConflict] = useState(false) const [saveError, setSaveError] = useState(null) - // Light preview: solid shading pairs with `?disable=postFx` (viewer post-processing flags). + // Light preview: re-run when query changes (same scene, client navigation to ?disable=…). + // See packages/mcp/docs/layout-clearance-error-log.md pitfall L5 (editor). useEffect(() => { - if (typeof window === 'undefined') return - const params = new URLSearchParams(window.location.search) - const disable = params.get('disable') ?? '' + const disable = searchParams.get('disable') ?? '' const light = - disable.split(',').some((p) => p.trim() === 'postFx') || params.get('safe') === '1' + disable.split(',').some((p) => p.trim() === 'postFx') || searchParams.get('safe') === '1' if (!light) return try { useViewer.getState().setShading('solid') } catch { // Viewer store may not be ready yet on first paint. } - }, []) + }, [searchParams]) const handleLoad = useCallback(async () => initialScene, [initialScene]) From ca5dfb47589f763215b7f180ff4f07e67e82f402 Mon Sep 17 00:00:00 2001 From: wolf10drc Date: Mon, 3 Aug 2026 15:42:18 +0400 Subject: [PATCH 3/6] editor: restore full shading when light-preview flags clear When ?disable=postFx is removed, set shading back to rendered so the viewer is not stuck on the solid/light path after client navigation. --- apps/editor/components/scene-loader.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/editor/components/scene-loader.tsx b/apps/editor/components/scene-loader.tsx index c700505fd..e46922f59 100644 --- a/apps/editor/components/scene-loader.tsx +++ b/apps/editor/components/scene-loader.tsx @@ -106,14 +106,13 @@ export function SceneLoader({ initialScene, meta }: SceneLoaderProps) { const [saveError, setSaveError] = useState(null) // Light preview: re-run when query changes (same scene, client navigation to ?disable=…). - // See packages/mcp/docs/layout-clearance-error-log.md pitfall L5 (editor). + // When flags are removed, restore default shading so the render pipeline is not left stuck. useEffect(() => { const disable = searchParams.get('disable') ?? '' const light = disable.split(',').some((p) => p.trim() === 'postFx') || searchParams.get('safe') === '1' - if (!light) return try { - useViewer.getState().setShading('solid') + useViewer.getState().setShading(light ? 'solid' : 'rendered') } catch { // Viewer store may not be ready yet on first paint. } From 319a77e7712a5fa34ca85a7e832ff486c326dddf Mon Sep 17 00:00:00 2001 From: wolf10drc Date: Mon, 3 Aug 2026 16:04:35 +0400 Subject: [PATCH 4/6] editor: wire light preview to disablePostFx and force re-apply Pass disablePostFx from SceneLoader into Viewer so post-FX is actually skipped (not only solid shading). Light preview button re-applies solid shading even when the query is already active. --- apps/editor/components/scene-loader.tsx | 41 ++++++++++++++----- .../editor/src/components/editor/index.tsx | 12 ++++++ 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/apps/editor/components/scene-loader.tsx b/apps/editor/components/scene-loader.tsx index e46922f59..b56b6e948 100644 --- a/apps/editor/components/scene-loader.tsx +++ b/apps/editor/components/scene-loader.tsx @@ -96,6 +96,13 @@ function sceneGraphSignature(graph: SceneGraphWithCollections): string { }) } +function isLightPreviewQuery(searchParams: URLSearchParams): boolean { + const disable = searchParams.get('disable') ?? '' + return ( + disable.split(',').some((p) => p.trim() === 'postFx') || searchParams.get('safe') === '1' + ) +} + export function SceneLoader({ initialScene, meta }: SceneLoaderProps) { const router = useRouter() const searchParams = useSearchParams() @@ -104,19 +111,20 @@ export function SceneLoader({ initialScene, meta }: SceneLoaderProps) { const suppressRemoteSaveUntilRef = useRef(0) const [conflict, setConflict] = useState(false) const [saveError, setSaveError] = useState(null) + /** Bumps when Light preview is clicked while already active so effects re-apply. */ + const [lightApplyTick, setLightApplyTick] = useState(0) - // Light preview: re-run when query changes (same scene, client navigation to ?disable=…). - // When flags are removed, restore default shading so the render pipeline is not left stuck. + const lightPreview = isLightPreviewQuery(searchParams) + + // Light preview: host prop disablePostFx rebuilds the pipeline; solid shading pairs with it. + // When flags clear, restore rendered so post-FX can come back. useEffect(() => { - const disable = searchParams.get('disable') ?? '' - const light = - disable.split(',').some((p) => p.trim() === 'postFx') || searchParams.get('safe') === '1' try { - useViewer.getState().setShading(light ? 'solid' : 'rendered') + useViewer.getState().setShading(lightPreview ? 'solid' : 'rendered') } catch { // Viewer store may not be ready yet on first paint. } - }, [searchParams]) + }, [lightPreview, lightApplyTick]) const handleLoad = useCallback(async () => initialScene, [initialScene]) @@ -242,12 +250,24 @@ export function SceneLoader({ initialScene, meta }: SceneLoaderProps) {
    )}
    - { + // Force re-apply even if URL already has light-preview flags (toolbar may have changed shading). + setLightApplyTick((n) => n + 1) + try { + useViewer.getState().setShading('solid') + } catch { + // ignore + } + if (!lightPreview) { + router.push(`/scene/${meta.id}?${LIGHT_PREVIEW_QUERY}`) + } + }} > Light preview - +
    void + /** + * When true, skip the viewer post-FX pipeline (same as `?disable=postFx`). + * Hosts use this for a stable local "light preview" without relying only on + * module-load URL flags or shading toggles. + */ + disablePostFx?: boolean + // Version preview overlays (rendered by host app) sidebarOverlay?: ReactNode viewerBanner?: ReactNode @@ -961,6 +968,7 @@ const ViewerCanvas = memo(function ViewerCanvas({ onThumbnailCapture, viewerSceneSlot, floorplanSceneSlot, + disablePostFx = false, }: { isVersionPreviewMode: boolean isLoading: boolean @@ -973,6 +981,7 @@ const ViewerCanvas = memo(function ViewerCanvas({ onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void viewerSceneSlot?: ReactNode floorplanSceneSlot?: ReactNode + disablePostFx?: boolean }) { const viewMode = useEditor((s) => s.viewMode) const floorplanPaneRatio = useEditor((s) => s.floorplanPaneRatio) @@ -1086,6 +1095,7 @@ const ViewerCanvas = memo(function ViewerCanvas({ Date: Tue, 4 Aug 2026 17:57:06 +0400 Subject: [PATCH 5/6] editor: pass disablePostFx into version preview Viewer Toolbar/version preview mounts a second Viewer; without disablePostFx, light-preview scenes re-enable post-FX and can reintroduce GPU load. --- packages/editor/src/components/editor/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/editor/src/components/editor/index.tsx b/packages/editor/src/components/editor/index.tsx index 8ed7180be..90130ecdf 100644 --- a/packages/editor/src/components/editor/index.tsx +++ b/packages/editor/src/components/editor/index.tsx @@ -1324,6 +1324,7 @@ export default function Editor({ const previewViewerContent = ( Date: Tue, 4 Aug 2026 15:59:42 -0400 Subject: [PATCH 6/6] editor: make light preview a reversible toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every scene card linked to `?disable=postFx,outline`, so all saved scenes opened with the post-FX pipeline off and no way back — the button only pushed the query when it was absent. Links go back to `/scene/` and the button toggles the flag both ways. Drop the `setShading` effect. `disablePostFx` already skips the whole pipeline, and `shading` is a persisted user preference keyed by render context: forcing it wrote 'solid' (or 'rendered') into `shadingByContext`, so opening one scene changed the shading of the blank canvas at `/`. `?disable=outline` is redundant once postFx is off (the outline pass only exists inside the pipeline), and it costs the selection/hover outlines that are the only 3D feedback for a single selected node. `safe=1` was an undocumented second spelling with no other reference in the repo. Co-Authored-By: Claude Opus 5 --- apps/editor/app/scenes/page.tsx | 3 +- apps/editor/components/scene-loader.tsx | 48 +++++++++---------------- 2 files changed, 17 insertions(+), 34 deletions(-) diff --git a/apps/editor/app/scenes/page.tsx b/apps/editor/app/scenes/page.tsx index 9e35f622f..39b8dbf07 100644 --- a/apps/editor/app/scenes/page.tsx +++ b/apps/editor/app/scenes/page.tsx @@ -83,8 +83,7 @@ export default async function ScenesPage() {
  • {scene.thumbnailUrl ? ( diff --git a/apps/editor/components/scene-loader.tsx b/apps/editor/components/scene-loader.tsx index b56b6e948..fb891f1cb 100644 --- a/apps/editor/components/scene-loader.tsx +++ b/apps/editor/components/scene-loader.tsx @@ -9,18 +9,15 @@ import { type SceneGraph, type SidebarTab, } from '@pascal-app/editor' -import { useViewer } from '@pascal-app/viewer' import { Hammer, Layers } from 'lucide-react' import Image from 'next/image' import Link from 'next/link' import { useRouter, useSearchParams } from 'next/navigation' import { useCallback, useEffect, useRef, useState } from 'react' +import { cn } from '@/lib/utils' import { BuildTab } from './build-tab' import { CommunityViewerToolbarLeft, CommunityViewerToolbarRight } from './viewer-toolbar' -/** Lighter preview path: skips post-FX and outline passes (viewer `?disable=` flags). */ -export const LIGHT_PREVIEW_QUERY = 'disable=postFx,outline' - export interface SceneMeta { id: string name: string @@ -96,11 +93,14 @@ function sceneGraphSignature(graph: SceneGraphWithCollections): string { }) } +/** + * `?disable=postFx` is read at post-processing module load, so it only takes + * effect on a full page load. Reading it here as well lets the flag survive a + * client-side navigation, since `disablePostFx` is a live prop. + */ function isLightPreviewQuery(searchParams: URLSearchParams): boolean { const disable = searchParams.get('disable') ?? '' - return ( - disable.split(',').some((p) => p.trim() === 'postFx') || searchParams.get('safe') === '1' - ) + return disable.split(',').some((p) => p.trim() === 'postFx') } export function SceneLoader({ initialScene, meta }: SceneLoaderProps) { @@ -111,21 +111,9 @@ export function SceneLoader({ initialScene, meta }: SceneLoaderProps) { const suppressRemoteSaveUntilRef = useRef(0) const [conflict, setConflict] = useState(false) const [saveError, setSaveError] = useState(null) - /** Bumps when Light preview is clicked while already active so effects re-apply. */ - const [lightApplyTick, setLightApplyTick] = useState(0) const lightPreview = isLightPreviewQuery(searchParams) - // Light preview: host prop disablePostFx rebuilds the pipeline; solid shading pairs with it. - // When flags clear, restore rendered so post-FX can come back. - useEffect(() => { - try { - useViewer.getState().setShading(lightPreview ? 'solid' : 'rendered') - } catch { - // Viewer store may not be ready yet on first paint. - } - }, [lightPreview, lightApplyTick]) - const handleLoad = useCallback(async () => initialScene, [initialScene]) const handleSave = useCallback( @@ -251,20 +239,16 @@ export function SceneLoader({ initialScene, meta }: SceneLoaderProps) { )}