# Tools *Editor tools structure in `apps/editor`.* Applies to: `apps/editor/components/tools/**`. Tools are React components that capture user input (pointer, keyboard) and translate it into `useScene` mutations. They live exclusively in `apps/editor/components/tools/`. ## Lifecycle `ToolManager` reads `useEditor` (phase + mode + tool) and mounts the active tool component. When the tool changes, the old component unmounts, cleaning up any transient state. See `apps/editor/components/tools/tool-manager.tsx`. ## Tool Categories by Phase **Site** - `site-boundary-editor` — draw/edit property boundary polygon **Structure** - `wall-tool` — draw walls segment by segment - `slab-tool` + `slab-boundary-editor` + `slab-hole-editor` - `ceiling-tool` + `ceiling-boundary-editor` + `ceiling-hole-editor` - `roof-tool` - `door-tool` + `door-move-tool` - `window-tool` + `window-move-tool` - `item-tool` + `item-move-tool` - `zone-tool` + `zone-boundary-editor` **Furnish** - `item-tool` — place furniture **Shared utilities** - `polygon-editor` — reusable boundary/hole editing logic - `cursor-sphere` — 3D cursor visualisation ## Pattern ```tsx // apps/editor/components/tools/my-tool/index.tsx import { useScene } from '@pascal-app/core' import { useEditor } from '../../store/use-editor' export function MyTool() { const createNode = useScene(s => s.createNode) const setTool = useEditor(s => s.setTool) // Pointer handlers mutate the scene store directly. // No local geometry — use a renderer for any preview mesh. return ( {/* ghost / preview geometry only */} ) } ``` ## Rules - **Tools mutate `useScene` for committed changes and `useLiveTransforms` for ephemeral drag state.** A tool's end-of-interaction write (click-to-commit, release-to-commit) goes to `useScene` and is captured in undo history. Per-mouse-move previews go to `useLiveTransforms` so history and subscribers aren't spammed. - **Live-drag exception for direct mesh transforms.** During an active drag a tool may apply a transform offset directly to `sceneRegistry.nodes.get(id).position`/`rotation`/`scale` *when and only when* the same offset is mirrored into `useLiveTransforms` for that node. This exception exists because the 3D renderers don't reconcile `useLiveTransforms` onto `mesh.position` yet; once a `LiveTransformSystem` does that, this exception goes away. Conditions: - The mesh offset must mirror the `useLiveTransforms` entry (**same exact value**, not "same conceptual translation"), so anything reading `useLiveTransforms` sees the same preview as the 3D view. `ParametricNodeRenderer` (used for every kind that ships `def.geometry`) binds `` via React — every Zustand notification re-renders and reconciles the group's position back to whatever value `useLiveTransforms` holds. If `mesh.position.set(delta)` and `useLiveTransforms.set({ position: someOtherValue })` disagree, the two writes fight every frame and the user sees jitter during the drag. - **For position-based kinds (spawn / item / column)**: the `position` field on the node IS the group's local-frame position, so `useLiveTransforms.position` should hold the live world position of the node (matches the eventual `scene.update`). - **For polygon-based kinds (slab / fence / ceiling / wall)**: the node has no `position` field — the canonical group position is `[0,0,0]` with geometry built in level-local coords. `useLiveTransforms.position` must hold the **delta** the tool wants to translate by (`[deltaX, 0, deltaZ]`), not the world location of the polygon's center. The cursor sphere position (which IS the translated polygon center) is tracked separately via React `useState`, not `useLiveTransforms`. - The offset must be cleared on tool unmount, cancel, *and* commit — both `mesh.position.set(0, 0, 0)` and `useLiveTransforms.clear(id)`. - The tool must not generate or mutate geometry in this path — only transform writes. Geometry generation still belongs in a core system. - **No business logic in tools** — delegate geometry/constraint rules to core systems. - **Preview geometry is local** — transient meshes shown while a tool is active live in the tool component, not in the scene store. - **Clean up on unmount** — remove any pending/incomplete nodes *and* any live transforms/mesh offsets when the tool unmounts. - **Tools must not import from `@pascal-app/viewer`** — use the scene store and core hooks only. `sceneRegistry` is exported from `@pascal-app/core` and is the allowed door into the Three.js graph for the narrow purposes above. - Each tool should handle a single, well-scoped interaction. Split complex tools (e.g. "draw + move") into separate components selected by `useEditor`. ## Adding a New Tool 1. Create `apps/editor/components/tools//index.tsx`. 2. Register the tool in `ToolManager` under the correct phase and mode. 3. Add the tool identifier to the `useEditor` tool union type. 4. If the tool requires new node types, add schema + renderer + system first.