Skip to content
Merged
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
24 changes: 24 additions & 0 deletions packages/editor/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,34 @@ export { default as Editor } from './components/editor'
// surface uses the shorter, shell-friendly names from the unified
// preset-system spec.
export { FloatingActionMenu as FloatingMenu } from './components/editor/floating-action-menu'
// Embed surface — the editor's real in-canvas affordances, so a host can mount
// authentic selection handles, interactive build tools, and the mover on top
// of a bare `<Viewer>` without the full `<Editor>` shell.
// - `NodeArrowHandles` renders the selected node's registry resize/rotate/move
// handles.
// - `MoveTool` runs the kind-owned mover once a translate handle arms
// `useEditor.movingNode`.
// - `ToolManager` mounts the active registry build tool (wall / door / window /
// …) for interactive placement when `useEditor` is in build mode with a
// tool, plus the snap/alignment guide layers. Mount it only while a tool is
// active to avoid its select-mode boundary editors.
// - `Grid` is the interactive drafting plane: it raycasts the pointer and
// emits the `grid:move` / `grid:click` events the build tools consume (the
// wall tool is driven entirely by them; door/window use them for free-follow
// alongside the viewer's `wall:*` mesh events). Without it the tools mount
// but their cursor never tracks the pointer. Mount it while a tool is active.
// All read `useViewer` selection + `useEditor` state, and cooperate with host
// camera controls via the `useViewer.inputDragging` / `useEditor.movingNode`
// flags. Tools place onto `useViewer.selection.levelId`, so the host must set a
// building + level selection first.
export { Grid } from './components/editor/grid'
export {
DimensionPill,
type DimensionPillPart,
formatMeasurement,
MeasurementPill,
} from './components/editor/measurement-pill'
export { NodeArrowHandles } from './components/editor/node-arrow-handles'
export {
type SnapshotCameraData,
ThumbnailGenerator,
Expand All @@ -40,6 +62,7 @@ export {
type FencePlanPoint,
snapFenceDraftPoint,
} from './components/tools/fence/fence-drafting'
export { MoveTool } from './components/tools/item/move-tool'
// Placement-math helpers — shared by kind-owned placement tools in
// `@pascal-app/nodes` (wall curve sagitta snap, door / window placement,
// item drop) so kinds don't reach into editor internals.
Expand Down Expand Up @@ -101,6 +124,7 @@ export {
DEFAULT_STAIR_TYPE,
DEFAULT_STAIR_WIDTH,
} from './components/tools/stair/stair-defaults'
export { ToolManager } from './components/tools/tool-manager'
export {
createWallOnCurrentLevel,
getSegmentGridStep,
Expand Down
20 changes: 17 additions & 3 deletions packages/viewer/src/components/viewer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
useScene,
} from '@pascal-app/core'
import { Canvas, extend, type ThreeToJSXElements, useFrame, useThree } from '@react-three/fiber'
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
import { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useRef } from 'react'
import * as THREE from 'three/webgpu'
import { hasDrawableGeometry } from '../../lib/drawable-geometry'
import { PERF_OVERLAY_ENABLED, pushGpuSample } from '../../lib/gpu-perf'
Expand Down Expand Up @@ -269,6 +269,7 @@ interface ViewerProps {
perf?: boolean
useBvh?: boolean
renderContext?: RenderContext
transparent?: boolean
defaultRender?: {
shading?: RenderShading
textures?: boolean
Expand Down Expand Up @@ -312,6 +313,7 @@ const Viewer = forwardRef<ViewerHandle, ViewerProps>(function Viewer(
perf = false,
useBvh = true,
renderContext = 'editor',
transparent,
defaultRender,
isolate,
sceneReadyKey,
Expand Down Expand Up @@ -342,6 +344,16 @@ const Viewer = forwardRef<ViewerHandle, ViewerProps>(function Viewer(
}, [isolate])

const isDark = useViewer((state) => getSceneTheme(state.sceneTheme).appearance === 'dark')
const transparentBackground = useViewer((state) => state.transparentBackground)
useLayoutEffect(() => {
if (transparent === undefined) return

useViewer.getState().setTransparentBackground(transparent)
return () => {
useViewer.getState().setTransparentBackground(false)
}
}, [transparent])

const defaultShading = defaultRender?.shading
const defaultTextures = defaultRender?.textures
const defaultColorPreset = defaultRender?.colorPreset
Expand Down Expand Up @@ -386,7 +398,9 @@ const Viewer = forwardRef<ViewerHandle, ViewerProps>(function Viewer(
return (
<Canvas
camera={{ position: [50, 50, 50], fov: 50 }}
className={`transition-colors duration-700 ${isDark ? 'bg-[#1f2433]' : 'bg-[#fafafa]'}`}
className={`transition-colors duration-700 ${
transparentBackground ? 'bg-transparent' : isDark ? 'bg-[#1f2433]' : 'bg-[#fafafa]'
}`}
dpr={[1, maxDpr]}
frameloop="never"
gl={
Expand All @@ -396,7 +410,7 @@ const Viewer = forwardRef<ViewerHandle, ViewerProps>(function Viewer(
if (cached) return cached
const promise = (async () => {
try {
const renderer = new THREE.WebGPURenderer(props as any)
const renderer = new THREE.WebGPURenderer({ ...(props as any), alpha: true })
renderer.toneMapping = THREE.ACESFilmicToneMapping
renderer.toneMappingExposure = getSceneTheme(
useViewer.getState().sceneTheme,
Expand Down
32 changes: 29 additions & 3 deletions packages/viewer/src/components/viewer/post-processing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
oscSine,
output,
pass,
premultiplyAlpha,
renderOutput,
sample,
time,
uniform,
Expand Down Expand Up @@ -180,6 +182,8 @@ const PostProcessingPasses = ({
const projectId = useViewer((s) => s.projectId)
const shading = useViewer((s) => s.shading)
const edges = useViewer((s) => s.edges)
const inkOpacityOverride = useViewer((s) => s.inkOpacity)
const transparentBackground = useViewer((s) => s.transparentBackground)
const lastProjectIdRef = useRef(projectId)

// Bump this to force a pipeline rebuild (used by retry logic)
Expand Down Expand Up @@ -272,7 +276,7 @@ const PostProcessingPasses = ({
// Same 1px line thickness for both (soft's thickness is the nice one);
// strong reads heavier purely by being fully solid vs soft's lighter 50%.
const inkRadius = 1
const inkOpacity = edges === 'strong' ? 1 : 0.5
const inkOpacity = inkOpacityOverride ?? (edges === 'strong' ? 1 : 0.5)

console.log('[viewer/post-processing] Building pipeline', {
version: pipelineVersion,
Expand All @@ -283,6 +287,7 @@ const PostProcessingPasses = ({
hoverHighlightMode,
projectId,
shading,
transparentBackground,
rendererCtor: (renderer as any).constructor?.name,
width,
height,
Expand Down Expand Up @@ -420,6 +425,7 @@ const PostProcessingPasses = ({
// Single merged outline node: one shared depth pass for both selected + hovered groups.
const outliner = useViewer.getState().outliner
let compositeWithOutlines = sceneColor
let visualAlpha = contentAlpha
if (outlineEnabled) {
const outlineNode = mergedOutline(scene, camera, {
primaryObjects: outliner.selectedObjects,
Expand Down Expand Up @@ -447,6 +453,11 @@ const PostProcessingPasses = ({
.mul(hoverStrength)
.mul(osc)

const outlineAlpha = outlineNode.primaryVisibleEdge
.max(outlineNode.primaryHiddenEdge)
.max(outlineNode.secondaryVisibleEdge)
.max(outlineNode.secondaryHiddenEdge)
visualAlpha = visualAlpha.max(outlineAlpha)
compositeWithOutlines = vec4(
add(sceneColor.rgb, selectedOutline.add(hoverOutline)),
sceneColor.a,
Expand All @@ -457,9 +468,22 @@ const PostProcessingPasses = ({
// Editor overlays painted on top by their own alpha — they never get inked,
// AO'd, or outlined, and always read crisp regardless of scene depth.
const withOverlay = mix(composited, overlayColor.rgb, overlayColor.a)
const finalOutput = vec4(withOverlay, float(1))
let finalOutput: ReturnType<typeof premultiplyAlpha> | ReturnType<typeof vec4> = vec4(
withOverlay,
float(1),
)
if (transparentBackground) {
const overlayAlpha = overlayColor.a
const alpha = overlayAlpha.add(visualAlpha.mul(overlayAlpha.oneMinus()))
const straightRgb = overlayColor.rgb
.mul(overlayAlpha)
.add(compositeWithOutlines.rgb.mul(visualAlpha).mul(overlayAlpha.oneMinus()))
.div(alpha.max(float(0.00001)))
finalOutput = premultiplyAlpha(renderOutput(vec4(straightRgb, alpha)))
}

const renderPipeline = new RenderPipeline(renderer as unknown as WebGPURenderer)
renderPipeline.outputColorTransform = !transparentBackground
renderPipeline.outputNode = finalOutput
renderPipelineRef.current = renderPipeline
retryCountRef.current = 0
Expand Down Expand Up @@ -494,11 +518,13 @@ const PostProcessingPasses = ({
hoverStrength,
hoverVisibleColor,
edges,
inkOpacityOverride,
pipelineVersion,
projectId,
renderer,
scene,
shading,
transparentBackground,
size.height,
size.width,
zoneLayers,
Expand All @@ -525,7 +551,7 @@ const PostProcessingPasses = ({
if (PERF_POST_FX_DISABLED || hasPipelineErrorRef.current || !renderPipelineRef.current) {
try {
if ((renderer as any).setClearAlpha) {
;(renderer as any).setClearAlpha(1)
;(renderer as any).setClearAlpha(transparentBackground ? 0 : 1)
}
const submittedAt = PERF_OVERLAY_ENABLED ? performance.now() : 0
;(renderer as any).render(scene, camera)
Expand Down
13 changes: 13 additions & 0 deletions packages/viewer/src/store/use-viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ type ViewerState = {
showGrid: boolean
setShowGrid: (show: boolean) => void

transparentBackground: boolean
setTransparentBackground: (transparent: boolean) => void

// Embed-controlled ink-edge opacity override (null = use the per-mode default).
inkOpacity: number | null
setInkOpacity: (opacity: number | null) => void

projectId: string | null
setProjectId: (id: string | null) => void
projectPreferences: Record<
Expand Down Expand Up @@ -283,6 +290,12 @@ const useViewer = create<ViewerState>()(
return { showGrid: show, projectPreferences }
}),

transparentBackground: false,
setTransparentBackground: (transparent) => set({ transparentBackground: transparent }),

inkOpacity: null,
setInkOpacity: (opacity) => set({ inkOpacity: opacity }),

projectId: null,
setProjectId: (id) =>
set((state) => {
Expand Down
Loading