@pascal-app/nodes 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/dist/box-vent/panel.js +4 -4
  2. package/dist/cabinet/appliance-layout.d.ts +26 -0
  3. package/dist/cabinet/appliance-layout.d.ts.map +1 -0
  4. package/dist/cabinet/appliance-layout.js +52 -0
  5. package/dist/cabinet/countertop-layout.d.ts +173 -0
  6. package/dist/cabinet/countertop-layout.d.ts.map +1 -0
  7. package/dist/cabinet/countertop-layout.js +80 -0
  8. package/dist/cabinet/definition.d.ts.map +1 -1
  9. package/dist/cabinet/definition.js +15 -8
  10. package/dist/cabinet/geometry/cooktop.d.ts.map +1 -1
  11. package/dist/cabinet/geometry/cooktop.js +2 -2
  12. package/dist/cabinet/geometry/run.d.ts.map +1 -1
  13. package/dist/cabinet/geometry/run.js +10 -71
  14. package/dist/cabinet/geometry/sink.d.ts +2 -12
  15. package/dist/cabinet/geometry/sink.d.ts.map +1 -1
  16. package/dist/cabinet/geometry/sink.js +8 -39
  17. package/dist/cabinet/hosted-resize.d.ts +8 -0
  18. package/dist/cabinet/hosted-resize.d.ts.map +1 -0
  19. package/dist/cabinet/hosted-resize.js +158 -0
  20. package/dist/cabinet/run-layout.d.ts +8 -1
  21. package/dist/cabinet/run-layout.d.ts.map +1 -1
  22. package/dist/cabinet/run-layout.js +22 -14
  23. package/dist/cabinet/run-ops.d.ts +3 -3
  24. package/dist/cabinet/run-ops.js +3 -3
  25. package/dist/cabinet/run-panel.d.ts +1 -1
  26. package/dist/cabinet/run-panel.d.ts.map +1 -1
  27. package/dist/cabinet/run-panel.js +16 -7
  28. package/dist/cabinet/surfaces.d.ts +4 -0
  29. package/dist/cabinet/surfaces.d.ts.map +1 -0
  30. package/dist/cabinet/surfaces.js +101 -0
  31. package/dist/cabinet/tree-structure.d.ts.map +1 -1
  32. package/dist/cabinet/tree-structure.js +10 -9
  33. package/dist/ceiling/definition.js +1 -1
  34. package/dist/ceiling/floorplan-affordances.d.ts +4 -4
  35. package/dist/ceiling/panel.js +1 -1
  36. package/dist/chimney/panel.js +9 -9
  37. package/dist/cupola/panel.js +1 -1
  38. package/dist/door/panel.js +12 -12
  39. package/dist/dormer/panel-position-section.js +2 -2
  40. package/dist/dormer/panel.js +1 -1
  41. package/dist/duct-segment/parametrics.d.ts.map +1 -1
  42. package/dist/duct-segment/parametrics.js +5 -1
  43. package/dist/duct-segment/tool.d.ts +1 -0
  44. package/dist/duct-segment/tool.d.ts.map +1 -1
  45. package/dist/duct-segment/tool.js +10 -8
  46. package/dist/eyebrow-vent/panel.js +1 -1
  47. package/dist/fence/inspector-editors.js +1 -1
  48. package/dist/imported-mesh/definition.d.ts +5 -0
  49. package/dist/imported-mesh/definition.d.ts.map +1 -0
  50. package/dist/imported-mesh/definition.js +38 -0
  51. package/dist/imported-mesh/floorplan.d.ts +4 -0
  52. package/dist/imported-mesh/floorplan.d.ts.map +1 -0
  53. package/dist/imported-mesh/floorplan.js +34 -0
  54. package/dist/imported-mesh/geometry.d.ts +5 -0
  55. package/dist/imported-mesh/geometry.d.ts.map +1 -0
  56. package/dist/imported-mesh/geometry.js +34 -0
  57. package/dist/imported-mesh/index.d.ts +4 -0
  58. package/dist/imported-mesh/index.d.ts.map +1 -0
  59. package/dist/imported-mesh/index.js +3 -0
  60. package/dist/imported-mesh/schema.d.ts +2 -0
  61. package/dist/imported-mesh/schema.d.ts.map +1 -0
  62. package/dist/imported-mesh/schema.js +1 -0
  63. package/dist/index.d.ts +2 -0
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +8 -0
  66. package/dist/item/floorplan-move.d.ts +9 -1
  67. package/dist/item/floorplan-move.d.ts.map +1 -1
  68. package/dist/item/floorplan-move.js +55 -2
  69. package/dist/item/floorplan.d.ts.map +1 -1
  70. package/dist/item/floorplan.js +11 -0
  71. package/dist/item/move-tool.js +1 -1
  72. package/dist/item/panel.js +1 -1
  73. package/dist/pipe-segment/parametrics.d.ts.map +1 -1
  74. package/dist/pipe-segment/parametrics.js +5 -1
  75. package/dist/procedural-item/definition.d.ts +4 -0
  76. package/dist/procedural-item/definition.d.ts.map +1 -0
  77. package/dist/procedural-item/definition.js +254 -0
  78. package/dist/procedural-item/geometry.d.ts +31 -0
  79. package/dist/procedural-item/geometry.d.ts.map +1 -0
  80. package/dist/procedural-item/geometry.js +88 -0
  81. package/dist/procedural-item/index.d.ts +3 -0
  82. package/dist/procedural-item/index.d.ts.map +1 -0
  83. package/dist/procedural-item/index.js +2 -0
  84. package/dist/procedural-item/move-session.d.ts +15 -0
  85. package/dist/procedural-item/move-session.d.ts.map +1 -0
  86. package/dist/procedural-item/move-session.js +197 -0
  87. package/dist/procedural-item/move-tool.d.ts +5 -0
  88. package/dist/procedural-item/move-tool.d.ts.map +1 -0
  89. package/dist/procedural-item/move-tool.js +193 -0
  90. package/dist/procedural-item/renderer.d.ts +5 -0
  91. package/dist/procedural-item/renderer.d.ts.map +1 -0
  92. package/dist/procedural-item/renderer.js +66 -0
  93. package/dist/ridge-vent/panel.js +4 -4
  94. package/dist/roof/panel.d.ts.map +1 -1
  95. package/dist/roof/panel.js +7 -5
  96. package/dist/roof-segment/panel.d.ts.map +1 -1
  97. package/dist/roof-segment/panel.js +5 -5
  98. package/dist/shared/automatic-run-end-cap.d.ts +1 -0
  99. package/dist/shared/automatic-run-end-cap.d.ts.map +1 -1
  100. package/dist/shared/automatic-run-end-cap.js +10 -0
  101. package/dist/shared/distribution-run-tool.d.ts +10 -0
  102. package/dist/shared/distribution-run-tool.d.ts.map +1 -1
  103. package/dist/shared/distribution-run-tool.js +56 -11
  104. package/dist/shared/panel-fields.d.ts +15 -0
  105. package/dist/shared/panel-fields.d.ts.map +1 -0
  106. package/dist/shared/panel-fields.js +31 -0
  107. package/dist/shared/resting-surface-plan.d.ts +3 -0
  108. package/dist/shared/resting-surface-plan.d.ts.map +1 -0
  109. package/dist/shared/resting-surface-plan.js +28 -0
  110. package/dist/shelf/definition.d.ts.map +1 -1
  111. package/dist/shelf/definition.js +2 -1
  112. package/dist/shelf/floorplan-affordances.d.ts.map +1 -1
  113. package/dist/shelf/floorplan-affordances.js +5 -3
  114. package/dist/shelf/floorplan-move.d.ts.map +1 -1
  115. package/dist/shelf/floorplan-move.js +3 -2
  116. package/dist/shelf/geometry.d.ts.map +1 -1
  117. package/dist/shelf/geometry.js +2 -10
  118. package/dist/skylight/panel.js +6 -6
  119. package/dist/slab/panel.js +1 -1
  120. package/dist/solar-panel/panel.js +2 -2
  121. package/dist/spawn/panel.js +1 -1
  122. package/dist/stair/panel.d.ts.map +1 -1
  123. package/dist/stair/panel.js +9 -7
  124. package/dist/stair-segment/panel.js +4 -4
  125. package/dist/turbine-vent/panel.js +1 -1
  126. package/dist/unit/definition.d.ts +10 -0
  127. package/dist/unit/definition.d.ts.map +1 -0
  128. package/dist/unit/definition.js +38 -0
  129. package/dist/unit/index.d.ts +2 -0
  130. package/dist/unit/index.d.ts.map +1 -0
  131. package/dist/unit/index.js +1 -0
  132. package/dist/unit/parametrics.d.ts +3 -0
  133. package/dist/unit/parametrics.d.ts.map +1 -0
  134. package/dist/unit/parametrics.js +4 -0
  135. package/dist/unit/schema.d.ts +2 -0
  136. package/dist/unit/schema.d.ts.map +1 -0
  137. package/dist/unit/schema.js +1 -0
  138. package/dist/unit/unit-panel.d.ts +5 -0
  139. package/dist/unit/unit-panel.d.ts.map +1 -0
  140. package/dist/unit/unit-panel.js +61 -0
  141. package/dist/window/panel.js +6 -6
  142. package/dist/zone/definition.d.ts.map +1 -1
  143. package/dist/zone/definition.js +7 -1
  144. package/dist/zone/floorplan.d.ts +1 -3
  145. package/dist/zone/floorplan.d.ts.map +1 -1
  146. package/dist/zone/floorplan.js +43 -28
  147. package/dist/zone/quantities-panel.d.ts.map +1 -1
  148. package/dist/zone/quantities-panel.js +33 -29
  149. package/dist/zone/renderer.d.ts.map +1 -1
  150. package/dist/zone/renderer.js +20 -6
  151. package/dist/zone/unit-membership.d.ts +12 -0
  152. package/dist/zone/unit-membership.d.ts.map +1 -0
  153. package/dist/zone/unit-membership.js +22 -0
  154. package/package.json +14 -8
@@ -2,10 +2,9 @@ import { resolveAutoZonePolygon, } from '@pascal-app/core';
2
2
  import { floorplanGeometryMetadata, readFloorplanContext } from '@pascal-app/editor';
3
3
  import { formatConstructionLength, } from '../shared/construction-length';
4
4
  import { buildRoomClearDimensions } from './room-clear-dimensions';
5
+ import { owningUnitForZone } from './unit-membership';
5
6
  /**
6
- * Stage C floor-plan builder for zone. Zones are colored polygons —
7
- * fill + outline both come from `zone.color`. Selection adds an
8
- * accent-colored outline.
7
+ * Stage C floor-plan builder for zone. Zones are colored polygons.
9
8
  *
10
9
  * The zone's `name` renders as a centered text label at the polygon's
11
10
  * geometric centroid. The registry layer sorts zones before every
@@ -18,23 +17,25 @@ export function buildZoneFloorplan(node, ctx) {
18
17
  return null;
19
18
  const view = ctx.viewState;
20
19
  const floorplanContext = readFloorplanContext(ctx);
21
- const palette = view?.palette;
22
20
  const isSelected = view?.selected ?? false;
23
21
  const isHighlighted = view?.highlighted ?? false;
24
22
  const showSelectedChrome = isSelected || isHighlighted;
25
23
  const points = ring.map(([x, z]) => [x, z]);
26
- const stroke = showSelectedChrome && palette ? palette.selectedStroke : node.color;
24
+ const unit = owningUnitForZone(node, ctx.resolve);
25
+ const tintColor = unit?.color ?? node.color;
26
+ const stroke = node.color;
27
+ const focusOpacity = view?.focusedUnitId && !view.focusedUnitMemberIds?.includes(node.id) ? 0.35 : 1;
27
28
  const isRoom = node.spaceRole === 'room';
28
29
  const fillOpacity = isRoom ? (isSelected ? 0.12 : 0.04) : isSelected ? 0.28 : 0.16;
29
30
  const children = [
30
31
  {
31
32
  kind: 'polygon',
32
33
  points,
33
- fill: node.color,
34
- fillOpacity,
34
+ fill: tintColor,
35
+ fillOpacity: fillOpacity * focusOpacity,
35
36
  stroke,
36
37
  strokeWidth: showSelectedChrome ? 0.08 : 0.05,
37
- strokeOpacity: showSelectedChrome ? 0.96 : 0.72,
38
+ strokeOpacity: (showSelectedChrome ? 0.96 : 0.72) * focusOpacity,
38
39
  strokeLinejoin: 'round',
39
40
  vectorEffect: 'non-scaling-stroke',
40
41
  },
@@ -88,44 +89,58 @@ export function buildZoneFloorplan(node, ctx) {
88
89
  const [cx, cy] = polygonCentroid(ring);
89
90
  const name = node.name?.trim();
90
91
  if (isRoom) {
91
- children.push(...buildRoomLabels(node, cx, cy, view?.unit ?? 'metric', floorplanContext.purpose === 'document' ? 'document' : 'editor', floorplanContext.metricNotation, stroke));
92
+ children.push(...buildRoomLabels(node, cx, cy, view?.unit ?? 'metric', floorplanContext.purpose === 'document' ? 'document' : 'editor', floorplanContext.metricNotation, stroke, unit?.name));
92
93
  if (floorplanContext.automaticDimensions) {
93
94
  children.push(...buildRoomClearDimensions(node, ctx));
94
95
  }
95
96
  }
96
97
  else if (name) {
97
- children.push({
98
- kind: 'text',
99
- x: cx,
100
- y: cy,
101
- text: name,
102
- // Same constants the legacy `FLOORPLAN_ZONE_LABEL_FONT_SIZE` uses
103
- // (0.2 plan metres ≈ readable at typical building zooms).
104
- fontSize: ZONE_LABEL_FONT_SIZE,
105
- fill: '#ffffff',
106
- stroke: node.color,
107
- strokeWidth: ZONE_LABEL_FONT_SIZE * 0.35,
108
- paintOrder: 'stroke',
109
- fontFamily: 'system-ui, -apple-system, sans-serif',
110
- fontWeight: 500,
111
- textAnchor: 'middle',
112
- dominantBaseline: 'central',
113
- opacity: showSelectedChrome ? 1 : 0.92,
114
- upright: true,
98
+ const unitName = unit?.name.trim();
99
+ const lines = unitName
100
+ ? [
101
+ { text: name, fontSize: ZONE_LABEL_FONT_SIZE, fontWeight: 500 },
102
+ { text: unitName, fontSize: ZONE_UNIT_LABEL_FONT_SIZE, fontWeight: 600 },
103
+ ]
104
+ : [{ text: name, fontSize: ZONE_LABEL_FONT_SIZE, fontWeight: 500 }];
105
+ const startY = cy - ((lines.length - 1) * ROOM_LABEL_LINE_SPACING) / 2;
106
+ lines.forEach((line, index) => {
107
+ children.push({
108
+ kind: 'text',
109
+ x: cx,
110
+ y: startY + index * ROOM_LABEL_LINE_SPACING,
111
+ text: line.text,
112
+ // Same constants the legacy `FLOORPLAN_ZONE_LABEL_FONT_SIZE` uses
113
+ // (0.2 plan metres ≈ readable at typical building zooms).
114
+ fontSize: line.fontSize,
115
+ fill: '#ffffff',
116
+ stroke: node.color,
117
+ strokeWidth: line.fontSize * 0.35,
118
+ paintOrder: 'stroke',
119
+ fontFamily: 'system-ui, -apple-system, sans-serif',
120
+ fontWeight: line.fontWeight,
121
+ textAnchor: 'middle',
122
+ dominantBaseline: 'central',
123
+ opacity: showSelectedChrome ? 1 : 0.92,
124
+ upright: true,
125
+ });
115
126
  });
116
127
  }
117
128
  return { kind: 'group', children };
118
129
  }
119
130
  const ZONE_LABEL_FONT_SIZE = 0.2;
131
+ const ZONE_UNIT_LABEL_FONT_SIZE = 0.13;
120
132
  const ROOM_NAME_FONT_SIZE = 0.2;
121
133
  const ROOM_NUMBER_FONT_SIZE = 0.16;
122
134
  const ROOM_DETAIL_FONT_SIZE = 0.11;
123
135
  const ROOM_LABEL_LINE_SPACING = 0.18;
124
- function buildRoomLabels(node, x, y, unit, profile, metricNotation, color) {
136
+ function buildRoomLabels(node, x, y, unit, profile, metricNotation, color, unitName) {
125
137
  const lines = [];
126
138
  const name = node.name.trim();
127
139
  if (name)
128
140
  lines.push({ text: name, fontSize: ROOM_NAME_FONT_SIZE, fontWeight: 700 });
141
+ if (unitName?.trim()) {
142
+ lines.push({ text: unitName.trim(), fontSize: ROOM_NUMBER_FONT_SIZE, fontWeight: 600 });
143
+ }
129
144
  if (node.roomNumber) {
130
145
  lines.push({ text: node.roomNumber, fontSize: ROOM_NUMBER_FONT_SIZE, fontWeight: 600 });
131
146
  }
@@ -1 +1 @@
1
- {"version":3,"file":"quantities-panel.d.ts","sourceRoot":"","sources":["../../src/zone/quantities-panel.tsx"],"names":[],"mappings":"AA8TA,MAAM,CAAC,OAAO,UAAU,mBAAmB,uCA6F1C"}
1
+ {"version":3,"file":"quantities-panel.d.ts","sourceRoot":"","sources":["../../src/zone/quantities-panel.tsx"],"names":[],"mappings":"AA8RA,MAAM,CAAC,OAAO,UAAU,mBAAmB,uCA8F1C"}
@@ -3,8 +3,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  import { deriveZoneQuantityReport, resolveAutoZonePolygon, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
4
4
  import { formatAreaLabel, formatLinearMeasurement, formatVolumeLabel, MetricControl, PanelSection, ToggleControl, } from '@pascal-app/editor';
5
5
  import { useViewer } from '@pascal-app/viewer';
6
- import { useEffect, useMemo, useRef, useState } from 'react';
6
+ import { useMemo } from 'react';
7
7
  import { useShallow } from 'zustand/react/shallow';
8
+ import { PanelSelect, PanelTextField } from '../shared/panel-fields';
9
+ import { buildingUnitsForZone } from './unit-membership';
8
10
  function ZonePlanSketch({ edgeLengths, metricNotation, polygon, unit, }) {
9
11
  if (polygon.length < 3) {
10
12
  return (_jsx("div", { className: "flex h-28 items-center justify-center rounded-md border border-border/50 text-muted-foreground text-xs", children: "Zone boundary unavailable" }));
@@ -44,43 +46,45 @@ function ZonePlanSketch({ edgeLengths, metricNotation, polygon, unit, }) {
44
46
  function QuantityRow({ abbreviation, format, label, quantity, }) {
45
47
  return (_jsxs("div", { className: "rounded-md border border-border/50 bg-background/35 px-2.5 py-2", children: [_jsxs("div", { className: "flex items-baseline gap-2", children: [_jsx("span", { className: "font-mono font-semibold text-cyan-600 text-[10px]", children: abbreviation }), _jsx("span", { className: "text-muted-foreground text-xs", children: label }), _jsx("span", { className: "ml-auto font-mono font-medium text-foreground text-xs tabular-nums", children: quantity.status === 'available' ? format(quantity.value) : 'Not proven' })] }), _jsx("div", { className: "mt-1 text-[10px] text-muted-foreground leading-snug", children: quantity.status === 'available' ? quantity.note : quantity.reason })] }));
46
48
  }
47
- function RoomTextField({ label, onCommit, value, }) {
48
- const [draft, setDraft] = useState(value);
49
- const cancelRef = useRef(false);
50
- useEffect(() => setDraft(value), [value]);
51
- const commit = () => {
52
- if (cancelRef.current) {
53
- cancelRef.current = false;
54
- setDraft(value);
55
- return;
49
+ function ZoneUnitPanel({ zone }) {
50
+ const units = useScene(useShallow((state) => buildingUnitsForZone(zone, (id) => state.nodes[id])));
51
+ if (units.length === 0)
52
+ return null;
53
+ const current = units.find((unit) => unit.members.includes(zone.id));
54
+ const handleChange = (nextId) => {
55
+ const { nodes, updateNodes } = useScene.getState();
56
+ const updates = [];
57
+ for (const unit of Object.values(nodes)) {
58
+ if (unit.type !== 'unit')
59
+ continue;
60
+ const isMember = unit.members.includes(zone.id);
61
+ if (unit.id === nextId && !isMember) {
62
+ updates.push({ id: unit.id, data: { members: [...unit.members, zone.id] } });
63
+ }
64
+ else if (unit.id !== nextId && isMember) {
65
+ updates.push({
66
+ id: unit.id,
67
+ data: { members: unit.members.filter((id) => id !== zone.id) },
68
+ });
69
+ }
56
70
  }
57
- const next = draft.trim();
58
- if (next !== value)
59
- onCommit(next);
60
- else
61
- setDraft(value);
71
+ if (updates.length > 0)
72
+ updateNodes(updates);
62
73
  };
63
- return (_jsxs("label", { className: "flex h-10 items-center gap-3 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-sm", children: [_jsx("span", { className: "shrink-0 text-muted-foreground", children: label }), _jsx("input", { className: "min-w-0 flex-1 bg-transparent text-right text-foreground outline-none selection:bg-primary/30", onBlur: commit, onChange: (event) => setDraft(event.target.value), onKeyDown: (event) => {
64
- if (event.key === 'Enter')
65
- event.currentTarget.blur();
66
- if (event.key === 'Escape') {
67
- cancelRef.current = true;
68
- event.currentTarget.blur();
69
- }
70
- }, type: "text", value: draft })] }));
71
- }
72
- function RoomSelect({ label, onChange, options, value, }) {
73
- return (_jsxs("label", { className: "flex h-10 items-center justify-between gap-3 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: label }), _jsx("select", { className: "min-w-0 rounded-md border border-border/50 bg-[#232325] px-2 py-1 text-foreground text-xs outline-none", onChange: (event) => onChange(event.target.value), value: value, children: options.map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value))) })] }));
74
+ return (_jsx(PanelSection, { title: "Unit", children: _jsx(PanelSelect, { label: "Unit", onChange: handleChange, options: [
75
+ { label: 'None', value: '' },
76
+ ...units.map((unit) => ({ label: unit.name || 'Unit', value: unit.id })),
77
+ ], value: current?.id ?? '' }) }));
74
78
  }
75
79
  function RoomDocumentationPanel({ zone }) {
76
80
  const updateNode = useScene((state) => state.updateNode);
77
81
  const update = (patch) => updateNode(zone.id, patch);
78
82
  const isRoom = zone.spaceRole === 'room';
79
- return (_jsxs(PanelSection, { title: "Room documentation", children: [_jsx(ToggleControl, { checked: isRoom, label: "Architectural room", onChange: (checked) => update({ spaceRole: checked ? 'room' : 'generic' }) }), isRoom ? (_jsxs(_Fragment, { children: [_jsx(RoomTextField, { label: "Room name", onCommit: (name) => update({ name }), value: zone.name }), _jsx(RoomTextField, { label: "Room number", onCommit: (roomNumber) => update({ roomNumber }), value: zone.roomNumber }), _jsx(RoomSelect, { label: "Enclosure", onChange: (enclosureStatus) => update({ enclosureStatus: enclosureStatus }), options: [
83
+ return (_jsxs(PanelSection, { title: "Room documentation", children: [_jsx(ToggleControl, { checked: isRoom, label: "Architectural room", onChange: (checked) => update({ spaceRole: checked ? 'room' : 'generic' }) }), isRoom ? (_jsxs(_Fragment, { children: [_jsx(PanelTextField, { label: "Room name", onCommit: (name) => update({ name }), value: zone.name }), _jsx(PanelTextField, { label: "Room number", onCommit: (roomNumber) => update({ roomNumber }), value: zone.roomNumber }), _jsx(PanelSelect, { label: "Enclosure", onChange: (enclosureStatus) => update({ enclosureStatus: enclosureStatus }), options: [
80
84
  { label: 'Auto-detect', value: 'auto' },
81
85
  { label: 'Enclosed', value: 'enclosed' },
82
86
  { label: 'Open', value: 'open' },
83
- ], value: zone.enclosureStatus }), _jsx(RoomTextField, { label: "Occupancy / use", onCommit: (occupancy) => update({ occupancy }), value: zone.occupancy }), _jsx(RoomTextField, { label: "Floor finish", onCommit: (floorFinish) => update({ floorFinish }), value: zone.floorFinish }), _jsx(RoomTextField, { label: "Wall finish", onCommit: (wallFinish) => update({ wallFinish }), value: zone.wallFinish }), _jsx(RoomTextField, { label: "Ceiling finish", onCommit: (ceilingFinish) => update({ ceilingFinish }), value: zone.ceilingFinish }), _jsx(MetricControl, { label: "Ceiling height", max: 20, min: 0.1, onChange: (ceilingHeight) => update({ ceilingHeight }), precision: 2, step: 0.05, unit: "m", value: zone.ceilingHeight }), _jsx(RoomSelect, { label: "Clear dimensions", onChange: (clearDimensionPolicy) => update({
87
+ ], value: zone.enclosureStatus }), _jsx(PanelTextField, { label: "Occupancy / use", onCommit: (occupancy) => update({ occupancy }), value: zone.occupancy }), _jsx(PanelTextField, { label: "Floor finish", onCommit: (floorFinish) => update({ floorFinish }), value: zone.floorFinish }), _jsx(PanelTextField, { label: "Wall finish", onCommit: (wallFinish) => update({ wallFinish }), value: zone.wallFinish }), _jsx(PanelTextField, { label: "Ceiling finish", onCommit: (ceilingFinish) => update({ ceilingFinish }), value: zone.ceilingFinish }), _jsx(MetricControl, { label: "Ceiling height", max: 20, min: 0.1, onChange: (ceilingHeight) => update({ ceilingHeight }), precision: 2, step: 0.05, unit: "m", value: zone.ceilingHeight }), _jsx(PanelSelect, { label: "Clear dimensions", onChange: (clearDimensionPolicy) => update({
84
88
  clearDimensionPolicy: clearDimensionPolicy,
85
89
  }), options: [
86
90
  { label: 'None', value: 'none' },
@@ -124,5 +128,5 @@ export default function ZoneQuantitiesPanel() {
124
128
  const report = useMemo(() => (effectiveZone ? deriveZoneQuantityReport(effectiveZone, effectiveNodes) : null), [effectiveNodes, effectiveZone]);
125
129
  if (!effectiveZone || !report)
126
130
  return null;
127
- return (_jsxs(_Fragment, { children: [_jsx(RoomDocumentationPanel, { zone: effectiveZone }), _jsxs(PanelSection, { title: effectiveZone.spaceRole === 'room' ? 'Room quantities' : 'Zone quantities', children: [_jsxs("div", { className: "overflow-hidden rounded-md border border-cyan-950/20 bg-[#f8faf7] text-slate-950", children: [_jsxs("div", { className: "flex items-center border-cyan-950/15 border-b px-2.5 py-2", children: [_jsx("span", { className: "font-semibold text-[11px]", children: effectiveZone.name }), _jsx("span", { className: "ml-auto rounded-full border border-cyan-800/25 bg-cyan-50 px-2 py-0.5 text-cyan-900 text-[9px]", children: report.classification === 'enclosed-room' ? 'Enclosed room' : 'Footprint only' })] }), _jsxs("div", { className: "flex items-baseline gap-2 px-2.5 py-2 font-mono text-[10px]", children: [_jsx("span", { className: "text-cyan-800", children: "A" }), _jsx("span", { children: formatAreaLabel(report.footprintArea, unit, 2) }), _jsx("span", { className: "ml-auto text-slate-600", children: "P" }), _jsx("span", { children: formatLinearMeasurement(report.perimeter, unit, metricNotation) })] })] }), _jsx(ZonePlanSketch, { edgeLengths: report.edgeLengths, metricNotation: metricNotation, polygon: effectiveZone.polygon, unit: unit }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(QuantityRow, { abbreviation: "Aw", format: (value) => formatAreaLabel(value, unit, 2), label: "Wall surface", quantity: report.wallSurface }), _jsx(QuantityRow, { abbreviation: "Af", format: (value) => formatAreaLabel(value, unit, 2), label: "Floor surface", quantity: report.floorSurface }), _jsx(QuantityRow, { abbreviation: "V", format: (value) => formatVolumeLabel(value, unit, 2), label: "Volume", quantity: report.volume })] })] })] }));
131
+ return (_jsxs(_Fragment, { children: [_jsx(ZoneUnitPanel, { zone: effectiveZone }), _jsx(RoomDocumentationPanel, { zone: effectiveZone }), _jsxs(PanelSection, { title: effectiveZone.spaceRole === 'room' ? 'Room quantities' : 'Zone quantities', children: [_jsxs("div", { className: "overflow-hidden rounded-md border border-cyan-950/20 bg-[#f8faf7] text-slate-950", children: [_jsxs("div", { className: "flex items-center border-cyan-950/15 border-b px-2.5 py-2", children: [_jsx("span", { className: "font-semibold text-[11px]", children: effectiveZone.name }), _jsx("span", { className: "ml-auto rounded-full border border-cyan-800/25 bg-cyan-50 px-2 py-0.5 text-cyan-900 text-[9px]", children: report.classification === 'enclosed-room' ? 'Enclosed room' : 'Footprint only' })] }), _jsxs("div", { className: "flex items-baseline gap-2 px-2.5 py-2 font-mono text-[10px]", children: [_jsx("span", { className: "text-cyan-800", children: "A" }), _jsx("span", { children: formatAreaLabel(report.footprintArea, unit, 2) }), _jsx("span", { className: "ml-auto text-slate-600", children: "P" }), _jsx("span", { children: formatLinearMeasurement(report.perimeter, unit, metricNotation) })] })] }), _jsx(ZonePlanSketch, { edgeLengths: report.edgeLengths, metricNotation: metricNotation, polygon: effectiveZone.polygon, unit: unit }), _jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(QuantityRow, { abbreviation: "Aw", format: (value) => formatAreaLabel(value, unit, 2), label: "Wall surface", quantity: report.wallSurface }), _jsx(QuantityRow, { abbreviation: "Af", format: (value) => formatAreaLabel(value, unit, 2), label: "Floor surface", quantity: report.floorSurface }), _jsx(QuantityRow, { abbreviation: "V", format: (value) => formatVolumeLabel(value, unit, 2), label: "Volume", quantity: report.volume })] })] })] }));
128
132
  }
@@ -1 +1 @@
1
- {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../src/zone/renderer.tsx"],"names":[],"mappings":"AAEA,OAAO,EAML,KAAK,QAAQ,EACd,MAAM,kBAAkB,CAAA;AAyGzB,eAAO,MAAM,YAAY,GAAI,UAAU;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,uCA0LxD,CAAA;AAED,eAAe,YAAY,CAAA"}
1
+ {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../src/zone/renderer.tsx"],"names":[],"mappings":"AAEA,OAAO,EAML,KAAK,QAAQ,EACd,MAAM,kBAAkB,CAAA;AA0GzB,eAAO,MAAM,YAAY,GAAI,UAAU;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,uCA+MxD,CAAA;AAED,eAAe,YAAY,CAAA"}
@@ -8,6 +8,7 @@ import { BufferGeometry, Color, DoubleSide, Float32BufferAttribute, Shape } from
8
8
  import { color, float, uniform, uv } from 'three/tsl';
9
9
  import { MeshBasicNodeMaterial } from 'three/webgpu';
10
10
  import { useShallow } from 'zustand/react/shallow';
11
+ import { owningUnitForZone } from './unit-membership';
11
12
  const Y_OFFSET = 0.01;
12
13
  const WALL_HEIGHT = 2.3;
13
14
  /**
@@ -113,6 +114,10 @@ export const ZoneRenderer = ({ node }) => {
113
114
  return override ? { ...dependency, ...override } : dependency;
114
115
  }), [dependencyIds, dependencyNodes, dependencyOverrides, node]);
115
116
  const polygon = livePolygon ?? proceduralPolygon;
117
+ // The selector returns the unit node itself, so only edits to that unit
118
+ // or its membership re-render this zone.
119
+ const unit = useScene((s) => owningUnitForZone(node, (id) => s.nodes[id]));
120
+ const tintColor = unit?.color ?? node.color;
116
121
  // Create floor shape from polygon
117
122
  const floorShape = useMemo(() => {
118
123
  if (!polygon || polygon.length < 3)
@@ -158,15 +163,15 @@ export const ZoneRenderer = ({ node }) => {
158
163
  }, [polygon]);
159
164
  // Create materials
160
165
  const floorMaterial = useMemo(() => {
161
- if (!node?.color)
166
+ if (!tintColor)
162
167
  return null;
163
- return createFloorMaterial(node.color);
164
- }, [node?.color]);
168
+ return createFloorMaterial(tintColor);
169
+ }, [tintColor]);
165
170
  const wallMaterial = useMemo(() => {
166
- if (!node?.color)
171
+ if (!node.color)
167
172
  return null;
168
173
  return createWallGradientMaterial(node.color);
169
- }, [node?.color]);
174
+ }, [node.color]);
170
175
  const handlers = useNodeEvents(node, 'zone');
171
176
  if (!(node && floorShape && wallGeometry && floorMaterial && wallMaterial)) {
172
177
  return null;
@@ -183,7 +188,16 @@ export const ZoneRenderer = ({ node }) => {
183
188
  color: 'white',
184
189
  textShadow: `-1px -1px 0 ${node.color}, 1px -1px 0 ${node.color}, -1px 1px 0 ${node.color}, 1px 1px 0 ${node.color}`,
185
190
  textAlign: 'center',
186
- }, children: _jsx("span", { children: node.name }) }), _jsxs("div", { className: "label-pin", style: {
191
+ }, children: _jsx("span", { children: node.name }) }), unit && (_jsx("div", { style: {
192
+ marginTop: '2px',
193
+ padding: '1px 6px',
194
+ borderRadius: '999px',
195
+ backgroundColor: tintColor,
196
+ color: 'white',
197
+ fontSize: '10px',
198
+ lineHeight: '14px',
199
+ whiteSpace: 'nowrap',
200
+ }, children: unit.name })), _jsxs("div", { className: "label-pin", style: {
187
201
  display: 'flex',
188
202
  flexDirection: 'column',
189
203
  alignItems: 'center',
@@ -0,0 +1,12 @@
1
+ import type { AnyNode, AnyNodeId, UnitNode, ZoneNode } from '@pascal-app/core';
2
+ type ResolveNode = (id: AnyNodeId) => AnyNode | undefined;
3
+ /**
4
+ * Units under the building that owns this zone's level. Units are building
5
+ * children, so this walks that short list instead of scanning every node
6
+ * the way `unitsForZone` does — cheap enough for per-zone store selectors.
7
+ */
8
+ export declare function buildingUnitsForZone(zone: Pick<ZoneNode, 'parentId'>, resolve: ResolveNode): UnitNode[];
9
+ /** The unit whose colour and name a member zone wears: the first one listing it. */
10
+ export declare function owningUnitForZone(zone: Pick<ZoneNode, 'id' | 'parentId'>, resolve: ResolveNode): UnitNode | null;
11
+ export {};
12
+ //# sourceMappingURL=unit-membership.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unit-membership.d.ts","sourceRoot":"","sources":["../../src/zone/unit-membership.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAE9E,KAAK,WAAW,GAAG,CAAC,EAAE,EAAE,SAAS,KAAK,OAAO,GAAG,SAAS,CAAA;AAEzD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,EAChC,OAAO,EAAE,WAAW,GACnB,QAAQ,EAAE,CAUZ;AAED,oFAAoF;AACpF,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,GAAG,UAAU,CAAC,EACvC,OAAO,EAAE,WAAW,GACnB,QAAQ,GAAG,IAAI,CAEjB"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Units under the building that owns this zone's level. Units are building
3
+ * children, so this walks that short list instead of scanning every node
4
+ * the way `unitsForZone` does — cheap enough for per-zone store selectors.
5
+ */
6
+ export function buildingUnitsForZone(zone, resolve) {
7
+ const level = zone.parentId ? resolve(zone.parentId) : undefined;
8
+ const building = level?.parentId ? resolve(level.parentId) : undefined;
9
+ if (building?.type !== 'building')
10
+ return [];
11
+ const units = [];
12
+ for (const childId of building.children) {
13
+ const child = resolve(childId);
14
+ if (child?.type === 'unit')
15
+ units.push(child);
16
+ }
17
+ return units;
18
+ }
19
+ /** The unit whose colour and name a member zone wears: the first one listing it. */
20
+ export function owningUnitForZone(zone, resolve) {
21
+ return buildingUnitsForZone(zone, resolve).find((unit) => unit.members.includes(zone.id)) ?? null;
22
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pascal-app/nodes",
3
- "version": "1.0.0",
4
- "description": "Built-in node bundles for the Pascal 3D editor \u2014 one folder per kind, exported as `builtinPlugin`",
3
+ "version": "1.0.1",
4
+ "description": "Built-in node bundles for the Pascal 3D editor — one folder per kind, exported as `builtinPlugin`",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -10,6 +10,11 @@
10
10
  "types": "./dist/index.d.ts",
11
11
  "import": "./dist/index.js",
12
12
  "default": "./dist/index.js"
13
+ },
14
+ "./procedural-item": {
15
+ "types": "./dist/procedural-item/index.d.ts",
16
+ "import": "./dist/procedural-item/index.js",
17
+ "default": "./dist/procedural-item/index.js"
13
18
  }
14
19
  },
15
20
  "files": [
@@ -23,9 +28,9 @@
23
28
  "prepublishOnly": "bun run build && bun test"
24
29
  },
25
30
  "peerDependencies": {
26
- "@pascal-app/core": "^1.0.0",
27
- "@pascal-app/editor": "^1.0.0",
28
- "@pascal-app/viewer": "^1.0.0",
31
+ "@pascal-app/core": "^1.0.1",
32
+ "@pascal-app/editor": "^1.0.1",
33
+ "@pascal-app/viewer": "^1.0.1",
29
34
  "@react-three/drei": "^10",
30
35
  "@react-three/fiber": "^9",
31
36
  "lucide-react": "^1",
@@ -35,10 +40,11 @@
35
40
  "zustand": "^5"
36
41
  },
37
42
  "devDependencies": {
38
- "@pascal-app/core": "^1.0.0",
39
- "@pascal-app/editor": "^1.0.0",
40
- "@pascal-app/viewer": "^1.0.0",
43
+ "@pascal-app/core": "^1.0.1",
44
+ "@pascal-app/editor": "^1.0.1",
45
+ "@pascal-app/viewer": "^1.0.1",
41
46
  "@pascal/typescript-config": "*",
47
+ "@react-three/test-renderer": "^9.1.0",
42
48
  "@types/bun": "^1.3.0",
43
49
  "@types/node": "^22.19.12",
44
50
  "@types/react": "^19.2.2",