Wawa e618175bba 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.
2026-05-09 20:52:26 +00:00

92 lines
3.3 KiB
TypeScript

import { type AnyNodeId, type StairNode, sceneRegistry, useScene } from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { useCallback, useEffect, useRef } from 'react'
/**
* Imperatively toggles the Three.js visibility of stair objects based on the
* editor selection — without causing React re-renders in StairRenderer.
*
* When a stair (or one of its segments) is selected:
* - merged-stair mesh is hidden
* - segments-wrapper group is shown (individual segments visible for editing)
* - all children are marked dirty so StairSystem rebuilds their geometry
*
* When deselected:
* - merged-stair mesh is shown
* - segments-wrapper group is hidden
*/
export const StairEditSystem = () => {
const selectedIds = useViewer((s) => s.selection.selectedIds)
const selectedStairSignature = useScene(
useCallback(
(state) =>
selectedIds
.map((id) => {
const node = state.nodes[id as AnyNodeId]
if (!node) return null
if (node.type === 'stair') {
return `${node.id}:${node.stairType}`
}
if (node.type === 'stair-segment' && node.parentId) {
const parent = state.nodes[node.parentId as AnyNodeId] as StairNode | undefined
return parent?.type === 'stair' ? `${parent.id}:${parent.stairType}` : null
}
return null
})
.filter(Boolean)
.join('|'),
[selectedIds],
),
)
const prevActiveStairIds = useRef(new Set<string>())
useEffect(() => {
const nodes = useScene.getState().nodes
// Collect which stair nodes should be in "edit mode"
const activeStairIds = new Set<string>()
for (const id of selectedIds) {
const node = nodes[id as AnyNodeId]
if (!node) continue
if (node.type === 'stair') {
activeStairIds.add(id)
} else if (node.type === 'stair-segment' && node.parentId) {
activeStairIds.add(node.parentId)
}
}
// Update all stairs that are currently active OR were previously active
const stairIdsToUpdate = new Set([...activeStairIds, ...prevActiveStairIds.current])
for (const stairId of stairIdsToUpdate) {
const group = sceneRegistry.nodes.get(stairId)
if (!group) continue
const stairNode = nodes[stairId as AnyNodeId] as StairNode | undefined
const isCurved = stairNode?.stairType === 'curved' || stairNode?.stairType === 'spiral'
const mergedMesh = group.getObjectByName('merged-stair')
const segmentsWrapper = group.getObjectByName('segments-wrapper')
const isActive = activeStairIds.has(stairId)
if (mergedMesh) mergedMesh.visible = !(isActive || isCurved)
if (segmentsWrapper) segmentsWrapper.visible = isActive && !isCurved
if (stairNode?.children?.length) {
const wasActive = prevActiveStairIds.current.has(stairId)
if (isActive !== wasActive) {
// Entering edit mode: rebuild individual segment geometries
// Exiting edit mode: sync transforms + rebuild merged mesh
const { markDirty } = useScene.getState()
for (const childId of stairNode.children) {
markDirty(childId as AnyNodeId)
}
}
}
}
prevActiveStairIds.current = activeStairIds
}, [selectedIds, selectedStairSignature])
return null
}