* feat(editor): preset-system primitives — presettable, sceneApi subtree round-trip, isolate + setCaptureMode enum, headless exports
Per pascalorg/editor#340 (redesigned: single live canvas, no Viewer scene prop).
Core
- `capabilities.presettable` on `NodeDefinition` + `isPresettable` /
`isPresettableKind` helpers. Explicit `false` on level / building /
site / zone / spawn / guide / scan / item; implicit `true` for any
kind with `def.parametrics`.
- `sceneApi.getSubtreeSnapshot(rootId)` + `materializeSubtree(subtree,
position, parentId?)` for round-tripping a node subtree through
catalog storage. Strips id / parentId / absolute root position /
host refs (`wallId`, `wallT`); fresh IDs minted at materialize time;
child ordering preserved (FIFO walk).
Viewer
- `<Viewer isolate>` prop + `ViewerHandle.setIsolated(ids | null)`.
Walks `sceneRegistry`, hides every registered group not in the
isolated set's ancestor + descendant closure. Building block for
preset capture + future focus-mode UX.
Editor
- `useEditor.captureMode: CaptureMode` discriminated union
(`idle` | `standard` | `preset`). `isCaptureMode` stays as a derived
boolean for the existing read sites; `setCaptureMode` accepts both
the boolean shape (back-compat) and the enum.
- `preset` capture mode in `SnapshotCaptureOverlay`: drag locked to a
square, mode-picker hidden, transparent flag forwarded through the
`camera-controls:generate-thumbnail` emitter event.
- Headless exports: `Inspector` (alias of `ParametricInspector`),
`FloatingMenu` (alias of `FloatingActionMenu`), `ToolbarLeft` /
`ToolbarRight` (aliases of `ViewerToolbarLeft` / `ViewerToolbarRight`),
`useSelection` hook returning `{selectedIds, selectedNode, building/
level/zone}`, plus re-exports of `useScene` / `useViewer` from core /
viewer so consumer shells (community, embedders) need only one import.
Out of scope by design (see issue #340 "Out of scope"): a separate
offscreen Viewer rendering an arbitrary subtree. The unified preset
modal captures inside the live canvas via isolation + the existing
snapshot pipeline — no `useScene` factory / React context refactor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(editor): split snapshot/materialize into pure getSubtree + cloneNodesInto; add def.hostRefFields; auto-stage preset capture square
Per pascalorg/editor#340 redesign discussion: the editor's scene API
should expose *pure* primitives and let the host (community modal,
embedders) own storage shape, position stripping, and host-ref
re-derivation policy.
Editor API delta
- `sceneApi.getSubtreeSnapshot(rootId)` → `sceneApi.getSubtree(rootId)`
Returns the live subtree verbatim (BFS via `children[]`, no clones,
no stripping). Callers deep-clone if they need persistence.
- `sceneApi.materializeSubtree(subtree, pos, parent?)`
→ `sceneApi.cloneNodesInto(nodes, { rootId, parentId?, position? })`
Generic clone-and-insert. Deep-clones via JSON, mints fresh ids
preserving the prefix, rewires parent/children, stamps position +
parent if supplied. Host-ref-agnostic — `wallId`/`wallT` etc are
preserved verbatim.
- New `capabilities.hostRefFields?: string[]` on `NodeDefinition`.
Declares per kind which schema fields are placement-derived so the
host strips them at preset-save time. Declared on door (`['wallId']`),
window (`['wallId']`), item (`['wallId', 'wallT']`).
- New `getHostRefFields(def)` exported from `@pascal-app/core`.
Removed the intermediate token-based payload format (`NodeSubtree`,
`buildSubtreeSnapshot`, `materializeSubtree`, `SubtreeNode`).
UX polish
- `<SnapshotCaptureOverlay>` in `preset` mode now auto-stages a centered
square crop sized to ~75% of the shorter viewport dimension. The
user can pan / move / resize within square-aspect, but doesn't have
to drag from scratch — clicking the capture button works
immediately on entry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(editor): lock preset capture frame; allow item presets
- SnapshotCaptureOverlay: in `preset` mode, the auto-staged centered
square is now fully locked — corner handles hidden, the dim layer is
click-through (no drag-to-move, no drag-to-resize). The user just
adjusts the camera (orbit / pan / zoom) and clicks capture. The
letterbox + dashed border stay visible as a cosmetic frame.
- `item.capabilities.presettable` removed (implicit `true` via
`def.parametrics`). Enables compositions like "table-with-plants",
"shelf-with-books" where the preset root may be an item and other
items ride along as descendants. The GLB-kind item catalog is
unchanged; presets become siblings of GLB rows under the same
`items` table.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): auto-frame camera on preset capture entry; restore on exit
`<CustomCameraControls>` now watches `useEditor.captureMode` and, when
preset capture mode begins, flies the camera to a pose that fits the
union bounds of the isolated subtree inside the locked square crop —
no more hunting for the subject after opening the modal. The
pre-capture pose is stashed and restored on exit so the user lands
exactly where they were.
The user can still pan / orbit / zoom from the auto-staged pose if
they want a different angle before snapping.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
144 lines
6.2 KiB
TypeScript
144 lines
6.2 KiB
TypeScript
// `NodeRenderer` is the recursive dispatch component used by parent
|
|
// renderers (wall renders doors/windows, slab renders hosted items).
|
|
// Public so registry-driven kinds can compose children without reaching
|
|
// into viewer's internal paths.
|
|
|
|
export type { SurfaceRole } from '@pascal-app/core'
|
|
export { ErrorBoundary } from './components/error-boundary'
|
|
// Stage A wrap-exports for the rest of the kinds — `@pascal-app/nodes`
|
|
// registers each via `def.renderer` (and `def.system` when present)
|
|
// Generic dispatch component used by recursive renderers (e.g. level →
|
|
// children, building → children). The per-kind renderers live in
|
|
// `@pascal-app/nodes/<kind>/renderer.tsx` and are loaded by the registry
|
|
// — no per-kind re-exports needed.
|
|
export { NodeRenderer } from './components/renderers/node-renderer'
|
|
export { default as Viewer, type ViewerHandle } from './components/viewer'
|
|
export type { HoverStyle, HoverStyles } from './components/viewer/post-processing'
|
|
export {
|
|
DEFAULT_HOVER_STYLES,
|
|
SSGI_PARAMS,
|
|
} from './components/viewer/post-processing'
|
|
export { WalkthroughControls } from './components/viewer/walkthrough-controls'
|
|
export { useAssetUrl } from './hooks/use-asset-url'
|
|
export { useGLTFKTX2 } from './hooks/use-gltf-ktx2'
|
|
export { useNodeEvents } from './hooks/use-node-events'
|
|
export { ASSETS_CDN_URL, resolveAssetUrl, resolveCdnUrl } from './lib/asset-url'
|
|
// CSG primitives — used by chimney's roof-trim and other kinds whose
|
|
// geometry subtracts pieces against their host. Lives in viewer
|
|
// because three-bvh-csg / three-mesh-bvh are viewer-only deps.
|
|
export {
|
|
ADDITION,
|
|
Brush,
|
|
computeGeometryBoundsTree,
|
|
csgEvaluator,
|
|
csgGeometry,
|
|
csgMaterials,
|
|
prepareBrushForCSG,
|
|
SUBTRACTION,
|
|
} from './lib/csg-utils'
|
|
export type { EdgeMode } from './lib/edge-style'
|
|
export { applyIsolation, clearIsolation, collectIsolationSubtree } from './lib/isolation'
|
|
export { GRID_LAYER, OVERLAY_LAYER, SCENE_LAYER, ZONE_LAYER } from './lib/layers'
|
|
export {
|
|
applyMaterialPresetToMaterials,
|
|
BLUEPRINT_PALETTE,
|
|
baseMaterial,
|
|
CLAY_PALETTE,
|
|
type ColorPreset,
|
|
clearMaterialCache,
|
|
createDefaultMaterial,
|
|
createMaterial,
|
|
createMaterialFromPresetRef,
|
|
createSurfaceRoleMaterial,
|
|
DEFAULT_CEILING_MATERIAL,
|
|
DEFAULT_DOOR_MATERIAL,
|
|
DEFAULT_ROOF_MATERIAL,
|
|
DEFAULT_SHELF_MATERIAL,
|
|
DEFAULT_SLAB_MATERIAL,
|
|
DEFAULT_STAIR_MATERIAL,
|
|
DEFAULT_WALL_MATERIAL,
|
|
DEFAULT_WINDOW_MATERIAL,
|
|
disposeMaterial,
|
|
glassMaterial,
|
|
MONO_PALETTE,
|
|
PRESET_PALETTES,
|
|
type RenderShading,
|
|
resolveSurfaceColor,
|
|
WHITE_PALETTE,
|
|
} from './lib/materials'
|
|
export { mergedOutline } from './lib/merged-outline-node'
|
|
export { unionPolygons } from './lib/polygon-union'
|
|
export {
|
|
getSceneTheme,
|
|
SCENE_THEME_IDS,
|
|
SCENE_THEMES,
|
|
type SceneTheme,
|
|
} from './lib/scene-themes'
|
|
export { useItemLightPool } from './store/use-item-light-pool'
|
|
export { default as useViewer } from './store/use-viewer'
|
|
export { CeilingSystem } from './systems/ceiling/ceiling-system'
|
|
export {
|
|
createColumnBoxGeometry,
|
|
createColumnCylinderGeometry,
|
|
createColumnSphereGeometry,
|
|
createColumnTorusGeometry,
|
|
} from './systems/column/column-geometry'
|
|
export { DoorAnimationSystem } from './systems/door/door-animation-system'
|
|
export { DoorSystem } from './systems/door/door-system'
|
|
export { ElevatorInteractionSystem } from './systems/elevator/elevator-interaction-system'
|
|
// Fence system follows the wall re-export pattern — composed into the
|
|
// registry-driven fence definition's `def.system`. Removed in Phase 6
|
|
// alongside the legacy fence mount point.
|
|
export { FenceSystem, generateFenceGeometry } from './systems/fence/fence-system'
|
|
// Generic floor-elevation system. Lifts the rendered mesh of any kind
|
|
// whose definition declares `capabilities.floorPlaced` by the slab
|
|
// elevation under its footprint. Replaces the per-kind elevation block
|
|
// that used to live inside `ItemSystem`.
|
|
export { FloorElevationSystem } from './systems/floor-elevation/floor-elevation-system'
|
|
export { GuideSystem } from './systems/guide/guide-system'
|
|
export { InteractiveSystem } from './systems/interactive/interactive-system'
|
|
// Item systems for the registry-driven item definition. ItemSystem
|
|
// applies attachTo-driven transforms each frame; ItemLightSystem
|
|
// manages item-mounted light sources.
|
|
export { ItemSystem } from './systems/item/item-system'
|
|
export { ItemLightSystem } from './systems/item-light/item-light-system'
|
|
export { LevelSystem } from './systems/level/level-system'
|
|
export { snapLevelsToTruePositions } from './systems/level/level-utils'
|
|
export { getRoofMaterialArray } from './systems/roof/roof-materials'
|
|
// Generic roof-segment primitives. Kinds that compose CSG against
|
|
// the roof shell (chimney's self-trim, dormer's virtual-segment cut)
|
|
// read these through the public surface. No kind-specific helpers
|
|
// belong here — those live in `@pascal-app/nodes/<kind>/`.
|
|
export {
|
|
getRoofOuterSurfaceFrameAtPoint,
|
|
getRoofSegmentBrushes,
|
|
mapRoofGroupMaterialIndex,
|
|
ROOF_MATERIAL_SLOT_COUNT,
|
|
RoofSystem,
|
|
remapRoofShellFaces,
|
|
roofCsgDummyMats,
|
|
type SurfaceFrame,
|
|
} from './systems/roof/roof-system'
|
|
export { ScanSystem } from './systems/scan/scan-system'
|
|
// Slab system follows the wall + fence re-export pattern — composed into
|
|
// the registry-driven slab definition's `def.system`. Removed in Phase 6
|
|
// alongside the legacy slab mount point.
|
|
export { generateSlabGeometry, SlabSystem } from './systems/slab/slab-system'
|
|
export {
|
|
getStairBodyMaterials,
|
|
getStairRailingMaterial,
|
|
getStraightStairSegmentBodyMaterials,
|
|
type StairBodyMaterials,
|
|
} from './systems/stair/stair-materials'
|
|
export { StairSystem } from './systems/stair/stair-system'
|
|
export { WallCutout } from './systems/wall/wall-cutout'
|
|
export { getVisibleWallMaterials } from './systems/wall/wall-materials'
|
|
// Wall internals re-exported so `@pascal-app/nodes`' registry-driven wall
|
|
// definition can compose them into `def.system` without duplicating the
|
|
// 800+ lines of CSG / mitering logic during Phase 3. These exports are
|
|
// removed in Phase 6 when the legacy mount points are deleted.
|
|
export { WallSystem } from './systems/wall/wall-system'
|
|
export { WindowAnimationSystem } from './systems/window/window-animation-system'
|
|
export { WindowSystem } from './systems/window/window-system'
|
|
export { ZoneSystem } from './systems/zone/zone-system'
|