Merge pull request #419 from pascalorg/feat/embed-surface-on-main
Land embed-surface on main (in-canvas affordance exports + transparent/faint-ink viewer)
This commit is contained in:
@@ -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,
|
||||
@@ -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.
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
@@ -269,6 +269,7 @@ interface ViewerProps {
|
||||
perf?: boolean
|
||||
useBvh?: boolean
|
||||
renderContext?: RenderContext
|
||||
transparent?: boolean
|
||||
defaultRender?: {
|
||||
shading?: RenderShading
|
||||
textures?: boolean
|
||||
@@ -312,6 +313,7 @@ const Viewer = forwardRef<ViewerHandle, ViewerProps>(function Viewer(
|
||||
perf = false,
|
||||
useBvh = true,
|
||||
renderContext = 'editor',
|
||||
transparent,
|
||||
defaultRender,
|
||||
isolate,
|
||||
sceneReadyKey,
|
||||
@@ -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
|
||||
@@ -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={
|
||||
@@ -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,
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
oscSine,
|
||||
output,
|
||||
pass,
|
||||
premultiplyAlpha,
|
||||
renderOutput,
|
||||
sample,
|
||||
time,
|
||||
uniform,
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
@@ -283,6 +287,7 @@ const PostProcessingPasses = ({
|
||||
hoverHighlightMode,
|
||||
projectId,
|
||||
shading,
|
||||
transparentBackground,
|
||||
rendererCtor: (renderer as any).constructor?.name,
|
||||
width,
|
||||
height,
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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
|
||||
@@ -494,11 +518,13 @@ const PostProcessingPasses = ({
|
||||
hoverStrength,
|
||||
hoverVisibleColor,
|
||||
edges,
|
||||
inkOpacityOverride,
|
||||
pipelineVersion,
|
||||
projectId,
|
||||
renderer,
|
||||
scene,
|
||||
shading,
|
||||
transparentBackground,
|
||||
size.height,
|
||||
size.width,
|
||||
zoneLayers,
|
||||
@@ -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)
|
||||
|
||||
@@ -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<
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user