Merge remote-tracking branch 'origin/main' into feat/paint-slots

# Conflicts:
#	packages/core/src/store/use-scene.ts
#	packages/editor/src/components/editor/index.tsx
This commit is contained in:
Wassim SAMAD
2026-06-18 12:22:26 -04:00
415 changed files with 22805 additions and 1414 deletions
@@ -70,12 +70,16 @@ export const ParametricNodeRenderer = ({ node }: { node: AnyNode }) => {
const position = liveTransform?.position ?? overridePosition ?? n.position ?? [0, 0, 0]
const rawRotation = overrideRotation ?? n.rotation
const baseRotation: [number, number, number] =
typeof rawRotation === 'number' ? [0, rawRotation, 0] : (rawRotation ?? [0, 0, 0])
// The live transform carries only the plan-view Y rotation; keep the
// node's own X/Z so 3D-oriented kinds (e.g. a duct-fitting riser at
// X=π/2) don't visually flatten to horizontal mid-drag. Matches the
// move tool's commit, which also replaces only the Y component.
const rotation: [number, number, number] =
liveTransform?.rotation !== undefined
? [0, liveTransform.rotation, 0]
: typeof rawRotation === 'number'
? [0, rawRotation, 0]
: (rawRotation ?? [0, 0, 0])
? [baseRotation[0], liveTransform.rotation, baseRotation[2]]
: baseRotation
return (
<group
@@ -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'
@@ -275,6 +275,7 @@ interface ViewerProps {
perf?: boolean
useBvh?: boolean
renderContext?: RenderContext
transparent?: boolean
defaultRender?: {
shading?: RenderShading
textures?: boolean
@@ -318,6 +319,7 @@ const Viewer = forwardRef<ViewerHandle, ViewerProps>(function Viewer(
perf = false,
useBvh = true,
renderContext = 'editor',
transparent,
defaultRender,
isolate,
sceneReadyKey,
@@ -348,6 +350,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
@@ -392,7 +404,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={
@@ -402,7 +416,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,
@@ -282,6 +286,7 @@ const PostProcessingPasses = ({
perfDisable,
projectId,
shading,
transparentBackground,
rendererCtor: (renderer as any).constructor?.name,
width,
height,
@@ -419,6 +424,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,
@@ -446,6 +452,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,
@@ -456,9 +467,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
@@ -496,11 +520,13 @@ const PostProcessingPasses = ({
hoverStrength,
hoverVisibleColor,
edges,
inkOpacityOverride,
pipelineVersion,
projectId,
renderer,
scene,
shading,
transparentBackground,
size.height,
size.width,
zoneLayers,
@@ -527,7 +553,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)