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 + `` 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 = { kind: 'zone', snapProfile: 'structural', schemaVersion: 2, schema: ZoneNode, category: 'site', extensions: { 'pascal:editor/floorplan': { contextualDimensions: buildZoneContextualDimensions, schedule: buildRoomFloorplanSchedule, } satisfies FloorplanNodeExtension, }, 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 / ...).', }, }