Reset core/viewer/editor/mcp packages and apps/editor from private-editor
Wholesale swap of packages/{core,viewer,editor,mcp} and apps/editor with the
versions from the private editor repo, which is the production source of truth.
Setup changes:
- packages/{core,viewer,editor} versions held at 0.7.0 baseline (matching
the most recent published release) so a bump=minor publishes 0.8.0
- packages/mcp held at 0.1.1 (never published; first publish will go through
the new release.yml flow)
- peerDependencies and devDependencies for inter-package @pascal-app/*
references pinned to ^0.7.0 instead of '*' / 'workspace:*' so they are
valid for npm consumers
- Root package.json: TypeScript bumped to 6.0.2, added overrides for
@types/react, @types/react-dom, @types/three to prevent JSX namespace
fragmentation across the workspace
- release.yml extended to also publish editor and mcp; 'both' option renamed
to 'all'; added a sync step that updates inter-package peerDeps/devDeps to
match the new versions on every bump (so viewer/editor/mcp tarballs always
reference the version of core they were built against)
- Root scripts gained release:editor and release:mcp shortcuts
Verification:
- bun install --frozen-lockfile is consistent
- packages/{core,viewer,mcp} build cleanly, dist/index.d.ts emitted
- packages/editor check-types reports 21 pre-existing errors, identical to
what private-editor currently reports
Open PRs against editor-v2 will need rebasing/conflict resolution.
This commit is contained in:
@@ -834,37 +834,24 @@ function Flutes({
|
||||
)
|
||||
}
|
||||
|
||||
function DravidianShaftPanels({
|
||||
node,
|
||||
shaftY,
|
||||
shaftHeight,
|
||||
function DravidianPanelFace({
|
||||
position,
|
||||
rotation = 0,
|
||||
panelHeight,
|
||||
panelWidth,
|
||||
rail,
|
||||
reliefDepth,
|
||||
panelShape,
|
||||
}: {
|
||||
node: ColumnNode
|
||||
shaftY: number
|
||||
shaftHeight: number
|
||||
position: [number, number, number]
|
||||
rotation?: number
|
||||
panelHeight: number
|
||||
panelWidth: number
|
||||
rail: number
|
||||
reliefDepth: number
|
||||
panelShape: NonNullable<ColumnNode['panelShape']>
|
||||
}) {
|
||||
const panelCount = Math.max(
|
||||
node.panelCount ?? 0,
|
||||
node.style === 'dravidian-carved' || node.shaftDetail === 'panelled' ? 3 : 0,
|
||||
)
|
||||
if (panelCount <= 0 || shaftHeight <= 0) return null
|
||||
|
||||
const shaftWidth = node.width * 0.72
|
||||
const shaftDepth = node.depth * 0.72
|
||||
const panelHeight = Math.min(0.42, shaftHeight / Math.max(4, panelCount + 2))
|
||||
const panelWidth = node.width * 0.26
|
||||
const rail = Math.max(0.012, node.width * 0.028)
|
||||
const reliefDepth = Math.max(0.012, node.panelInsetDepth ?? node.width * 0.025)
|
||||
const rows = Array.from({ length: panelCount }, (_, index) => (index + 1) / (panelCount + 1))
|
||||
const panelShape = node.panelShape ?? 'rectangle'
|
||||
|
||||
const PanelFace = ({
|
||||
position,
|
||||
rotation = 0,
|
||||
}: {
|
||||
position: [number, number, number]
|
||||
rotation?: number
|
||||
}) => (
|
||||
return (
|
||||
<group position={position} rotation={[0, rotation, 0]}>
|
||||
<MappedBox
|
||||
depth={reliefDepth}
|
||||
@@ -917,6 +904,33 @@ function DravidianShaftPanels({
|
||||
)}
|
||||
</group>
|
||||
)
|
||||
}
|
||||
|
||||
function DravidianShaftPanels({
|
||||
node,
|
||||
shaftY,
|
||||
shaftHeight,
|
||||
}: {
|
||||
node: ColumnNode
|
||||
shaftY: number
|
||||
shaftHeight: number
|
||||
}) {
|
||||
const panelCount = Math.max(
|
||||
node.panelCount ?? 0,
|
||||
node.style === 'dravidian-carved' || node.shaftDetail === 'panelled' ? 3 : 0,
|
||||
)
|
||||
if (panelCount <= 0 || shaftHeight <= 0) return null
|
||||
|
||||
const shaftWidth = node.width * 0.72
|
||||
const shaftDepth = node.depth * 0.72
|
||||
const panelHeight = Math.min(0.42, shaftHeight / Math.max(4, panelCount + 2))
|
||||
const panelWidth = node.width * 0.26
|
||||
const rail = Math.max(0.012, node.width * 0.028)
|
||||
const reliefDepth = Math.max(0.012, node.panelInsetDepth ?? node.width * 0.025)
|
||||
const rows = Array.from({ length: panelCount }, (_, index) => (index + 1) / (panelCount + 1))
|
||||
const panelShape = node.panelShape ?? 'rectangle'
|
||||
|
||||
const faceProps = { panelHeight, panelWidth, rail, reliefDepth, panelShape }
|
||||
|
||||
return (
|
||||
<group>
|
||||
@@ -924,12 +938,23 @@ function DravidianShaftPanels({
|
||||
const y = shaftY + shaftHeight * t
|
||||
return (
|
||||
<group key={rowIndex}>
|
||||
<PanelFace position={[0, y, shaftDepth / 2 + reliefDepth / 2]} />
|
||||
<PanelFace position={[0, y, -shaftDepth / 2 - reliefDepth / 2]} />
|
||||
<PanelFace position={[shaftWidth / 2 + reliefDepth / 2, y, 0]} rotation={Math.PI / 2} />
|
||||
<PanelFace
|
||||
<DravidianPanelFace
|
||||
position={[0, y, shaftDepth / 2 + reliefDepth / 2]}
|
||||
{...faceProps}
|
||||
/>
|
||||
<DravidianPanelFace
|
||||
position={[0, y, -shaftDepth / 2 - reliefDepth / 2]}
|
||||
{...faceProps}
|
||||
/>
|
||||
<DravidianPanelFace
|
||||
position={[shaftWidth / 2 + reliefDepth / 2, y, 0]}
|
||||
rotation={Math.PI / 2}
|
||||
{...faceProps}
|
||||
/>
|
||||
<DravidianPanelFace
|
||||
position={[-shaftWidth / 2 - reliefDepth / 2, y, 0]}
|
||||
rotation={Math.PI / 2}
|
||||
{...faceProps}
|
||||
/>
|
||||
</group>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type DoorNode, useRegistry, useScene } from '@pascal-app/core'
|
||||
import { useLayoutEffect, useRef } from 'react'
|
||||
import { MeshBasicMaterial, type Mesh } from 'three'
|
||||
import { type Mesh, MeshBasicMaterial } from 'three'
|
||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||
|
||||
const doorHitboxMaterial = new MeshBasicMaterial({ visible: false })
|
||||
|
||||
@@ -2,28 +2,12 @@ import { type FenceNode, useRegistry, useScene } from '@pascal-app/core'
|
||||
import { useLayoutEffect, useMemo, useRef } from 'react'
|
||||
import type { Mesh } from 'three'
|
||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||
import {
|
||||
createMaterial,
|
||||
createMaterialFromPresetRef,
|
||||
DEFAULT_STAIR_MATERIAL,
|
||||
} from '../../../lib/materials'
|
||||
import { DEFAULT_STAIR_MATERIAL } from '../../../lib/materials'
|
||||
|
||||
export const FenceRenderer = ({ node }: { node: FenceNode }) => {
|
||||
const ref = useRef<Mesh>(null!)
|
||||
const handlers = useNodeEvents(node, 'fence')
|
||||
const material = useMemo(() => {
|
||||
const presetMaterial = createMaterialFromPresetRef(node.materialPreset)
|
||||
if (presetMaterial) return presetMaterial
|
||||
const mat = node.material
|
||||
if (!mat) return DEFAULT_STAIR_MATERIAL
|
||||
return createMaterial(mat)
|
||||
}, [
|
||||
node.materialPreset,
|
||||
node.material,
|
||||
node.material?.preset,
|
||||
node.material?.properties,
|
||||
node.material?.texture,
|
||||
])
|
||||
const material = useMemo(() => DEFAULT_STAIR_MATERIAL, [])
|
||||
|
||||
useRegistry(node.id, 'fence', ref)
|
||||
useLayoutEffect(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getMaterialPresetByRef, type SlabNode, useRegistry } from '@pascal-app/core'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import type { Mesh } from 'three'
|
||||
import * as THREE from 'three'
|
||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||
|
||||
@@ -24,10 +24,27 @@ export const StairSegmentRenderer = ({ node }: { node: StairSegmentNode }) => {
|
||||
const parentNode = node.parentId
|
||||
? (nodes[node.parentId as AnyNodeId] as StairNode | undefined)
|
||||
: undefined
|
||||
const material = useMemo(
|
||||
() => getStraightStairSegmentBodyMaterials(node, parentNode),
|
||||
[node, parentNode],
|
||||
)
|
||||
|
||||
const material = useMemo(() => {
|
||||
return getStraightStairSegmentBodyMaterials(node, parentNode)
|
||||
}, [
|
||||
node.materialPreset,
|
||||
node.material,
|
||||
node.material?.preset,
|
||||
node.material?.properties,
|
||||
node.material?.texture,
|
||||
parentNode?.materialPreset,
|
||||
parentNode?.material,
|
||||
parentNode?.material?.preset,
|
||||
parentNode?.material?.properties,
|
||||
parentNode?.material?.texture,
|
||||
parentNode?.railingMaterialPreset,
|
||||
parentNode?.railingMaterial,
|
||||
parentNode?.sideMaterialPreset,
|
||||
parentNode?.sideMaterial,
|
||||
parentNode?.treadMaterialPreset,
|
||||
parentNode?.treadMaterial,
|
||||
])
|
||||
|
||||
const placeholderGeometry = useMemo(() => {
|
||||
const geometry = new THREE.BufferGeometry()
|
||||
|
||||
@@ -112,9 +112,9 @@ export const StairRenderer = ({ node }: { node: StairNode }) => {
|
||||
receiveShadow
|
||||
/>
|
||||
) : null}
|
||||
{!isSegmentBasedStair ? (
|
||||
{isSegmentBasedStair ? null : (
|
||||
<CurvedStairBody bodyMaterials={straightBodyMaterials} stair={node} />
|
||||
) : null}
|
||||
)}
|
||||
<StairRailings material={railingMaterial} stair={node} />
|
||||
{isSegmentBasedStair ? (
|
||||
<group name="segments-wrapper" visible={false}>
|
||||
@@ -292,7 +292,7 @@ function StairRailings({ stair, material }: { stair: StairNode; material: THREE.
|
||||
))}
|
||||
{railPaths.slice(1).map((segmentPath, index) => {
|
||||
const previousPath = railPaths[index]
|
||||
if (!previousPath || !segmentPath.connectFromPrevious) return null
|
||||
if (!(previousPath && segmentPath.connectFromPrevious)) return null
|
||||
if (previousPath.layout.segment.segmentType === 'landing') return null
|
||||
if (segmentPath.layout.segment.segmentType === 'landing') return null
|
||||
|
||||
@@ -451,10 +451,10 @@ function CurvedStairBody({
|
||||
{isSpiral && (stair.showCenterColumn ?? true) ? (
|
||||
<mesh
|
||||
castShadow
|
||||
receiveShadow
|
||||
material={sideMaterial}
|
||||
name="stair-side"
|
||||
position={[0, spiralColumnHeight / 2, 0]}
|
||||
receiveShadow
|
||||
>
|
||||
<cylinderGeometry
|
||||
args={[spiralColumnRadius, spiralColumnRadius, spiralColumnHeight, 10]}
|
||||
|
||||
@@ -6,7 +6,8 @@ type FrameLimiterProps = {
|
||||
}
|
||||
|
||||
const FrameLimiter: React.FC<FrameLimiterProps> = ({ fps = 50 }) => {
|
||||
const { advance, set, frameloop: initFrameloop } = useThree()
|
||||
const { advance, set, frameloop: initFrameloop, scene, clock } = useThree()
|
||||
const renderer = useThree((state) => state.gl)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
let elapsed = 0
|
||||
@@ -14,7 +15,6 @@ const FrameLimiter: React.FC<FrameLimiterProps> = ({ fps = 50 }) => {
|
||||
let i = 0
|
||||
let raf: number | null = null
|
||||
const interval = 1000 / fps
|
||||
|
||||
function tick(t: DOMHighResTimeStamp) {
|
||||
raf = requestAnimationFrame(tick)
|
||||
elapsed = t - then
|
||||
@@ -24,10 +24,11 @@ const FrameLimiter: React.FC<FrameLimiterProps> = ({ fps = 50 }) => {
|
||||
then = t - (elapsed % interval)
|
||||
}
|
||||
}
|
||||
|
||||
// Set frameloop to never, it will shut down the default render loop
|
||||
set({ frameloop: 'never' })
|
||||
// Kick off custom render loop
|
||||
raf = requestAnimationFrame(tick)
|
||||
|
||||
// Restore initial setting
|
||||
return () => {
|
||||
if (raf) {
|
||||
cancelAnimationFrame(raf)
|
||||
|
||||
@@ -81,7 +81,7 @@ extend(THREE as any)
|
||||
const WEBGPU_RENDERER_CACHE = new WeakMap<HTMLCanvasElement, Promise<THREE.WebGPURenderer>>()
|
||||
|
||||
/**
|
||||
* Monitors the WebGPU device for loss events and logs them.
|
||||
* Monitors the WebGPU device for loss / uncaptured errors and logs them.
|
||||
* WebGPU device loss can happen when:
|
||||
* - Tab is backgrounded and OS reclaims GPU
|
||||
* - Driver crash or GPU reset
|
||||
@@ -105,7 +105,7 @@ function GPUDeviceWatcher() {
|
||||
|
||||
useEffect(() => {
|
||||
const backend = (gl as any).backend
|
||||
const device = backend?.device as WebGPUDeviceLike | undefined
|
||||
const device: GPUDevice | undefined = backend?.device
|
||||
|
||||
if (!device) {
|
||||
console.warn('[viewer] No WebGPU device on backend — running on a fallback renderer.', {
|
||||
@@ -117,25 +117,25 @@ function GPUDeviceWatcher() {
|
||||
|
||||
console.log('[viewer] WebGPU device ready', {
|
||||
label: device.label,
|
||||
features: device.features ? Array.from(device.features) : [],
|
||||
features: Array.from(device.features ?? []),
|
||||
})
|
||||
|
||||
device.lost.then((info: WebGPUDeviceLossInfo) => {
|
||||
device.lost.then((info) => {
|
||||
console.error(
|
||||
`[viewer] WebGPU device lost: reason="${info.reason ?? 'unknown'}", message="${info.message ?? ''}". ` +
|
||||
`[viewer] WebGPU device lost: reason="${info.reason}", message="${info.message}". ` +
|
||||
'The page must be reloaded to recover the GPU context.',
|
||||
)
|
||||
})
|
||||
|
||||
// Uncaptured errors are normally silent (only console-warned by Chrome at
|
||||
// best). Pipe them to console.error so silent mobile crashes show up.
|
||||
const onUncapturedError = (event: any) => {
|
||||
console.error('[viewer] WebGPU uncaptured error:', event?.error?.message, event?.error)
|
||||
const onUncapturedError = (event: GPUUncapturedErrorEvent) => {
|
||||
console.error('[viewer] WebGPU uncaptured error:', event.error.message, event.error)
|
||||
}
|
||||
device.addEventListener?.('uncapturederror', onUncapturedError)
|
||||
device.addEventListener('uncapturederror', onUncapturedError as EventListener)
|
||||
|
||||
return () => {
|
||||
device.removeEventListener?.('uncapturederror', onUncapturedError)
|
||||
device.removeEventListener('uncapturederror', onUncapturedError as EventListener)
|
||||
}
|
||||
}, [gl])
|
||||
|
||||
@@ -147,6 +147,7 @@ interface ViewerProps {
|
||||
hoverStyles?: HoverStyles
|
||||
selectionManager?: 'default' | 'custom'
|
||||
perf?: boolean
|
||||
useBvh?: boolean
|
||||
}
|
||||
|
||||
const Viewer: React.FC<ViewerProps> = ({
|
||||
@@ -154,6 +155,7 @@ const Viewer: React.FC<ViewerProps> = ({
|
||||
hoverStyles = DEFAULT_HOVER_STYLES,
|
||||
selectionManager = 'default',
|
||||
perf = false,
|
||||
useBvh = true,
|
||||
}) => {
|
||||
const theme = useViewer((state) => state.theme)
|
||||
return (
|
||||
@@ -216,9 +218,13 @@ const Viewer: React.FC<ViewerProps> = ({
|
||||
{/* <directionalLight position={[10, 10, 5]} intensity={0.5} castShadow
|
||||
/> */}
|
||||
<Lights />
|
||||
<Bvh>
|
||||
{useBvh ? (
|
||||
<Bvh>
|
||||
<SceneRenderer />
|
||||
</Bvh>
|
||||
) : (
|
||||
<SceneRenderer />
|
||||
</Bvh>
|
||||
)}
|
||||
|
||||
{/* Default Systems */}
|
||||
<LevelSystem />
|
||||
|
||||
@@ -131,8 +131,6 @@ const PostProcessingPasses = ({
|
||||
|
||||
// Reset retry state when project changes
|
||||
useEffect(() => {
|
||||
// Intentionally touch projectId so the effect reruns on project switches.
|
||||
void projectId
|
||||
retryCountRef.current = 0
|
||||
if (rebuildTimeoutRef.current !== null) {
|
||||
clearTimeout(rebuildTimeoutRef.current)
|
||||
@@ -168,11 +166,6 @@ const PostProcessingPasses = ({
|
||||
|
||||
// Build / rebuild the post-processing pipeline
|
||||
useEffect(() => {
|
||||
// Intentionally touch these so React/biome treat project switches and retry bumps
|
||||
// as explicit rebuild triggers instead of accidental extra dependencies.
|
||||
void projectId
|
||||
void pipelineVersion
|
||||
|
||||
if (!(renderer && scene && camera)) {
|
||||
console.warn('[viewer/post-processing] Skipping pipeline build — missing dependency.', {
|
||||
hasRenderer: !!renderer,
|
||||
@@ -192,24 +185,6 @@ const PostProcessingPasses = ({
|
||||
|
||||
hasPipelineErrorRef.current = false
|
||||
|
||||
// WebGPU availability check: SSGI, denoise, and RenderPipeline are all
|
||||
// WebGPU-only APIs. When the browser falls back to WebGL2 (no
|
||||
// `navigator.gpu`, or the device couldn't be created), building the
|
||||
// pipeline either throws silently or produces a broken output where
|
||||
// the scene renders for a few frames and then goes black as the retry
|
||||
// loop fights the direct-render fallback path. Short-circuit here so
|
||||
// `useFrame` uses the direct `renderer.render(scene, camera)` path
|
||||
// exclusively and never attempts the TSL pipeline.
|
||||
const hasWebGPU = typeof navigator !== 'undefined' && 'gpu' in navigator
|
||||
if (!hasWebGPU) {
|
||||
console.warn(
|
||||
'[viewer] WebGPU unavailable — rendering without post-processing (SSGI, outlines, denoise).',
|
||||
)
|
||||
hasPipelineErrorRef.current = true
|
||||
renderPipelineRef.current = null
|
||||
return
|
||||
}
|
||||
|
||||
// Clear outliner arrays synchronously to prevent stale Object3D refs
|
||||
// from the previous project leaking into the new pipeline's outline passes.
|
||||
const outliner = useViewer.getState().outliner
|
||||
|
||||
Reference in New Issue
Block a user