@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
@@ -0,0 +1,31 @@
1
+ import { type Evaluation, type ProceduralItemNode } from '@pascal-app/core/procedural-items';
2
+ import { type BufferGeometry } from 'three';
3
+ export type Batch = {
4
+ slot: string;
5
+ geometry: BufferGeometry;
6
+ ranges: {
7
+ end: number;
8
+ partId: string;
9
+ shapeId: string;
10
+ }[];
11
+ };
12
+ export type BuiltItem = {
13
+ batches: Batch[];
14
+ evaluation: Evaluation;
15
+ milliseconds: number;
16
+ triangles: number;
17
+ };
18
+ export declare const proceduralMetrics: {
19
+ builds: number;
20
+ cacheHits: number;
21
+ lastBuildMs: number;
22
+ liveEntries: number;
23
+ };
24
+ export declare const geometrySignature: (node: ProceduralItemNode) => string;
25
+ export declare function buildProceduralGeometry(node: ProceduralItemNode): BuiltItem;
26
+ export declare function acquireProceduralGeometry(node: ProceduralItemNode): {
27
+ value: BuiltItem;
28
+ release: () => void;
29
+ };
30
+ export declare function partAtFace(ranges: Batch['ranges'], face: number): string | null;
31
+ //# sourceMappingURL=geometry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometry.d.ts","sourceRoot":"","sources":["../../src/procedural-item/geometry.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,EAEf,KAAK,kBAAkB,EACxB,MAAM,mCAAmC,CAAA;AAC1C,OAAO,EAEL,KAAK,cAAc,EAMpB,MAAM,OAAO,CAAA;AAGd,MAAM,MAAM,KAAK,GAAG;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,cAAc,CAAA;IACxB,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC3D,CAAA;AACD,MAAM,MAAM,SAAS,GAAG;IACtB,OAAO,EAAE,KAAK,EAAE,CAAA;IAChB,UAAU,EAAE,UAAU,CAAA;IACtB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AACD,eAAO,MAAM,iBAAiB;;;;;CAA8D,CAAA;AAE5F,eAAO,MAAM,iBAAiB,GAAI,MAAM,kBAAkB,WACV,CAAA;AAChD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,kBAAkB,GAAG,SAAS,CA8D3E;AACD,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,kBAAkB;;;EAwBjE;AACD,wBAAgB,UAAU,CAAC,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,iBAE/D"}
@@ -0,0 +1,88 @@
1
+ import { evaluateRecipe, } from '@pascal-app/core/procedural-items';
2
+ import { BoxGeometry, CylinderGeometry, Euler, Matrix4, Quaternion, Vector3, } from 'three';
3
+ import { RoundedBoxGeometry } from 'three/addons/geometries/RoundedBoxGeometry.js';
4
+ import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
5
+ export const proceduralMetrics = { builds: 0, cacheHits: 0, lastBuildMs: 0, liveEntries: 0 };
6
+ const cache = new Map();
7
+ export const geometrySignature = (node) => JSON.stringify([node.recipe, node.parameters]);
8
+ export function buildProceduralGeometry(node) {
9
+ const start = performance.now(), evaluation = evaluateRecipe(node.recipe, node.parameters);
10
+ const bySlot = new Map();
11
+ for (const shape of evaluation.shapes) {
12
+ const [w, h, d] = shape.size;
13
+ const source = shape.primitive === 'roundedBox'
14
+ ? new RoundedBoxGeometry(w, h, d, 2, shape.radius)
15
+ : shape.primitive === 'cylinder'
16
+ ? new CylinderGeometry(0.5, 0.5, 1, 24, 1)
17
+ : new BoxGeometry(w, h, d);
18
+ if (shape.primitive === 'cylinder')
19
+ source.scale(w, h, d);
20
+ const geometry = source.index ? source.toNonIndexed() : source;
21
+ if (geometry !== source)
22
+ source.dispose();
23
+ geometry.clearGroups();
24
+ const vertices = geometry.getAttribute('position'), normals = geometry.getAttribute('normal'), uv = geometry.getAttribute('uv');
25
+ for (let i = 0; i < vertices.count; i++) {
26
+ const x = vertices.getX(i), y = vertices.getY(i), z = vertices.getZ(i);
27
+ const nx = Math.abs(normals.getX(i)), ny = Math.abs(normals.getY(i)), nz = Math.abs(normals.getZ(i));
28
+ uv.setXY(i, nx > ny && nx > nz ? z : x, ny > nx && ny > nz ? z : y);
29
+ }
30
+ geometry.applyMatrix4(new Matrix4().compose(new Vector3(...shape.position), new Quaternion().setFromEuler(new Euler(...shape.rotation)), new Vector3(1, 1, 1)));
31
+ const entry = bySlot.get(shape.slot) ?? { geometries: [], ranges: [], faces: 0 };
32
+ entry.geometries.push(geometry);
33
+ entry.faces += geometry.getAttribute('position').count / 3;
34
+ entry.ranges.push({ end: entry.faces, partId: shape.partId, shapeId: shape.id });
35
+ bySlot.set(shape.slot, entry);
36
+ }
37
+ const batches = [];
38
+ for (const [slot, entry] of bySlot) {
39
+ const geometry = mergeGeometries(entry.geometries, false);
40
+ for (const g of entry.geometries)
41
+ g.dispose();
42
+ if (!geometry)
43
+ throw new Error('Unable to batch procedural geometry');
44
+ geometry.computeBoundingBox();
45
+ geometry.computeBoundingSphere();
46
+ batches.push({ slot, geometry, ranges: entry.ranges });
47
+ }
48
+ const milliseconds = performance.now() - start;
49
+ proceduralMetrics.builds++;
50
+ proceduralMetrics.lastBuildMs = milliseconds;
51
+ return {
52
+ batches,
53
+ evaluation,
54
+ milliseconds,
55
+ triangles: batches.reduce((n, b) => n + b.geometry.getAttribute('position').count / 3, 0),
56
+ };
57
+ }
58
+ export function acquireProceduralGeometry(node) {
59
+ const key = geometrySignature(node);
60
+ let entry = cache.get(key);
61
+ if (entry)
62
+ proceduralMetrics.cacheHits++;
63
+ else {
64
+ entry = { value: buildProceduralGeometry(node), users: 0 };
65
+ cache.set(key, entry);
66
+ }
67
+ entry.users++;
68
+ proceduralMetrics.liveEntries = cache.size;
69
+ let released = false;
70
+ return {
71
+ value: entry.value,
72
+ release: () => {
73
+ if (released)
74
+ return;
75
+ released = true;
76
+ const current = cache.get(key);
77
+ if (current && --current.users === 0) {
78
+ for (const batch of current.value.batches)
79
+ batch.geometry.dispose();
80
+ cache.delete(key);
81
+ }
82
+ proceduralMetrics.liveEntries = cache.size;
83
+ },
84
+ };
85
+ }
86
+ export function partAtFace(ranges, face) {
87
+ return ranges.find((r) => face < r.end)?.partId ?? null;
88
+ }
@@ -0,0 +1,3 @@
1
+ export { proceduralItemDefinition } from './definition';
2
+ export { acquireProceduralGeometry, buildProceduralGeometry, geometrySignature, partAtFace, proceduralMetrics, } from './geometry';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/procedural-item/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAA;AACvD,OAAO,EACL,yBAAyB,EACzB,uBAAuB,EACvB,iBAAiB,EACjB,UAAU,EACV,iBAAiB,GAClB,MAAM,YAAY,CAAA"}
@@ -0,0 +1,2 @@
1
+ export { proceduralItemDefinition } from './definition';
2
+ export { acquireProceduralGeometry, buildProceduralGeometry, geometrySignature, partAtFace, proceduralMetrics, } from './geometry';
@@ -0,0 +1,15 @@
1
+ import { type AnyNodeId, type CeilingNode, type FloorplanMoveTarget, type FloorplanMoveTargetSession, type WallNode } from '@pascal-app/core';
2
+ import { type ProceduralItemNode } from '@pascal-app/core/procedural-items';
3
+ export declare const createProceduralWallMoveSession: typeof createProceduralMountedMoveSession;
4
+ export declare const createProceduralCeilingMoveSession: typeof createProceduralMountedMoveSession;
5
+ declare function createProceduralMountedMoveSession(node: ProceduralItemNode, levelId: AnyNodeId): FloorplanMoveTargetSession & {
6
+ wall(wall: WallNode, x: number, y: number, side: "front" | "back", alt: boolean): void;
7
+ ceiling(ceiling: CeilingNode, x: number, z: number, alt: boolean): void;
8
+ rotate(direction: number): void;
9
+ free(point: readonly [number, number]): void;
10
+ commit(): void;
11
+ readonly candidate: ProceduralItemNode | null;
12
+ };
13
+ export declare const proceduralFloorplanMoveTarget: FloorplanMoveTarget<ProceduralItemNode>;
14
+ export {};
15
+ //# sourceMappingURL=move-session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"move-session.d.ts","sourceRoot":"","sources":["../../src/procedural-item/move-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,0BAA0B,EAI/B,KAAK,QAAQ,EACd,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAML,KAAK,kBAAkB,EAOxB,MAAM,mCAAmC,CAAA;AA0B1C,eAAO,MAAM,+BAA+B,2CAAqC,CAAA;AACjF,eAAO,MAAM,kCAAkC,2CAAqC,CAAA;AAEpF,iBAAS,kCAAkC,CAAC,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,SAAS;eA4BzE,QAAQ,KAAK,MAAM,KAAK,MAAM,QAAQ,OAAO,GAAG,MAAM,OAAO,OAAO,GAAG,IAAI;qBACrE,WAAW,KAAK,MAAM,KAAK,MAAM,OAAO,OAAO,GAAG,IAAI;sBACrD,MAAM,GAAG,IAAI;gBACnB,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI;cAClC,IAAI;wBACM,kBAAkB,GAAG,IAAI;EAiKhD;AAED,eAAO,MAAM,6BAA6B,EAAE,mBAAmB,CAAC,kBAAkB,CAqBjF,CAAA"}
@@ -0,0 +1,197 @@
1
+ import { useLiveNodeOverrides, useScene, } from '@pascal-app/core';
2
+ import { attachmentBounds, boundsOf, boxCorners, evaluateRecipe, frame, proceduralLocalPose, resolveProceduralCeilingPlacement, resolveProceduralWallPlacement, transformPoint, validateProceduralRelations, } from '@pascal-app/core/procedural-items';
3
+ import { isGridSnapActive, isMagneticSnapActive, snapToHalf, triggerSFX, useEditor, usePlacementPreview, } from '@pascal-app/editor';
4
+ import { findContainingSurface, itemFloorplanMoveTarget } from '../item/floorplan-move';
5
+ import { findClosestWallInPlan, hasWallChildOverlap, snapLocalXToNeighbors, } from '../shared/wall-attach-target';
6
+ function mountedBounds(node, nodes) {
7
+ const e = evaluateRecipe(node.recipe, node.parameters);
8
+ const pose = proceduralLocalPose(node, nodes);
9
+ return boundsOf(boxCorners(e.min, e.max).map((point) => transformPoint(frame(pose.position, pose.rotation), point)));
10
+ }
11
+ export const createProceduralWallMoveSession = createProceduralMountedMoveSession;
12
+ export const createProceduralCeilingMoveSession = createProceduralMountedMoveSession;
13
+ function createProceduralMountedMoveSession(node, levelId) {
14
+ const ceilingMounted = node.recipe.mounting?.attachTo === 'ceiling';
15
+ let yaw = node.rotation[1];
16
+ const e = evaluateRecipe(node.recipe, node.parameters);
17
+ let candidate = null;
18
+ let flipped = false;
19
+ let force = false;
20
+ let last = null;
21
+ let onHost = false;
22
+ let stepKey = '';
23
+ const id = node.id;
24
+ const show = (next) => {
25
+ const key = next.position
26
+ .map((v) => Math.round(v / (isGridSnapActive() ? useEditor.getState().gridSnapStep : 0.1)))
27
+ .join(':');
28
+ if (key !== stepKey) {
29
+ stepKey = key;
30
+ triggerSFX('sfx:grid-snap');
31
+ }
32
+ useLiveNodeOverrides.getState().set(id, { visible: false });
33
+ usePlacementPreview
34
+ .getState()
35
+ .set(next, next.parentId ? useScene.getState().nodes[next.parentId] : null);
36
+ };
37
+ const session = {
38
+ affectedIds: [id],
39
+ get candidate() {
40
+ return candidate;
41
+ },
42
+ flipSide() {
43
+ if (ceilingMounted) {
44
+ session.rotate(1);
45
+ return;
46
+ }
47
+ flipped = !flipped;
48
+ triggerSFX('sfx:item-rotate');
49
+ last?.();
50
+ },
51
+ rotate(direction) {
52
+ yaw = (Math.round(yaw / (Math.PI / 4)) + direction) * (Math.PI / 4);
53
+ triggerSFX('sfx:item-rotate');
54
+ last?.();
55
+ },
56
+ ceiling(ceiling, x, z, alt) {
57
+ if (!ceilingMounted || ceiling.parentId !== levelId)
58
+ return;
59
+ last = () => session.ceiling(ceiling, x, z, alt);
60
+ force = alt;
61
+ candidate = resolveProceduralCeilingPlacement(node, ceiling, alt ? x : snapToHalf(x), alt ? z : snapToHalf(z), yaw);
62
+ if (!onHost)
63
+ triggerSFX('sfx:item-pick');
64
+ onHost = true;
65
+ show(candidate);
66
+ },
67
+ free(point) {
68
+ last = () => session.free(point);
69
+ candidate = null;
70
+ onHost = false;
71
+ show({
72
+ ...node,
73
+ wallId: undefined,
74
+ side: undefined,
75
+ supportSlabId: undefined,
76
+ parentId: levelId,
77
+ position: [snapToHalf(point[0]), node.position[1] || 1.2, snapToHalf(point[1])],
78
+ rotation: [0, ceilingMounted ? yaw : flipped ? Math.PI : 0, 0],
79
+ visible: true,
80
+ });
81
+ },
82
+ wall(wall, x, y, side, alt) {
83
+ if (ceilingMounted || wall.parentId !== levelId)
84
+ return;
85
+ last = () => session.wall(wall, x, y, side, alt);
86
+ force = alt;
87
+ const nodes = useScene.getState().nodes;
88
+ const aligned = isMagneticSnapActive() && !alt
89
+ ? snapLocalXToNeighbors({ wall, localX: x, width: e.dimensions[0], selfId: id, nodes })
90
+ : null;
91
+ candidate = resolveProceduralWallPlacement(node, wall, aligned ?? (alt ? x : snapToHalf(x)), alt ? y : snapToHalf(y), flipped ? (side === 'front' ? 'back' : 'front') : side, nodes);
92
+ if (!candidate) {
93
+ session.free([wall.start[0], wall.start[1]]);
94
+ return;
95
+ }
96
+ if (!onHost)
97
+ triggerSFX('sfx:item-pick');
98
+ onHost = true;
99
+ show(candidate);
100
+ },
101
+ apply({ planPoint, modifiers }) {
102
+ if (ceilingMounted) {
103
+ const ceiling = findContainingSurface(planPoint, useScene.getState().nodes, levelId, 'ceiling');
104
+ if (ceiling)
105
+ session.ceiling(ceiling, planPoint[0], planPoint[1], modifiers.altKey);
106
+ else
107
+ session.free(planPoint);
108
+ return;
109
+ }
110
+ const hit = findClosestWallInPlan(planPoint, useScene.getState().nodes, levelId);
111
+ if (hit)
112
+ session.wall(hit.wall, hit.localX, node.position[1] || 1.2, hit.side, modifiers.altKey);
113
+ else
114
+ session.free(planPoint);
115
+ },
116
+ canCommit() {
117
+ if (!candidate || useScene.getState().readOnly)
118
+ return false;
119
+ const nodes = useScene.getState().nodes;
120
+ try {
121
+ validateProceduralRelations(candidate, nodes);
122
+ }
123
+ catch {
124
+ return false;
125
+ }
126
+ if (force)
127
+ return true;
128
+ if (ceilingMounted) {
129
+ const own = mountedBounds(candidate, nodes);
130
+ return !Object.values(nodes).some((other) => {
131
+ if (other.id === id || other.parentId !== candidate.parentId)
132
+ return false;
133
+ const box = other.type === 'procedural-item'
134
+ ? mountedBounds(other, nodes)
135
+ : other.type === 'item'
136
+ ? attachmentBounds(other)
137
+ : null;
138
+ return (box &&
139
+ own.min[0] < box.max[0] - 1e-6 &&
140
+ own.max[0] > box.min[0] + 1e-6 &&
141
+ own.min[2] < box.max[2] - 1e-6 &&
142
+ own.max[2] > box.min[2] + 1e-6);
143
+ });
144
+ }
145
+ const b = mountedBounds(candidate, nodes);
146
+ const centerX = (b.min[0] + b.max[0]) / 2;
147
+ const centerY = (b.min[1] + b.max[1]) / 2;
148
+ if (hasWallChildOverlap(candidate.wallId, nodes, centerX, centerY, b.dimensions[0], b.dimensions[1], id))
149
+ return false;
150
+ const own = b;
151
+ return !Object.values(nodes).some((other) => {
152
+ if (other.type !== 'procedural-item' || other.id === id)
153
+ return false;
154
+ const p = other;
155
+ if (p.wallId !== candidate.wallId || p.side !== candidate.side)
156
+ return false;
157
+ const box = mountedBounds(p, nodes);
158
+ return own.min.every((v, i) => v < box.max[i] - 1e-6 && own.max[i] > box.min[i] + 1e-6);
159
+ });
160
+ },
161
+ commit() {
162
+ if (!candidate || !session.canCommit())
163
+ return;
164
+ useLiveNodeOverrides.getState().clear(id);
165
+ const { parentId, wallId, position, rotation, side, visible, supportSlabId } = candidate;
166
+ useScene.getState().updateNode(id, {
167
+ parentId,
168
+ wallId,
169
+ position,
170
+ rotation,
171
+ side,
172
+ visible,
173
+ supportSlabId,
174
+ });
175
+ usePlacementPreview.getState().clear();
176
+ },
177
+ };
178
+ return session;
179
+ }
180
+ export const proceduralFloorplanMoveTarget = ({ node, nodes, sceneApi, }) => {
181
+ if (!node.recipe.mounting) {
182
+ const e = evaluateRecipe(node.recipe, node.parameters);
183
+ return itemFloorplanMoveTarget({
184
+ node: {
185
+ ...node,
186
+ asset: { dimensions: e.dimensions },
187
+ scale: [1, 1, 1],
188
+ },
189
+ nodes,
190
+ sceneApi,
191
+ });
192
+ }
193
+ let parent = node.parentId ? nodes[node.parentId] : undefined;
194
+ while (parent && parent.type !== 'level')
195
+ parent = parent.parentId ? nodes[parent.parentId] : undefined;
196
+ return createProceduralMountedMoveSession(node, parent?.id);
197
+ };
@@ -0,0 +1,5 @@
1
+ import { type ProceduralItemNode } from '@pascal-app/core/procedural-items';
2
+ export default function MoveProceduralItem({ node }: {
3
+ node: ProceduralItemNode;
4
+ }): import("react").JSX.Element;
5
+ //# sourceMappingURL=move-tool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"move-tool.d.ts","sourceRoot":"","sources":["../../src/procedural-item/move-tool.tsx"],"names":[],"mappings":"AAaA,OAAO,EAAE,KAAK,kBAAkB,EAAuB,MAAM,mCAAmC,CAAA;AAsBhG,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,kBAAkB,CAAA;CAAE,+BAMhF"}
@@ -0,0 +1,193 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { emitter, holdHiddenWallPointerEvents, sceneRegistry, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
4
+ import { queryProceduralItem } from '@pascal-app/core/procedural-items';
5
+ import { clearPlacementSurface, commitFreshPlacementSubtree, consumePlacementDragRelease, getSideFromNormal, isFreshPlacementMetadata, isValidWallSideFace, MoveRegistryNodeTool, publishPlacementSurface, triggerSFX, useEditor, usePlacementPreview, useRegistryToolContext, } from '@pascal-app/editor';
6
+ import { useEffect, useMemo, useState } from 'react';
7
+ import { Vector3 } from 'three';
8
+ import { MeshBasicNodeMaterial } from 'three/webgpu';
9
+ import { beginOpeningMoveHistorySession } from '../shared/opening-move-history';
10
+ import { acquireProceduralGeometry } from './geometry';
11
+ import { createProceduralCeilingMoveSession, createProceduralWallMoveSession } from './move-session';
12
+ export default function MoveProceduralItem({ node }) {
13
+ return node.recipe.mounting ? (_jsx(MountedMove, { node: node })) : (_jsx(MoveRegistryNodeTool, { node: node }));
14
+ }
15
+ function MountedMove({ node }) {
16
+ const { activeLevelId, isCameraDragging, selectNode } = useRegistryToolContext();
17
+ const [valid, setValid] = useState(false);
18
+ useEffect(() => {
19
+ if (!activeLevelId)
20
+ return;
21
+ const ceilingMounted = node.recipe.mounting?.attachTo === 'ceiling';
22
+ const session = (ceilingMounted ? createProceduralCeilingMoveSession : createProceduralWallMoveSession)(node, activeLevelId);
23
+ const releaseWallEvents = holdHiddenWallPointerEvents();
24
+ let wallAt = -Infinity;
25
+ let finished = false;
26
+ const onWall = (event) => {
27
+ if (ceilingMounted ||
28
+ useEditor.getState().isFloorplanHovered ||
29
+ !isValidWallSideFace(event.normal))
30
+ return;
31
+ wallAt = performance.now();
32
+ session.wall(event.node, event.localPosition[0], event.localPosition[1], getSideFromNormal(event.normal), event.nativeEvent.altKey);
33
+ setValid(session.canCommit());
34
+ const normal = new Vector3(...(event.normal ?? [0, 0, 1]));
35
+ const wall = sceneRegistry.nodes.get(event.node.id);
36
+ if (wall)
37
+ normal.transformDirection(wall.matrixWorld);
38
+ publishPlacementSurface(new Vector3(...event.position), normal);
39
+ };
40
+ const onCeiling = (event) => {
41
+ if (!ceilingMounted || useEditor.getState().isFloorplanHovered)
42
+ return;
43
+ wallAt = performance.now();
44
+ session.ceiling(event.node, event.localPosition[0], event.localPosition[2], event.nativeEvent.altKey);
45
+ setValid(session.canCommit());
46
+ publishPlacementSurface(new Vector3(...event.position), new Vector3(0, -1, 0));
47
+ };
48
+ const onGrid = (event) => {
49
+ if (useEditor.getState().isFloorplanHovered || performance.now() - wallAt < 64)
50
+ return;
51
+ if (ceilingMounted)
52
+ session.free([event.position[0], event.position[2]]);
53
+ else
54
+ session.apply({
55
+ planPoint: [event.position[0], event.position[2]],
56
+ modifiers: event.nativeEvent,
57
+ });
58
+ setValid(session.canCommit());
59
+ if (!session.candidate)
60
+ clearPlacementSurface();
61
+ };
62
+ const commit = () => {
63
+ if (finished || isCameraDragging() || !session.canCommit())
64
+ return;
65
+ const history = beginOpeningMoveHistorySession();
66
+ let id = node.id;
67
+ try {
68
+ if (isFreshPlacementMetadata(node.metadata)) {
69
+ session.commit();
70
+ id = commitFreshPlacementSubtree(node.id, { visible: true });
71
+ }
72
+ else
73
+ history.commitStep(() => session.commit());
74
+ finished = true;
75
+ }
76
+ finally {
77
+ history.end();
78
+ }
79
+ if (id)
80
+ selectNode(id);
81
+ triggerSFX('sfx:item-place');
82
+ useEditor.getState().setMovingNode(null);
83
+ };
84
+ const clickWall = (event) => {
85
+ if (ceilingMounted)
86
+ return;
87
+ onWall(event);
88
+ if (session.canCommit()) {
89
+ event.stopPropagation();
90
+ commit();
91
+ }
92
+ };
93
+ const clickCeiling = (event) => {
94
+ if (!ceilingMounted)
95
+ return;
96
+ onCeiling(event);
97
+ if (session.canCommit()) {
98
+ event.stopPropagation();
99
+ commit();
100
+ }
101
+ };
102
+ const leaveCeiling = (event) => {
103
+ if (!ceilingMounted || useEditor.getState().isFloorplanHovered)
104
+ return;
105
+ wallAt = -Infinity;
106
+ session.free([event.position[0], event.position[2]]);
107
+ setValid(false);
108
+ clearPlacementSurface();
109
+ };
110
+ const clickGrid = (event) => {
111
+ onGrid(event);
112
+ commit();
113
+ };
114
+ const cancel = () => {
115
+ usePlacementPreview.getState().clear();
116
+ useLiveNodeOverrides.getState().clear(node.id);
117
+ useEditor.getState().setMovingNode(null);
118
+ };
119
+ const release = (event) => {
120
+ if (consumePlacementDragRelease(event))
121
+ commit();
122
+ };
123
+ const key = (event) => {
124
+ if (useEditor.getState().isFloorplanHovered || event.repeat || event.metaKey || event.ctrlKey)
125
+ return;
126
+ if (event.target?.closest('input,textarea,[contenteditable="true"]'))
127
+ return;
128
+ if (event.key.toLowerCase() === 'r' || (ceilingMounted && event.key.toLowerCase() === 't')) {
129
+ event.preventDefault();
130
+ event.stopImmediatePropagation();
131
+ if (ceilingMounted)
132
+ session.rotate(event.key.toLowerCase() === 't' ? -1 : 1);
133
+ else
134
+ session.flipSide?.();
135
+ setValid(session.canCommit());
136
+ }
137
+ };
138
+ emitter.on('ceiling:enter', onCeiling);
139
+ emitter.on('ceiling:move', onCeiling);
140
+ emitter.on('ceiling:click', clickCeiling);
141
+ emitter.on('ceiling:leave', leaveCeiling);
142
+ emitter.on('wall:enter', onWall);
143
+ emitter.on('wall:move', onWall);
144
+ emitter.on('wall:click', clickWall);
145
+ emitter.on('grid:move', onGrid);
146
+ emitter.on('grid:click', clickGrid);
147
+ emitter.on('tool:cancel', cancel);
148
+ window.addEventListener('pointerup', release);
149
+ window.addEventListener('keydown', key, true);
150
+ return () => {
151
+ emitter.off('ceiling:enter', onCeiling);
152
+ emitter.off('ceiling:move', onCeiling);
153
+ emitter.off('ceiling:click', clickCeiling);
154
+ emitter.off('ceiling:leave', leaveCeiling);
155
+ emitter.off('wall:enter', onWall);
156
+ emitter.off('wall:move', onWall);
157
+ emitter.off('wall:click', clickWall);
158
+ emitter.off('grid:move', onGrid);
159
+ emitter.off('grid:click', clickGrid);
160
+ emitter.off('tool:cancel', cancel);
161
+ window.removeEventListener('pointerup', release);
162
+ window.removeEventListener('keydown', key, true);
163
+ useLiveNodeOverrides.getState().clear(node.id);
164
+ usePlacementPreview.getState().clear();
165
+ clearPlacementSurface();
166
+ releaseWallEvents();
167
+ };
168
+ }, [node, activeLevelId, isCameraDragging, selectNode]);
169
+ return _jsx(MountedGhost, { node: node, valid: valid, levelId: activeLevelId });
170
+ }
171
+ function MountedGhost({ node, valid, levelId, }) {
172
+ const preview = usePlacementPreview((s) => s.node?.id === node.id ? s.node : null);
173
+ const [built, setBuilt] = useState(null);
174
+ useEffect(() => {
175
+ const lease = acquireProceduralGeometry(node);
176
+ setBuilt(lease.value);
177
+ return lease.release;
178
+ }, [node.recipe, node.parameters, node]);
179
+ const material = useMemo(() => new MeshBasicNodeMaterial({
180
+ color: valid ? '#86efac' : '#f87171',
181
+ transparent: true,
182
+ opacity: 0.65,
183
+ depthWrite: false,
184
+ }), [valid]);
185
+ useEffect(() => () => material.dispose(), [material]);
186
+ if (!preview || !built)
187
+ return null;
188
+ const { frame } = queryProceduralItem(preview, useScene.getState().nodes);
189
+ const position = [...frame.position];
190
+ position[1] += levelId ? (sceneRegistry.nodes.get(levelId)?.position.y ?? 0) : 0;
191
+ const yaw = Math.atan2(frame.axes[2][0], frame.axes[2][2]);
192
+ return (_jsx("group", { position: position, "rotation-y": yaw, children: built.batches.map((batch) => (_jsx("mesh", { geometry: batch.geometry, material: material, dispose: null, raycast: () => { } }, batch.slot))) }));
193
+ }
@@ -0,0 +1,5 @@
1
+ import { type ProceduralItemNode } from '@pascal-app/core/procedural-items';
2
+ export default function ProceduralRenderer({ node }: {
3
+ node: ProceduralItemNode;
4
+ }): import("react").JSX.Element;
5
+ //# sourceMappingURL=renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../../src/procedural-item/renderer.tsx"],"names":[],"mappings":"AASA,OAAO,EAAE,KAAK,kBAAkB,EAAuB,MAAM,mCAAmC,CAAA;AAahG,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,kBAAkB,CAAA;CAAE,+BA8FhF"}
@@ -0,0 +1,66 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useLiveNodeOverrides, useLiveTransforms, useRegistry, useScene, } from '@pascal-app/core';
4
+ import { proceduralLocalPose } from '@pascal-app/core/procedural-items';
5
+ import { createSurfaceRoleMaterial, NodeRenderer, resolveMaterialRef, resolveSlotDefaultMaterial, useLibraryMaterialsVersion, useNodeEvents, useViewer, } from '@pascal-app/viewer';
6
+ import { useLayoutEffect, useMemo, useRef, useState } from 'react';
7
+ import { Mesh } from 'three';
8
+ import { acquireProceduralGeometry, geometrySignature } from './geometry';
9
+ export default function ProceduralRenderer({ node }) {
10
+ const ref = useRef(null);
11
+ const overrides = useLiveNodeOverrides((s) => s.overrides.get(node.id));
12
+ const live = useLiveTransforms((s) => s.get(node.id));
13
+ const effective = { ...node, ...overrides };
14
+ const host = useScene((s) => (node.parentId ? s.nodes[node.parentId] : undefined));
15
+ const hostOverride = useLiveNodeOverrides((s) => node.parentId ? s.overrides.get(node.parentId) : undefined);
16
+ const pose = proceduralLocalPose(effective, host ? { [host.id]: { ...host, ...hostOverride } } : {});
17
+ const sceneMaterials = useScene((s) => s.materials);
18
+ const shading = useViewer((s) => s.shading), textures = useViewer((s) => s.textures), colorPreset = useViewer((s) => s.colorPreset), sceneTheme = useViewer((s) => s.sceneTheme);
19
+ const libraryVersion = useLibraryMaterialsVersion();
20
+ const key = geometrySignature(effective);
21
+ const [built, setBuilt] = useState(null);
22
+ const handlers = useNodeEvents(node, 'procedural-item');
23
+ useRegistry(node.id, 'procedural-item', ref);
24
+ useLayoutEffect(() => {
25
+ // React can restore base Y after the previous frame consumed the elevation mark.
26
+ useScene.getState().markDirty(node.id);
27
+ });
28
+ useLayoutEffect(() => {
29
+ const [recipe, parameters] = JSON.parse(key);
30
+ const lease = acquireProceduralGeometry({ recipe, parameters });
31
+ setBuilt(lease.value);
32
+ return lease.release;
33
+ }, [key]);
34
+ const materialKey = JSON.stringify([effective.recipe.slots, effective.slots, libraryVersion]);
35
+ const materials = useMemo(() => {
36
+ const [slots, overrides] = JSON.parse(materialKey);
37
+ return new Map(slots.map((s) => {
38
+ const ref = overrides[s.id];
39
+ const material = textures
40
+ ? (resolveMaterialRef(ref, sceneMaterials, shading) ??
41
+ resolveSlotDefaultMaterial(ref?.startsWith('#') ? ref : s.color, shading, 0.75))
42
+ : createSurfaceRoleMaterial('furnishing', colorPreset, undefined, sceneTheme);
43
+ return [s.id, material];
44
+ }));
45
+ }, [materialKey, sceneMaterials, shading, textures, colorPreset, sceneTheme]);
46
+ useLayoutEffect(() => () => {
47
+ for (const material of materials.values())
48
+ if (!material.userData.__pascalCachedMaterial)
49
+ material.dispose();
50
+ }, [materials]);
51
+ const meshes = useMemo(() => built?.batches.map((batch) => {
52
+ const mesh = new Mesh(batch.geometry, materials.get(batch.slot));
53
+ mesh.name = `slot_${batch.slot}`;
54
+ mesh.userData = { slotId: batch.slot, proceduralRanges: batch.ranges };
55
+ mesh.castShadow = true;
56
+ mesh.receiveShadow = true;
57
+ return mesh;
58
+ }) ?? [], [built, materials]);
59
+ const rotation = live?.rotation === undefined
60
+ ? pose.rotation
61
+ : [pose.rotation[0], live.rotation, pose.rotation[2]];
62
+ return (_jsxs("group", { ref: ref, position: live?.position ?? pose.position, rotation: rotation, visible: effective.visible, ...handlers, children: [meshes.map((mesh) => (_jsx("primitive", { object: mesh, dispose: null }, mesh.uuid))), effective.children.map((id) => {
63
+ const surface = built?.evaluation.surfaces.find((s) => s.id === effective.attachments[id]);
64
+ return (_jsx("group", { position: surface?.position ?? [0, 0, 0], rotation: surface?.rotation, children: _jsx(NodeRenderer, { nodeId: id }) }, id));
65
+ })] }));
66
+ }