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.
92 lines
3.3 KiB
TypeScript
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
|
|
}
|