Files
ab76686b8b
CI / quality (push) Has been cancelled
mcp-ci / ci (push) Has been cancelled
editor: improve floorplan modes and annotations (#549)
* 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

* fix(editor): anchor floorplan cursor to snapped point

* fix(nodes): preview floorplan edits through live overrides

* feat(editor): add context-aware floorplan modes

* fix(nodes): render crisp wall selection hatching

* fix(editor): cap floorplan handles at extreme zoom

* fix(editor): keep zone labels upright after rotation

* refactor(editor): make referenced annotations registry-driven

* refactor(nodes): colocate contextual dimension builders

* fix(editor): use mode-driven angle snapping

* fix(nodes): use mode-driven move snapping

* chore(editor): update react scan tooling

* refactor(floorplan): streamline construction documentation

* refactor(floorplan): remove wall assembly roadmap

* fix(floorplan): migrate retired scene data

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-27 18:57:59 -04:00

107 lines
3.6 KiB
TypeScript

import {
type NodeDefinition,
resolveAutoZonePolygon,
ZoneNode as ZoneNodeSchema,
} from '@pascal-app/core'
import type { FloorplanNodeExtension } from '@pascal-app/editor'
import { polygonMeasurementFeatures } from '../shared/polygon-measurement'
import { buildZoneContextualDimensions } from './contextual-dimensions'
import { buildZoneFloorplan } from './floorplan'
import {
zoneAddVertexAffordance,
zoneDeleteVertexAffordance,
zoneMoveEdgeAffordance,
zoneMoveVertexAffordance,
} from './floorplan-affordances'
import { zoneFloorplanMoveTarget } from './floorplan-move'
import { zoneParametrics } from './parametrics'
import { zoneQuickMeasurement } from './quick-measurement'
import { buildRoomFloorplanSchedule } from './room-documentation'
import { ZoneNode } from './schema'
/**
* Zone — Stage A. Custom-behavior escape hatch: zone uses TSL shader
* materials + `<Html>` portals + per-frame uniform poking, so it
* lives via `def.renderer` + `def.system` (no `def.geometry` possible
* because zone isn't really a mesh).
*/
export const zoneDefinition: NodeDefinition<typeof ZoneNode> = {
kind: 'zone',
snapProfile: 'structural',
schemaVersion: 2,
schema: ZoneNode,
category: 'site',
extensions: {
'pascal:editor/floorplan': {
contextualDimensions: buildZoneContextualDimensions,
schedule: buildRoomFloorplanSchedule,
} satisfies FloorplanNodeExtension<ZoneNode>,
},
defaults: () => {
const stub = ZoneNodeSchema.parse({ id: 'zone_default' as never, type: 'zone' })
const { id: _id, type: _type, ...rest } = stub
return rest
},
capabilities: {
selectable: { hitVolume: 'bbox' },
duplicable: true,
deletable: true,
// Zones describe regions of a site — they don't translate as
// reusable presets independent of their site context.
presettable: false,
},
parametrics: zoneParametrics,
measurement: {
features: (node, ctx) =>
polygonMeasurementFeatures({
featurePrefix: 'zone',
height: 0,
label: 'Zone',
polygon: resolveAutoZonePolygon(node, ctx.resolve),
}),
quickMeasure: (node, ctx) => zoneQuickMeasurement(node, ctx),
},
// No dirty consumer rebuilds this kind — see NodeDefinition.dirtyTracking.
dirtyTracking: false,
renderer: {
kind: 'parametric',
module: () => import('./renderer'),
},
system: {
module: () => import('./system'),
priority: 4,
},
floorplan: buildZoneFloorplan,
floorplanDependencies: (node) => (node.autoFromWalls ? node.boundaryWallIds : []),
// 2D body move — centroid-pivot polygon mover (same as slab / ceiling).
// Without this, zone fell through to the overlay's generic free-translate
// path, which committed a `position` field zone has no schema for, so the
// polygon never actually moved on drop.
floorplanMoveTarget: zoneFloorplanMoveTarget,
// Polygon editor when selected — same four operations slabs / ceilings
// expose. The shared factories key off `node.polygon`, optional
// `node.holes` (absent on zones). See `floorplan-affordances.ts`.
floorplanAffordances: {
'move-vertex': zoneMoveVertexAffordance,
'add-vertex': zoneAddVertexAffordance,
'move-edge': zoneMoveEdgeAffordance,
'delete-vertex': zoneDeleteVertexAffordance,
},
presentation: {
label: 'Zone',
description: 'A polygonal site zone (lawn, water, paving) with a TSL gradient material.',
icon: { kind: 'url', src: '/icons/zone.webp' },
paletteSection: 'site',
paletteOrder: 20,
},
mcp: {
description: 'A polygon-bounded site zone with a typed surface (grass / water / paving / ...).',
},
}