diff --git a/apps/ifc-converter/next-env.d.ts b/apps/ifc-converter/next-env.d.ts index c4b7818f..9edff1c7 100644 --- a/apps/ifc-converter/next-env.d.ts +++ b/apps/ifc-converter/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/packages/core/src/registry/types.ts b/packages/core/src/registry/types.ts index 347af4d7..6f57ff0b 100644 --- a/packages/core/src/registry/types.ts +++ b/packages/core/src/registry/types.ts @@ -241,6 +241,12 @@ export type FloorplanGeometry = stroke?: string strokeWidth?: number paintOrder?: 'stroke' | 'fill' | 'normal' + /** + * When true, the registry layer counter-rotates the label by + * `sceneRotationDeg` so it reads horizontally on screen regardless + * of the floor-plan's scene rotation (default 90°). + */ + upright?: boolean } /** * Bitmap overlay — captured top-down asset thumbnail, AI-generated @@ -981,6 +987,22 @@ export type Capabilities = { * `AlignmentFootprintConfig`. */ alignmentFootprint?: AlignmentFootprintConfig + /** + * Bounds drawn by the 3D drag bounding box during a move. Opt-in: when + * omitted, the box auto-measures the rendered mesh, which is correct for + * most kinds. Set this when the rendered mesh tree contains extras the + * user wouldn't think of as "the thing being dragged" — e.g. an elevator + * whose mesh includes per-level landing assemblies, and the user expects + * the box to wrap just the shaft they're moving. + * + * `size`: `[width, height, depth]` in the node's local frame. + * `centerY`: optional Y center; defaults to `size[1] / 2` (box sits on + * the ground plane). Override when the local origin isn't at the base. + */ + dragBounds?: ( + node: AnyNode, + nodes?: Readonly>, + ) => { size: [number, number, number]; centerY?: number } roofAccessory?: RoofAccessoryConfig /** * Kind cuts a hole in the ceiling surface it is attached to (e.g. recessed diff --git a/packages/core/src/services/alignment.ts b/packages/core/src/services/alignment.ts index 408e64af..1ca9e286 100644 --- a/packages/core/src/services/alignment.ts +++ b/packages/core/src/services/alignment.ts @@ -75,15 +75,108 @@ export type ResolveAlignmentResult = { const EMPTY: ResolveAlignmentResult = { guides: [], snap: null } +/** Forward rotation: local XZ → world XZ for a node whose parent has + * position `bx,_,bz` and rotation-Y `rotY` (radians). Matches the + * transform used throughout the editor's tools / floor-plan. */ +function localToWorld( + x: number, + z: number, + bx: number, + bz: number, + cos: number, + sin: number, +): { x: number; z: number } { + return { + x: bx + x * cos + z * sin, + z: bz - x * sin + z * cos, + } +} + +function transformAnchorToWorld( + anchor: AlignmentAnchor, + bx: number, + bz: number, + cos: number, + sin: number, +): AlignmentAnchor { + const w = localToWorld(anchor.x, anchor.z, bx, bz, cos, sin) + return { nodeId: anchor.nodeId, kind: anchor.kind, x: w.x, z: w.z } +} + +export type BuildingPose = { + position: readonly [number, number, number] + rotationY: number +} + +export type ResolveAlignmentInBuildingResult = { + /** Guides in WORLD coordinates. Renderers must be in a world-space group. */ + guides: AlignmentGuide[] + /** Snap delta in the BUILDING-LOCAL frame, ready to add to a local position. */ + snap: { dx: number; dz: number } | null +} + +/** + * Resolve alignment in WORLD space while accepting BUILDING-LOCAL anchors. + * + * Why this exists: the floor-plan grid lives in world XZ (rendered outside + * the rotated scene group), so alignment must follow the same axes — + * otherwise rotating a building drags the alignment guides off the visible + * grid and onto the rotated wall's local axes (the bug the user hit). The + * resolver itself is frame-agnostic; this wrapper just transforms anchors + * to world, resolves, then rotates the snap delta back into building-local + * so callers can add it to a local position without further math. + * + * `pose === null` → resolve in the caller's frame as-is (no transform). + */ +export function resolveAlignmentInBuildingWorld(input: { + moving: readonly AlignmentAnchor[] + candidates: readonly AlignmentAnchor[] + threshold: number + pose: BuildingPose | null +}): ResolveAlignmentInBuildingResult { + const { moving, candidates, threshold, pose } = input + if (!pose) { + return resolveAlignment({ moving, candidates, threshold }) + } + const cos = Math.cos(pose.rotationY) + const sin = Math.sin(pose.rotationY) + const bx = pose.position[0] + const bz = pose.position[2] + const movingWorld = moving.map((a) => transformAnchorToWorld(a, bx, bz, cos, sin)) + const candidatesWorld = candidates.map((a) => transformAnchorToWorld(a, bx, bz, cos, sin)) + const result = resolveAlignment({ + moving: movingWorld, + candidates: candidatesWorld, + threshold, + }) + if (!result.snap) return { guides: result.guides, snap: null } + // World → local rotation (orthogonal matrix → transpose). The inverse of + // `localToWorld` above maps (dx_world, dz_world) → (dx_local, dz_local). + const dxW = result.snap.dx + const dzW = result.snap.dz + const dxL = dxW * cos - dzW * sin + const dzL = dxW * sin + dzW * cos + return { guides: result.guides, snap: { dx: dxL, dz: dzL } } +} + export function resolveAlignment(input: ResolveAlignmentInput): ResolveAlignmentResult { const { moving, candidates, threshold } = input if (threshold <= 0 || moving.length === 0 || candidates.length === 0) return EMPTY - // Best match per axis: smallest |Δ| on the matched axis (tightest - // alignment), then — crucially — tie-break to the candidate anchor NEAREST - // on the perpendicular axis. Anchors are real points (corners / endpoints / - // midpoints), so the guide always connects to the closest actual point of - // the candidate, never a far one that merely shares the same coordinate. + // Best match per axis: among all candidate anchors within `threshold` of + // the moving anchor on the matched axis, pick the one CLOSEST in the + // perpendicular direction — so the guide always connects to the visually + // nearest actual point of the candidate. Primary delta only breaks perp + // ties. + // + // Why perp-first: a wall pre-rotation contributes anchors that share an + // exact X (vertical wall) or Z (horizontal wall), so primary deltas tie + // and perp picks the nearer endpoint. Post-rotation, the same wall's + // anchors are at slightly-different world coordinates after a float + // rotation — primary deltas differ by tiny amounts and a primary-first + // tie-break would lock onto whichever happens to be marginally tighter, + // often the far endpoint. Perp-first keeps the "closest point of + // reference" behaviour stable through rotation. type Best = { delta: number primary: number @@ -102,13 +195,13 @@ export function resolveAlignment(input: ResolveAlignmentInput): ResolveAlignment const adz = Math.abs(dz) if ( adx <= threshold && - (bestX === null || adx < bestX.primary || (adx === bestX.primary && adz < bestX.perp)) + (bestX === null || adz < bestX.perp || (adz === bestX.perp && adx < bestX.primary)) ) { bestX = { delta: dx, primary: adx, perp: adz, m, c } } if ( adz <= threshold && - (bestZ === null || adz < bestZ.primary || (adz === bestZ.primary && adx < bestZ.perp)) + (bestZ === null || adx < bestZ.perp || (adx === bestZ.perp && adz < bestZ.primary)) ) { bestZ = { delta: dz, primary: adz, perp: adx, m, c } } diff --git a/packages/core/src/services/index.ts b/packages/core/src/services/index.ts index c68e6770..6ee342bc 100644 --- a/packages/core/src/services/index.ts +++ b/packages/core/src/services/index.ts @@ -3,11 +3,14 @@ export { type AlignmentGuide, type AlignmentGuideAxis, type AnchorKind, + type BuildingPose, bboxAnchors, bboxCornerAnchors, + type ResolveAlignmentInBuildingResult, type ResolveAlignmentInput, type ResolveAlignmentResult, resolveAlignment, + resolveAlignmentInBuildingWorld, } from './alignment' export { collectAlignmentAnchors, @@ -56,4 +59,5 @@ export { snapScalar, snapServices, snapVec3ToGrid, + snapWorldXZToBuildingLocal, } from './snap' diff --git a/packages/core/src/services/snap.ts b/packages/core/src/services/snap.ts index 74aa9cd8..faac9c74 100644 --- a/packages/core/src/services/snap.ts +++ b/packages/core/src/services/snap.ts @@ -36,6 +36,53 @@ export function snapVec3ToGrid(point: Vec3, step: number = DEFAULT_GRID_STEP): V return [snapScalar(point[0], step), point[1], snapScalar(point[2], step)] } +/** + * Snap a world XZ point to the grid, then express it in the local frame of + * a building positioned at `buildingPosition` with rotation `buildingRotationY` + * (radians, around the Y axis). Returns both the snapped world point and its + * local-frame equivalent, so callers can render in either frame without + * recomputing the rotation. + * + * Use when a tool needs to keep snapping on the world grid (the grid the + * editor renders) even when the active building is rotated. Snapping in the + * building's local frame would otherwise chase the rotated axes and miss + * the visible grid lines. + */ +export function snapWorldXZToBuildingLocal( + worldX: number, + worldZ: number, + buildingPosition: Vec3, + buildingRotationY: number, + step: number = DEFAULT_GRID_STEP, +): { world: [number, number]; local: [number, number] } { + if (step <= 0) { + const dx = worldX - buildingPosition[0] + const dz = worldZ - buildingPosition[2] + const cos = Math.cos(buildingRotationY) + const sin = Math.sin(buildingRotationY) + return { + world: [worldX, worldZ], + local: [dx * cos - dz * sin, dx * sin + dz * cos], + } + } + const snappedWX = Math.round(worldX / step) * step + const snappedWZ = Math.round(worldZ / step) * step + const dx = snappedWX - buildingPosition[0] + const dz = snappedWZ - buildingPosition[2] + const cos = Math.cos(buildingRotationY) + const sin = Math.sin(buildingRotationY) + // The forward (local → world) rotation used in the editor is + // wx = bx + lx*cos + lz*sin + // wz = bz - lx*sin + lz*cos + // so the inverse (orthogonal, so transpose) is + // lx = dx*cos - dz*sin + // lz = dx*sin + dz*cos + return { + world: [snappedWX, snappedWZ], + local: [dx * cos - dz * sin, dx * sin + dz * cos], + } +} + // ─── Angle snap ─────────────────────────────────────────────────────── /** diff --git a/packages/editor/src/components/editor-2d/floorplan-alignment-guide-layer.tsx b/packages/editor/src/components/editor-2d/floorplan-alignment-guide-layer.tsx index 13016b02..b98d2394 100644 --- a/packages/editor/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +++ b/packages/editor/src/components/editor-2d/floorplan-alignment-guide-layer.tsx @@ -1,27 +1,29 @@ 'use client' -import { useAlignmentGuides } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { memo } from 'react' +import useAlignmentGuides from '../../store/use-alignment-guides' import { formatMeasurement } from '../editor/measurement-pill' import { useFloorplanRender } from './floorplan-render-context' /** * Figma-style alignment guides for the 2D floor plan. * - * Subscribes to `useAlignmentGuides` — populated by - * `FloorplanRegistryMoveOverlay` (Path 2) during a generic free-translate - * drag. Each guide renders as a red line between the moving and matched - * candidate anchors with small `×` end-caps. A distance pill is drawn at - * the line's midpoint when the perpendicular gap is non-zero. + * Subscribes to the editor-local `useAlignmentGuides` store (separate + * from the core store the 3D layer reads). Guides come in + * building-local meters, so the layer is mounted INSIDE the rotated + * `` — the SVG transform that takes the rest + * of the floor-plan geometry from local → screen carries the guide + * lines too. Pill labels are counter-rotated by `sceneRotationDeg` + * (from `FloorplanRenderProvider`) so they stay upright even when the + * scene `` is rotated by building rotation. + * + * Each guide renders as a red line between the moving and matched + * candidate anchors with small `×` end-caps. A distance pill is drawn + * at the midpoint when the perpendicular gap is non-zero. * * Stroke widths and handle radii are scaled by `unitsPerPixel` so they - * stay a constant size on screen no matter the zoom. Text labels are - * counter-rotated by `sceneRotationDeg` so they read upright even when - * the building rotation rotates the scene ``. - * - * Mounted inside the `data-floorplan-scene` group so coordinates match - * world meters 1:1 with the rest of the floor plan. + * stay a constant size on screen no matter the zoom. */ export const FloorplanAlignmentGuideLayer = memo(function FloorplanAlignmentGuideLayer() { const guides = useAlignmentGuides((s) => s.guides) diff --git a/packages/editor/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx b/packages/editor/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx index 64dbf290..731925fa 100644 --- a/packages/editor/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +++ b/packages/editor/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx @@ -76,6 +76,10 @@ export const FloorplanCursorIndicatorOverlay = memo(function FloorplanCursorIndi return { kind: 'icon', icon: 'mdi:trash-can-outline' } } + if (mode === 'material-paint') { + return { kind: 'asset', iconSrc: '/icons/paint.png' } + } + return null }, [activeFloorplanToolConfig, floorplanSelectionTool, mode, structureLayer]) diff --git a/packages/editor/src/components/editor-2d/floorplan-registry-move-overlay.tsx b/packages/editor/src/components/editor-2d/floorplan-registry-move-overlay.tsx index 930df0ef..ee482975 100644 --- a/packages/editor/src/components/editor-2d/floorplan-registry-move-overlay.tsx +++ b/packages/editor/src/components/editor-2d/floorplan-registry-move-overlay.tsx @@ -16,13 +16,13 @@ import { useLiveTransforms, useScene, } from '@pascal-app/core' -import { useAlignmentGuides } from '@pascal-app/editor' import { useViewer } from '@pascal-app/viewer' import { useEffect } from 'react' import { commitFreshPlacementSubtree } from '../../lib/fresh-planar-placement' import { isFreshPlacementMetadata, stripPlacementMetadataFlags } from '../../lib/placement-metadata' import { resolvePlanarCursorPosition } from '../../lib/planar-cursor-placement' import { sfxEmitter } from '../../lib/sfx-bus' +import useAlignmentGuides from '../../store/use-alignment-guides' import useEditor from '../../store/use-editor' import { useWallMoveGhosts } from '../../store/use-wall-move-ghosts' @@ -464,6 +464,11 @@ export function FloorplanRegistryMoveOverlay() { movingLocalBBox.x + movingLocalBBox.width + dxProposed, movingLocalBBox.y + movingLocalBBox.height + dzProposed, ) + // Local-frame resolve (anchors come from the building-local + // SVG `getBBox()`). Guides land in the editor-local alignment + // store, which the 2D FloorplanAlignmentGuideLayer renders + // inside the rotated scene . The 3D pipeline uses a + // separate store, so frames stay isolated per surface. const result = resolveAlignment({ moving: movingAnchors, candidates: candidateAnchors, diff --git a/packages/editor/src/components/editor-2d/renderers/floorplan-registry-layer.tsx b/packages/editor/src/components/editor-2d/renderers/floorplan-registry-layer.tsx index bb5a1ae8..124bcd91 100644 --- a/packages/editor/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +++ b/packages/editor/src/components/editor-2d/renderers/floorplan-registry-layer.tsx @@ -1596,6 +1596,34 @@ function InteractiveGeometry({ ) } + case 'text': { + if (!g.upright) return + // Counter-rotate by the scene rotation so the label reads + // horizontally on screen even when the floor-plan view is + // rotated (default `sceneRotationDeg` is 90°). + return ( + + + {g.text} + + + ) + } default: return ( `). * - * Guide coordinates are XZ meters in the building-local frame; this layer is - * mounted inside ToolManager's building-local group so they render at the - * right world position (and line up with the cursor). The whole ribbon is - * lifted to the active level's building-local Y each frame so it lies on the - * floor being edited — not the building base — when floors are stacked. + * Guide coordinates are XZ meters in the WORLD frame — alignment now resolves + * on the world axes (via `resolveAlignmentForActiveBuilding`) so the guides + * stay parallel to the visible world grid even when the active building is + * rotated. This layer is mounted OUTSIDE the building-local ToolManager group + * for the same reason. The whole ribbon is lifted to the active level's WORLD + * Y each frame so it lies on the floor being edited — not the building base — + * when floors are stacked. */ const LINE_COLOR = 0x81_8c_f8 // indigo-400 — matches the editor's selection accent (box-select / wall highlights) @@ -58,16 +60,17 @@ export const Alignment3DGuideLayer = memo(function Alignment3DGuideLayer() { const unit = useViewer((s) => s.unit) const groupRef = useRef(null) - // Guides carry only XZ (building-local plan coords); their Y has to track - // the active level so the ground ribbon lies on the floor being edited, - // not the building base. Read the level mesh's building-local Y each frame - // — the same source `grid.tsx` uses, so the ribbon stays locked to the - // grid plane (and lerps with it during a level switch). + // Guides carry only XZ in WORLD coords; their Y has to track the active + // level's world Y so the ground ribbon lies on the floor being edited, + // not the building base. `getWorldPosition` walks the level mesh's + // parents (building / site) so it stays correct even if the building has + // a Y offset. + const worldYWork = useMemo(() => new Vector3(), []) useFrame(() => { const group = groupRef.current if (!group) return const levelMesh = levelId ? sceneRegistry.nodes.get(levelId) : null - group.position.y = levelMesh ? levelMesh.position.y : 0 + group.position.y = levelMesh ? levelMesh.getWorldPosition(worldYWork).y : 0 }) if (guides.length === 0) return null diff --git a/packages/editor/src/components/editor/floorplan-panel.tsx b/packages/editor/src/components/editor/floorplan-panel.tsx index 39a8d880..959df800 100644 --- a/packages/editor/src/components/editor/floorplan-panel.tsx +++ b/packages/editor/src/components/editor/floorplan-panel.tsx @@ -77,6 +77,7 @@ import { guideEmitter } from '../../lib/guide-events' import { sfxEmitter } from '../../lib/sfx-bus' import { SITE_BOUNDARY_DRAG_LABEL } from '../../lib/site-boundary' import { cn } from '../../lib/utils' +import { snapBuildingLocalToWorldGrid } from '../../lib/world-grid-snap' import type { GuideUiState, NavigationSyncPose } from '../../store/use-editor' import useEditor, { selectSiteFloorplanContext } from '../../store/use-editor' import { FloorplanAlignmentGuideLayer } from '../editor-2d/floorplan-alignment-guide-layer' @@ -3243,20 +3244,20 @@ const FloorplanGridLayer = memo(function FloorplanGridLayer({ @@ -8831,7 +8832,7 @@ export function FloorplanPanel() { ) const handleWallPlacementPoint = useCallback( - (point: WallPlanPoint) => { + (point: WallPlanPoint, options?: { singleWall?: boolean }) => { if (!draftStart) { setDraftStart(point) setDraftEnd(point) @@ -8859,6 +8860,16 @@ export function FloorplanPanel() { // clearing it (the previous behaviour caused the 2nd-segment // draft to silently break after click 2). const createdWall = createWallOnCurrentLevel(draftStart, point) + + // Alt commits a single wall: drop the draft so the next click + // starts a fresh segment instead of chaining off this endpoint. + if (options?.singleWall) { + setDraftStart(null) + setDraftEnd(null) + setCursorPoint(null) + return + } + const nextStart: WallPlanPoint = createdWall ? [createdWall.end[0], createdWall.end[1]] : point @@ -8934,6 +8945,11 @@ export function FloorplanPanel() { snapWallDraftPoint: snapWallDraftPointMagnetic, toPoint2D, walls, + // World-axis grid snap so drafts land on the visible grid even + // when the active building is rotated. The helper resolves the + // active building's pose internally; this hook stays oblivious to + // building rotation / position. + worldGridSnap: snapBuildingLocalToWorldGrid, }) const handleBackgroundClick = useCallback( diff --git a/packages/editor/src/components/editor/grid.tsx b/packages/editor/src/components/editor/grid.tsx index d413bfa2..347603f9 100644 --- a/packages/editor/src/components/editor/grid.tsx +++ b/packages/editor/src/components/editor/grid.tsx @@ -1,10 +1,10 @@ 'use client' -import { type AnyNodeId, emitter, type GridEvent, sceneRegistry } from '@pascal-app/core' +import { emitter, type GridEvent, sceneRegistry } from '@pascal-app/core' import { GRID_LAYER, getSceneTheme, useViewer } from '@pascal-app/viewer' import { useFrame } from '@react-three/fiber' import { useEffect, useMemo, useRef, useState } from 'react' -import { MathUtils, type Mesh, PlaneGeometry, Vector2, Vector3 } from 'three' +import { MathUtils, type Mesh, PlaneGeometry, Vector2 } from 'three' import { color, float, fract, fwidth, mix, positionLocal, uniform } from 'three/tsl' import { MeshBasicNodeMaterial } from 'three/webgpu' import { useCeilingEvents } from '../../hooks/use-ceiling-events' @@ -143,23 +143,11 @@ export const Grid = ({ } }, []) - const worldPosScratch = useMemo(() => new Vector3(), []) useFrame((_, delta) => { - const { levelId, buildingId } = useViewer.getState().selection - // Align the grid's XZ origin to the active building so its visible cell - // lines pass through building-local snap points (walls snap in - // building-local coords; a building placed at world (0.25, 0.25) would - // otherwise leave snapped wall endpoints stranded between grid lines). - let targetX = 0 - let targetZ = 0 - if (buildingId) { - const buildingMesh = sceneRegistry.nodes.get(buildingId as AnyNodeId) - if (buildingMesh) { - buildingMesh.getWorldPosition(worldPosScratch) - targetX = worldPosScratch.x - targetZ = worldPosScratch.z - } - } + const { levelId } = useViewer.getState().selection + // Grid stays anchored to world XZ (0, 0) — never chases the active + // building. The Y origin still lerps to the active level so the grid + // sits at floor height when a level is open. let targetY = 0 if (levelId) { const levelMesh = sceneRegistry.nodes.get(levelId) @@ -167,22 +155,16 @@ export const Grid = ({ targetY = levelMesh.position.y } } - const t = 12 * delta - gridRef.current.position.x = MathUtils.lerp(gridRef.current.position.x, targetX, t) - gridRef.current.position.z = MathUtils.lerp(gridRef.current.position.z, targetZ, t) - const newY = MathUtils.lerp(gridRef.current.position.y, targetY, t) + const newY = MathUtils.lerp(gridRef.current.position.y, targetY, 12 * delta) gridRef.current.position.y = newY setGridY(newY) - // Re-derive the local-frame cursor uniform after the grid's XZ has - // lerped this frame, so the reveal ring stays locked under the world - // cursor even when the grid origin is mid-transition. + // Grid XZ is fixed at world origin, so the local-frame cursor uniform + // is just the world cursor (mirrored on Z to match the -π/2 X-rotation + // of the plane). const world = lastWorldCursorRef.current if (world) { - cursorPositionRef.current.set( - world.x - gridRef.current.position.x, - -(world.z - gridRef.current.position.z), - ) + cursorPositionRef.current.set(world.x, -world.z) } }) diff --git a/packages/editor/src/components/editor/index.tsx b/packages/editor/src/components/editor/index.tsx index 444dae9a..88e96497 100644 --- a/packages/editor/src/components/editor/index.tsx +++ b/packages/editor/src/components/editor/index.tsx @@ -8,15 +8,7 @@ import { useScene, } from '@pascal-app/core' import { type HoverStyles, InteractiveSystem, useViewer, Viewer } from '@pascal-app/viewer' -import { - memo, - type ReactNode, - useCallback, - useEffect, - useLayoutEffect, - useRef, - useState, -} from 'react' +import { memo, type ReactNode, useCallback, useEffect, useRef, useState } from 'react' import { ViewerOverlay } from '../../components/viewer-overlay' import { ViewerZoneSystem } from '../../components/viewer-zone-system' import { type SaveStatus, useAutoSave } from '../../hooks/use-auto-save' @@ -76,7 +68,7 @@ const CAMERA_CONTROLS_HINT_DISMISSED_STORAGE_KEY = 'editor-camera-controls-hint- const DELETE_CURSOR_BADGE_COLOR = '#ef4444' const DELETE_CURSOR_BADGE_OFFSET_X = 14 const DELETE_CURSOR_BADGE_OFFSET_Y = 14 -const PAINT_CURSOR_BADGE_COLOR = '#f59e0b' +const PAINT_CURSOR_BADGE_COLOR = '#818cf8' const PAINT_CURSOR_BADGE_DISABLED_COLOR = '#94a3b8' const PAINT_CURSOR_BADGE_OFFSET_X = 14 const PAINT_CURSOR_BADGE_OFFSET_Y = 14 @@ -536,43 +528,46 @@ function DeleteCursorBadge({ position }: { position: { x: number; y: number } }) function PaintCursorBadge({ position, - label, disabled, - icon, }: { position: { x: number; y: number } - label: string disabled: boolean - icon: string }) { const accentColor = disabled ? PAINT_CURSOR_BADGE_DISABLED_COLOR : PAINT_CURSOR_BADGE_COLOR + const lineHeight = 18 return (