Skip to content
Open
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
12 changes: 4 additions & 8 deletions apps/editor/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,12 @@ export default function Home() {
<div className="relative h-screen w-screen">
{PROJECT_ID === 'local-editor' && (
<div className="pointer-events-none absolute top-3 left-1/2 z-40 -translate-x-1/2">
<div className="pointer-events-auto flex items-center gap-3 rounded-full border border-border/60 bg-background/90 px-4 py-1.5 text-xs shadow-sm backdrop-blur">
<span className="text-muted-foreground">Local editor — scenes are not saved.</span>
<Link className="font-medium text-foreground hover:underline" href="/scenes">
Open recent scenes
</Link>
<span aria-hidden className="text-muted-foreground">
·
<div className="pointer-events-auto flex max-w-[min(92vw,42rem)] flex-wrap items-center justify-center gap-x-3 gap-y-1 rounded-full border border-border/60 bg-background/90 px-4 py-1.5 text-xs shadow-sm backdrop-blur">
<span className="text-muted-foreground">
Blank canvas — saved scenes are under Scenes (not this page).
</span>
<Link className="font-medium text-foreground hover:underline" href="/scenes">
Create new
Open saved scenes
</Link>
</div>
</div>
Expand Down
3 changes: 2 additions & 1 deletion apps/editor/app/scenes/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ export default async function ScenesPage() {
<li key={scene.id}>
<Link
className="group block rounded-xl border border-border/60 bg-background p-4 transition-colors hover:border-border hover:bg-accent/30"
href={`/scene/${scene.id}`}
href={`/scene/${scene.id}?disable=postFx,outline`}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Post-FX stuck after light preview

High Severity

The new light preview feature causes two viewer state issues: post-FX remains disabled after client navigation clears the ?disable=postFx query, and the useEffect incorrectly persists 'rendered' shading for the editor context, leading other editor instances (like the blank home editor) to load with 'rendered' instead of the default 'solid'.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a035819. Configure here.

title="Opens with light preview (post-FX off) for stable local WebGPU"
>
<div className="flex aspect-video items-center justify-center overflow-hidden rounded-lg bg-accent/30">
{scene.thumbnailUrl ? (
Expand Down
47 changes: 46 additions & 1 deletion apps/editor/components/scene-loader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@ 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 } 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'

/** 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
Expand Down Expand Up @@ -92,13 +96,35 @@ 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()
const versionRef = useRef(meta.version)
const lastRemoteGraphJsonRef = useRef<string | null>(null)
const suppressRemoteSaveUntilRef = useRef(0)
const [conflict, setConflict] = useState(false)
const [saveError, setSaveError] = useState<string | null>(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])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rendered shading leaks to home

Medium Severity

When a scene loads without light-preview query params, SceneLoader forces the shared viewer store to rendered shading. That value persists in global shadingByContext after leaving the scene, so the blank home editor can mount with rendered shading instead of the editor’s default solid mode.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a035819. Configure here.


const handleLoad = useCallback(async () => initialScene, [initialScene])

Expand Down Expand Up @@ -224,6 +250,24 @@ export function SceneLoader({ initialScene, meta }: SceneLoaderProps) {
</div>
)}
<div className="pointer-events-none absolute top-4 right-4 z-40 flex items-center gap-2">
<button
type="button"
className="pointer-events-auto rounded-md border border-border bg-background/90 px-3 py-1.5 font-medium text-xs shadow-sm backdrop-blur hover:bg-accent/40"
onClick={() => {
// 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
Comment thread
cursor[bot] marked this conversation as resolved.
</button>
<Link
className="pointer-events-auto rounded-md border border-border bg-background/90 px-3 py-1.5 font-medium text-xs shadow-sm backdrop-blur hover:bg-accent/40"
href="/scenes"
Expand All @@ -232,6 +276,7 @@ export function SceneLoader({ initialScene, meta }: SceneLoaderProps) {
</Link>
</div>
<Editor
disablePostFx={lightPreview}
layoutVersion="v2"
onLoad={handleLoad}
onSave={handleSave}
Expand Down
11 changes: 11 additions & 0 deletions apps/editor/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ const nextConfig: NextConfig = {
typescript: {
ignoreBuildErrors: true,
},
// MCP / package metadata returns `/editor/<id>` (hosted route). This open-source
// app serves saved scenes at `/scene/<id>` — redirect so links and bookmarks work.
async redirects() {
return [
{
source: '/editor/:id',
destination: '/scene/:id',
permanent: false,
},
]
},
transpilePackages: [
'three',
'@pascal-app/viewer',
Expand Down
13 changes: 13 additions & 0 deletions packages/editor/src/components/editor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,13 @@ export interface EditorProps {
// Thumbnail
onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => 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
Expand Down Expand Up @@ -961,6 +968,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
onThumbnailCapture,
viewerSceneSlot,
floorplanSceneSlot,
disablePostFx = false,
}: {
isVersionPreviewMode: boolean
isLoading: boolean
Expand All @@ -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)
Expand Down Expand Up @@ -1086,6 +1095,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
<SelectionPersistenceManager enabled={hasLoadedInitialScene && !showLoader} />
<Viewer
defaultRender={EDITOR_DEFAULT_RENDER}
disablePostFx={disablePostFx}
hoverStyles={EDITOR_HOVER_STYLES}
onSceneReadyChange={onSceneReadyChange}
renderContext="editor"
Expand Down Expand Up @@ -1131,6 +1141,7 @@ export default function Editor({
isLoading = false,
onLoaderChange,
onThumbnailCapture,
disablePostFx = false,
sidebarOverlay,
viewerBanner,
settingsPanelProps,
Expand Down Expand Up @@ -1313,6 +1324,7 @@ export default function Editor({
const previewViewerContent = (
<Viewer
defaultRender={EDITOR_DEFAULT_RENDER}
disablePostFx={disablePostFx}
hoverStyles={EDITOR_HOVER_STYLES}
renderContext="editor"
selectionManager="default"
Expand All @@ -1331,6 +1343,7 @@ export default function Editor({

const viewerCanvas = (
<ViewerCanvas
disablePostFx={disablePostFx}
Comment thread
cursor[bot] marked this conversation as resolved.
hasLoadedInitialScene={hasLoadedInitialScene}
isFirstPersonMode={isFirstPersonMode}
isLoading={isLoading}
Expand Down