editor: complete floorplan construction documentation (#531)
* Add roof surface placement support for items Items (e.g. solar panels) can now be placed on sloped roof surfaces. The placement system computes euler rotation from the roof surface normal so items sit flush on the slope instead of going inside. - Add roofStrategy to placement-strategies with enter/move/click/leave - Wire roof:enter/move/click/leave events in the placement coordinator - Add calculateRoofRotation in placement-math using surface normals - Support full 3D cursor rotation for sloped surfaces - Items on roofs are parented to the level with world-space rotation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fixed conflict * feat(floorplan): add construction dimension strings * feat(floorplan): coordinate opening dimensions * feat(floorplan): add opening documentation * feat(floorplan): add construction dimensions and notes * feat(floorplan): add interior dimensions and curved note leaders * feat(floorplan): improve construction dimensions and document plan * feat(floorplan): harden construction document output * feat(floorplan): add annotation collision diagnostics * feat(floorplan): size export annotations in paper space * feat(floorplan): automatically separate overlapping labels * fix(floorplan): resolve dense label overlaps * fix(floorplan): remove stale collision warning overlays * fix(floorplan): treat mark pills as collision obstacles * feat(floorplan): place short dimension values outside * fix(floorplan): preserve dimension string order * fix(floorplan): avoid architectural geometry in label layout * feat(floorplan): add dimension side fallback leaders * docs(floorplan): update chapter 17 implementation status * fix(floorplan): dimension subdivided interior walls * feat: add associative floor plan dimensions * feat: add continuous construction dimension strings * feat: add structural floor plan grids * feat: coordinate columns with structural grids Snap column placement and movement to structural axes and intersections, derive associative grid references, and preserve floor-plan rotation by allowing secondary-button pointer moves through the grid drafting layer. * feat: add architectural room documentation Add room-role metadata, editable documentation fields, centered room labels, and persisted live/PDF visibility while preserving generic zone behavior. * feat: generate architectural room schedules Add registry-driven room schedule rows with unit-aware areas and heights, natural room ordering, enclosure resolution, and document-quality warnings. * feat: add reliable room clear dimensions Derive unit-aware clear dimensions from proven modeled inside wall faces for straight rectangular rooms, including rotated and split-wall enclosures, while suppressing unproven datums. * feat: add architectural stair documentation Add level-aware UP/DN graphics, derived flight and rail notes, plan break and overhead conventions, linked destination-level projection, and persisted live/PDF visibility. * feat: add typed specialty construction notes Add schema-validated specialty payloads, standardized plan notation, contract-scope metadata, configurable overhead outlines, and editor authoring controls. * feat: add curved and circular dimensions Add associative radius, diameter, center, chord, arc-length, angular, and coordinate modes with unit-aware notation, repeated-feature labels, 2D authoring, and document controls. * feat: coordinate floor plan drawing types Add persistent floor, foundation, reflected-ceiling, roof, and site plan views with per-dimension show, omit, reference, and foundation-controller behavior across live and PDF output. * feat: add associative curved wall dimensions Bind radius, center, chord, arc-length, and angular construction dimensions directly to curved wall geometry so annotations update when the host curve changes. * fix: render automatic curved wall dimensions The wall floor-plan builder explicitly skipped curved walls, leaving the associative authoring workflow as the only dimension path. Render a concentric arc-length dimension automatically and keep it governed by automatic-dimension visibility. * fix: use radius callout for curved walls Replace the automatic arc-length annotation with the source-standard radius method: computed center mark, radial leader, curve arrow, and R value. Keep adjacent linear strings responsible for locating the curve tangencies and depth. * Implement construction dimension string editing * Add construction dimension standards controls * Apply drawing standards to automatic dimensions * Add floorplan overhead and reference visibility controls * Add view-specific dimension segment suppression * Add persistent drawing sheet model * Plot floorplan exports at fixed scale * Apply paper-space annotation profiles * Compose floorplan PDF sheets * Support sheet paper sizes and preflight * Persist pinned annotation layout overrides * Expand annotation collision obstacles * Add floorplan annotation preflight surface * Add reusable drawing sheet general notes * Add drawing sheet keyed note instances * Add drawing sheet document markers * Expand construction note leader terminators * Add wall assembly layer model * Resolve wall assembly datum references * Add wall assembly floorplan graphics * Add opening documentation dimension policies * Add finish-face room clear dimensions * Extend room clear dimensions to rectilinear rooms * Add construction module advisories * Add clearance advisory profiles * Add dimension completeness audit * Expand dimension completeness audit * Include preflight issues in completeness audit * feat: complete floorplan construction documentation * refactor: remove construction note node * feat: refine floorplan documentation and unit display * fix(editor): improve floorplan PDF dimensions * fix(floorplan): refresh annotation collision layout * fix(floorplan): keep annotations clear and restore registry boundaries * feat(floorplan): refine construction dimension references * fix(floorplan): align documentation tools with architecture --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
2adb50a340
commit
77442861d9
@@ -5,6 +5,7 @@ import { Hammer, Layers, Package, Settings } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { BuildTab } from '@/components/build-tab'
|
||||
import { FloorplanConstructionPreflight } from '@/components/floorplan-construction-preflight'
|
||||
import {
|
||||
CommunityViewerToolbarLeft,
|
||||
CommunityViewerToolbarRight,
|
||||
@@ -89,6 +90,7 @@ const PROJECT_ID = 'local-editor'
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="relative h-screen w-screen">
|
||||
<FloorplanConstructionPreflight />
|
||||
{PROJECT_ID === 'local-editor' && (
|
||||
<div className="pointer-events-none absolute top-3 left-1/2 z-40 -translate-x-1/2">
|
||||
<div className="pointer-events-auto flex items-center gap-3 rounded-full border border-border/60 bg-background/90 px-4 py-1.5 text-xs shadow-sm backdrop-blur">
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { nodeRegistry } from '@pascal-app/core'
|
||||
import { MaterialPaintPanel, triggerSFX, useEditor } from '@pascal-app/editor'
|
||||
import {
|
||||
getFloorplanNodeExtension,
|
||||
MaterialPaintPanel,
|
||||
triggerSFX,
|
||||
useEditor,
|
||||
} from '@pascal-app/editor'
|
||||
import { useLiquidLineToolOptions } from '@pascal-app/nodes'
|
||||
import Image from 'next/image'
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
@@ -13,24 +18,6 @@ import {
|
||||
} from '@/components/toolbar-tooltip'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
/**
|
||||
* Raw structure-tool kinds the Build tab can activate. These map 1:1 to the
|
||||
* editor's `StructureTool` ids.
|
||||
*/
|
||||
type BuildToolKind =
|
||||
| 'wall'
|
||||
| 'fence'
|
||||
| 'slab'
|
||||
| 'ceiling'
|
||||
| 'roof'
|
||||
| 'stair'
|
||||
| 'elevator'
|
||||
| 'door'
|
||||
| 'window'
|
||||
| 'column'
|
||||
| 'shelf'
|
||||
| 'spawn'
|
||||
|
||||
/**
|
||||
* MEP (mechanical / plumbing) tool kinds surfaced under the Build tab's "MEP"
|
||||
* group tile — its own sub-grid, like Roof's "Features".
|
||||
@@ -53,7 +40,8 @@ type BuildType = {
|
||||
/** Raster asset tile (legacy Build sidebar artwork). */
|
||||
iconSrc: string
|
||||
/** Present for structure-tool types (absent for paint mode and the MEP group). */
|
||||
kind?: BuildToolKind
|
||||
kind?: string
|
||||
paletteOrder?: number
|
||||
/** Non-placement special mode. */
|
||||
mode?: 'material-paint'
|
||||
}
|
||||
@@ -67,7 +55,7 @@ type MepItem = {
|
||||
}
|
||||
|
||||
// Same icons + ordering as the community Build sidebar, minus presets.
|
||||
const BUILD_TYPES: BuildType[] = [
|
||||
const BASE_BUILD_TYPES: BuildType[] = [
|
||||
{ id: 'wall', label: 'Wall', iconSrc: '/icons/wall.webp', kind: 'wall' },
|
||||
{ id: 'fence', label: 'Fence', iconSrc: '/icons/fence.webp', kind: 'fence' },
|
||||
{ id: 'slab', label: 'Slab', iconSrc: '/icons/floor.webp', kind: 'slab' },
|
||||
@@ -85,6 +73,37 @@ const BUILD_TYPES: BuildType[] = [
|
||||
{ id: 'painting', label: 'Painting', iconSrc: '/icons/paint.webp', mode: 'material-paint' },
|
||||
]
|
||||
|
||||
function collectBuildTypes(): BuildType[] {
|
||||
const baseKinds = new Set(BASE_BUILD_TYPES.flatMap((type) => (type.kind ? [type.kind] : [])))
|
||||
const tools = BASE_BUILD_TYPES.filter((type) => type.kind).map((type, index) => ({
|
||||
...type,
|
||||
paletteOrder:
|
||||
nodeRegistry.get(type.kind!)?.presentation?.paletteOrder ?? type.paletteOrder ?? index * 10,
|
||||
}))
|
||||
for (const [kind, definition] of nodeRegistry.entries()) {
|
||||
const presentation = definition.presentation
|
||||
const extension = getFloorplanNodeExtension(definition)
|
||||
if (
|
||||
baseKinds.has(kind) ||
|
||||
!extension?.tool ||
|
||||
!presentation ||
|
||||
presentation.hidden ||
|
||||
presentation.paletteSection !== 'structure'
|
||||
) {
|
||||
continue
|
||||
}
|
||||
tools.push({
|
||||
id: kind,
|
||||
kind,
|
||||
label: presentation.label,
|
||||
iconSrc: presentation.icon.kind === 'url' ? presentation.icon.src : '/icons/spawn-point.webp',
|
||||
paletteOrder: presentation.paletteOrder ?? Number.MAX_SAFE_INTEGER,
|
||||
})
|
||||
}
|
||||
tools.sort((left, right) => (left.paletteOrder ?? 0) - (right.paletteOrder ?? 0))
|
||||
return [...tools, ...BASE_BUILD_TYPES.filter((type) => !type.kind)]
|
||||
}
|
||||
|
||||
// MEP sub-grid surfaced under the "MEP" tile — same icons + ordering the MEP
|
||||
// tools had in the community Build sidebar.
|
||||
const MEP_ITEMS: MepItem[] = [
|
||||
@@ -105,8 +124,10 @@ const MEP_ITEMS: MepItem[] = [
|
||||
* Activate a raw structure draw/cursor tool. Mirrors the editor's own
|
||||
* structure-tool activation (`setPhase`/`setStructureLayer`/`setMode`/`setTool`).
|
||||
*/
|
||||
function activateBuildTool(kind: BuildToolKind | MepToolKind): void {
|
||||
function activateBuildTool(kind: string): void {
|
||||
const ed = useEditor.getState()
|
||||
const preferredView = getFloorplanNodeExtension(nodeRegistry.get(kind))?.preferredView
|
||||
if (preferredView) ed.setViewMode(preferredView)
|
||||
ed.setPhase('structure')
|
||||
ed.setStructureLayer('elements')
|
||||
ed.setCatalogCategory(null)
|
||||
@@ -142,7 +163,7 @@ function activateRoofFeatureTool(kind: string): void {
|
||||
ed.setStructureLayer('elements')
|
||||
ed.setCatalogCategory(null)
|
||||
ed.setMode('build')
|
||||
ed.setTool(kind as Parameters<typeof ed.setTool>[0])
|
||||
ed.setTool(kind)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,6 +186,7 @@ export function BuildTab() {
|
||||
const mode = useEditor((s) => s.mode)
|
||||
const follow = useLiquidLineToolOptions((s) => s.follow)
|
||||
const toggleFollow = useLiquidLineToolOptions((s) => s.toggleFollow)
|
||||
const buildTypes = useMemo(collectBuildTypes, [])
|
||||
|
||||
// The fitting / follow tools are armed from a segment's panel, not a grid
|
||||
// tile — keep the segment tile lit so the panel (and the way back) stays
|
||||
@@ -245,9 +267,9 @@ export function BuildTab() {
|
||||
didInitRef.current = true
|
||||
const ed = useEditor.getState()
|
||||
if (ed.mode === 'build' && ed.tool) return
|
||||
const firstType = BUILD_TYPES.find((t) => t.kind)
|
||||
const firstType = buildTypes.find((t) => t.kind)
|
||||
if (firstType) handleTypeClick(firstType)
|
||||
}, [handleTypeClick])
|
||||
}, [buildTypes, handleTypeClick])
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-3 p-3">
|
||||
@@ -256,7 +278,7 @@ export function BuildTab() {
|
||||
className="grid gap-1.5"
|
||||
style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(56px, 1fr))' }}
|
||||
>
|
||||
{BUILD_TYPES.map((type) => {
|
||||
{buildTypes.map((type) => {
|
||||
const active = isTypeActive(type)
|
||||
return (
|
||||
<Tooltip key={type.id}>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
'use client'
|
||||
|
||||
import { useScene } from '@pascal-app/core'
|
||||
import { useFloorplanPreflight } from '@pascal-app/editor'
|
||||
import {
|
||||
buildClearanceAdvisories,
|
||||
buildConstructionModuleAdvisories,
|
||||
buildDimensionCompletenessAudit,
|
||||
} from '@pascal-app/nodes'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
export function FloorplanConstructionPreflight() {
|
||||
const nodes = useDebouncedSceneNodes()
|
||||
const clearanceChecksEnabled = useFloorplanPreflight((state) => state.clearanceChecksEnabled)
|
||||
const moduleChecksEnabled = useFloorplanPreflight((state) => state.moduleChecksEnabled)
|
||||
const setAuditIssues = useFloorplanPreflight((state) => state.setAuditIssues)
|
||||
|
||||
const issues = useMemo(() => {
|
||||
const completeness = buildDimensionCompletenessAudit(nodes, {
|
||||
includeAutomaticDimensions: true,
|
||||
}).map((issue) => ({
|
||||
id: issue.id,
|
||||
kind: 'dimension-completeness' as const,
|
||||
severity: issue.severity,
|
||||
message: issue.message,
|
||||
}))
|
||||
const clearance = clearanceChecksEnabled
|
||||
? buildClearanceAdvisories(nodes, { includeDisabled: true }).map((issue) => ({
|
||||
id: issue.id,
|
||||
kind: 'clearance-advisory' as const,
|
||||
severity: issue.severity,
|
||||
message: issue.message,
|
||||
}))
|
||||
: []
|
||||
const modules = moduleChecksEnabled
|
||||
? buildConstructionModuleAdvisories(nodes, { includeDisabled: true }).map((issue) => ({
|
||||
id: issue.id,
|
||||
kind: 'module-advisory' as const,
|
||||
severity: issue.severity,
|
||||
message: issue.message,
|
||||
}))
|
||||
: []
|
||||
return [...completeness, ...clearance, ...modules]
|
||||
}, [clearanceChecksEnabled, moduleChecksEnabled, nodes])
|
||||
|
||||
useEffect(() => {
|
||||
setAuditIssues(issues)
|
||||
return () => setAuditIssues([])
|
||||
}, [issues, setAuditIssues])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function useDebouncedSceneNodes() {
|
||||
const [nodes, setNodes] = useState(() => useScene.getState().nodes)
|
||||
|
||||
useEffect(() => {
|
||||
let pending: ReturnType<typeof setTimeout> | undefined
|
||||
const unsubscribe = useScene.subscribe((state) => {
|
||||
if (pending) clearTimeout(pending)
|
||||
pending = setTimeout(() => setNodes(state.nodes), 100)
|
||||
})
|
||||
return () => {
|
||||
if (pending) clearTimeout(pending)
|
||||
unsubscribe()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return nodes
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { Icon as IconifyIcon } from '@iconify/react'
|
||||
import {
|
||||
DRAWING_TYPE_OPTIONS,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
@@ -10,7 +11,9 @@ import {
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
useDrawingView,
|
||||
useEditor,
|
||||
useFloorplanAnnotationVisibility,
|
||||
useSidebarStore,
|
||||
type ViewMode,
|
||||
} from '@pascal-app/editor'
|
||||
@@ -32,12 +35,16 @@ import {
|
||||
EyeOff,
|
||||
Footprints,
|
||||
Grid2X2,
|
||||
Layers3,
|
||||
Magnet,
|
||||
PenLine,
|
||||
Ruler,
|
||||
ScanLine,
|
||||
SlidersHorizontal,
|
||||
Sparkles,
|
||||
SquareUserRound,
|
||||
SwatchBook,
|
||||
Tag,
|
||||
} from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import { type ReactNode, useCallback } from 'react'
|
||||
@@ -133,6 +140,22 @@ const SHADING_OPTIONS = [
|
||||
{ id: 'rendered', name: 'Rendered', detail: 'Full ambient occlusion', icon: Sparkles },
|
||||
] as const
|
||||
|
||||
const FLOORPLAN_ANNOTATION_OPTIONS = [
|
||||
{ id: 'automaticDimensions', name: 'Automatic dimensions', icon: Ruler },
|
||||
{ id: 'manualDimensions', name: 'Manual dimensions', icon: Ruler },
|
||||
{ id: 'measurements', name: 'Measurements', icon: ScanLine },
|
||||
{ id: 'openingMarks', name: 'Door/window marks', icon: Tag },
|
||||
{ id: 'structuralGrids', name: 'Structural grids & column centers', icon: Grid2X2 },
|
||||
{ id: 'roomLabels', name: 'Room labels', icon: SquareUserRound },
|
||||
{ id: 'stairAnnotations', name: 'Stair annotations', icon: Footprints },
|
||||
] as const
|
||||
|
||||
const FLOORPLAN_WALL_DIMENSION_REFERENCE_OPTIONS = [
|
||||
{ id: 'finished-faces', name: 'Finished faces', detail: 'Full wall thickness' },
|
||||
{ id: 'centerline', name: 'Wall centerline', detail: 'Single wall axis' },
|
||||
{ id: 'stud-faces', name: 'Face of stud', detail: 'Structural core face' },
|
||||
] as const
|
||||
|
||||
function ViewModeControl() {
|
||||
const viewMode = useEditor((state) => state.viewMode)
|
||||
const setViewMode = useEditor((state) => state.setViewMode)
|
||||
@@ -165,6 +188,49 @@ function ViewModeControl() {
|
||||
)
|
||||
}
|
||||
|
||||
function DrawingTypeControl() {
|
||||
const viewMode = useEditor((state) => state.viewMode)
|
||||
const drawingType = useDrawingView((state) => state.drawingType)
|
||||
const setDrawingType = useDrawingView((state) => state.setDrawingType)
|
||||
if (viewMode === '3d') return null
|
||||
|
||||
const active =
|
||||
DRAWING_TYPE_OPTIONS.find((option) => option.id === drawingType) ?? DRAWING_TYPE_OPTIONS[0]
|
||||
|
||||
return (
|
||||
<div className={TOOLBAR_CONTAINER}>
|
||||
<DropdownMenu>
|
||||
<ToolbarTooltip label="Select coordinated drawing">
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
aria-label={`Drawing type: ${active.label}`}
|
||||
className="flex items-center gap-1.5 px-2.5 font-medium text-foreground/90 text-xs transition-colors hover:bg-white/8"
|
||||
type="button"
|
||||
>
|
||||
<Layers3 className="h-3.5 w-3.5" />
|
||||
<span>{active.label}</span>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
</ToolbarTooltip>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
className="w-56 rounded-xl border-border/45 bg-popover/95 backdrop-blur-xl"
|
||||
side="bottom"
|
||||
sideOffset={8}
|
||||
>
|
||||
{DRAWING_TYPE_OPTIONS.map((option) => (
|
||||
<DropdownMenuItem key={option.id} onSelect={() => setDrawingType(option.id)}>
|
||||
<Layers3 className="h-4 w-4" />
|
||||
<span>{option.label}</span>
|
||||
{drawingType === option.id ? <Check className="ml-auto h-4 w-4" /> : null}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CollapseSidebarButton() {
|
||||
const isCollapsed = useSidebarStore((state) => state.isCollapsed)
|
||||
const setIsCollapsed = useSidebarStore((state) => state.setIsCollapsed)
|
||||
@@ -278,12 +344,15 @@ const EDGE_OPTIONS = [
|
||||
const SUBMENU_CONTENT_CLASS = 'min-w-56 rounded-xl border-border/45 bg-popover/95 backdrop-blur-xl'
|
||||
|
||||
function DisplayMenu() {
|
||||
const viewMode = useEditor((state) => state.viewMode)
|
||||
const showGrid = useViewer((state) => state.showGrid)
|
||||
const setShowGrid = useViewer((state) => state.setShowGrid)
|
||||
const showMeasurements = useViewer((state) => state.showMeasurements)
|
||||
const setShowMeasurements = useViewer((state) => state.setShowMeasurements)
|
||||
const unit = useViewer((state) => state.unit)
|
||||
const setUnit = useViewer((state) => state.setUnit)
|
||||
const metricNotation = useViewer((state) => state.metricNotation)
|
||||
const setMetricNotation = useViewer((state) => state.setMetricNotation)
|
||||
const cameraMode = useViewer((state) => state.cameraMode)
|
||||
const setCameraMode = useViewer((state) => state.setCameraMode)
|
||||
const shading = useViewer((state) => state.shading)
|
||||
@@ -296,6 +365,14 @@ function DisplayMenu() {
|
||||
const setShadows = useViewer((state) => state.setShadows)
|
||||
const magneticSnap = useEditor((state) => state.magneticSnap)
|
||||
const setMagneticSnap = useEditor((state) => state.setMagneticSnap)
|
||||
const annotationVisibility = useFloorplanAnnotationVisibility((state) => state.visibility)
|
||||
const setAnnotationCategory = useFloorplanAnnotationVisibility((state) => state.setCategory)
|
||||
const wallDimensionReference = useFloorplanAnnotationVisibility(
|
||||
(state) => state.wallDimensionReference,
|
||||
)
|
||||
const setWallDimensionReference = useFloorplanAnnotationVisibility(
|
||||
(state) => state.setWallDimensionReference,
|
||||
)
|
||||
|
||||
const activeShading =
|
||||
SHADING_OPTIONS.find((option) => option.id === shading) ?? SHADING_OPTIONS[0]
|
||||
@@ -337,17 +414,82 @@ function DisplayMenu() {
|
||||
<EyeOff className="ml-auto h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => keepOpen(e, () => setShowMeasurements(!showMeasurements))}
|
||||
>
|
||||
<Ruler className="h-4 w-4" />
|
||||
<span>Measurements</span>
|
||||
{showMeasurements ? (
|
||||
<Eye className="ml-auto h-4 w-4 text-foreground" />
|
||||
) : (
|
||||
<EyeOff className="ml-auto h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
{viewMode !== '2d' ? (
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => keepOpen(e, () => setShowMeasurements(!showMeasurements))}
|
||||
>
|
||||
<Ruler className="h-4 w-4" />
|
||||
<span>{viewMode === 'split' ? '3D measurements' : 'Measurements'}</span>
|
||||
{showMeasurements ? (
|
||||
<Eye className="ml-auto h-4 w-4 text-foreground" />
|
||||
) : (
|
||||
<EyeOff className="ml-auto h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{viewMode !== '3d' ? (
|
||||
<>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Layers3 className="h-4 w-4" />
|
||||
<span>Floor plan annotations</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className={SUBMENU_CONTENT_CLASS}>
|
||||
{FLOORPLAN_ANNOTATION_OPTIONS.map((option) => {
|
||||
const OptionIcon = option.icon
|
||||
const visible = annotationVisibility[option.id]
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={option.id}
|
||||
onSelect={(e) =>
|
||||
keepOpen(e, () => setAnnotationCategory(option.id, !visible))
|
||||
}
|
||||
>
|
||||
<OptionIcon className="h-4 w-4" />
|
||||
<span>{option.name}</span>
|
||||
{visible ? (
|
||||
<Eye className="ml-auto h-4 w-4 text-foreground" />
|
||||
) : (
|
||||
<EyeOff className="ml-auto h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
})}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Ruler className="h-4 w-4" />
|
||||
<span>Wall dimensions</span>
|
||||
<span className="ml-auto text-muted-foreground text-xs">
|
||||
{
|
||||
FLOORPLAN_WALL_DIMENSION_REFERENCE_OPTIONS.find(
|
||||
(option) => option.id === wallDimensionReference,
|
||||
)?.name
|
||||
}
|
||||
</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className={SUBMENU_CONTENT_CLASS}>
|
||||
{FLOORPLAN_WALL_DIMENSION_REFERENCE_OPTIONS.map((option) => (
|
||||
<DropdownMenuItem
|
||||
key={option.id}
|
||||
onSelect={(event) =>
|
||||
keepOpen(event, () => setWallDimensionReference(option.id))
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-foreground">{option.name}</span>
|
||||
<span className="text-muted-foreground text-xs">{option.detail}</span>
|
||||
</div>
|
||||
{wallDimensionReference === option.id ? (
|
||||
<Check className="ml-auto h-4 w-4 text-foreground" />
|
||||
) : null}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
</>
|
||||
) : null}
|
||||
<DropdownMenuItem onSelect={(e) => keepOpen(e, () => setMagneticSnap(!magneticSnap))}>
|
||||
<Magnet className="h-4 w-4" />
|
||||
<span>Magnetic snap</span>
|
||||
@@ -377,17 +519,48 @@ function DisplayMenu() {
|
||||
{cameraMode === 'perspective' ? 'Perspective' : 'Orthographic'}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => keepOpen(e, () => setUnit(unit === 'metric' ? 'imperial' : 'metric'))}
|
||||
>
|
||||
<span className="flex h-4 w-4 items-center justify-center font-semibold text-[10px]">
|
||||
{unit === 'metric' ? 'm' : 'ft'}
|
||||
</span>
|
||||
<span>Units</span>
|
||||
<span className="ml-auto text-muted-foreground text-xs">
|
||||
{unit === 'metric' ? 'Metric' : 'Imperial'}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<span className="flex h-4 w-4 items-center justify-center font-semibold text-[10px]">
|
||||
{unit === 'imperial' ? 'ft' : metricNotation === 'millimeters' ? 'mm' : 'm'}
|
||||
</span>
|
||||
<span>Units</span>
|
||||
<span className="ml-auto text-muted-foreground text-xs">
|
||||
{unit === 'imperial'
|
||||
? 'Feet & inches'
|
||||
: metricNotation === 'millimeters'
|
||||
? 'Millimeters'
|
||||
: 'Meters'}
|
||||
</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className={SUBMENU_CONTENT_CLASS}>
|
||||
<DropdownMenuItem onSelect={() => setMetricNotation('meters')}>
|
||||
<span className="flex h-4 w-4 items-center justify-center font-semibold text-[10px]">
|
||||
m
|
||||
</span>
|
||||
<span>Meters</span>
|
||||
{unit === 'metric' && metricNotation === 'meters' ? (
|
||||
<Check className="ml-auto h-4 w-4 text-foreground" />
|
||||
) : null}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => setMetricNotation('millimeters')}>
|
||||
<span className="flex h-4 w-4 items-center justify-center font-semibold text-[10px]">
|
||||
mm
|
||||
</span>
|
||||
<span>Millimeters</span>
|
||||
{unit === 'metric' && metricNotation === 'millimeters' ? (
|
||||
<Check className="ml-auto h-4 w-4 text-foreground" />
|
||||
) : null}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => setUnit('imperial')}>
|
||||
<span className="flex h-4 w-4 items-center justify-center font-semibold text-[10px]">
|
||||
ft
|
||||
</span>
|
||||
<span>Feet & inches</span>
|
||||
{unit === 'imperial' ? <Check className="ml-auto h-4 w-4 text-foreground" /> : null}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
@@ -521,6 +694,7 @@ export function CommunityViewerToolbarLeft() {
|
||||
<>
|
||||
<CollapseSidebarButton />
|
||||
<ViewModeControl />
|
||||
<DrawingTypeControl />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
Reference in New Issue
Block a user