@pascal-app/nodes 1.0.0-beta.1 → 1.0.0-beta.3

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.
@@ -1 +1 @@
1
- {"version":3,"file":"definition.d.ts","sourceRoot":"","sources":["../../src/fence/definition.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,cAAc,EAEpB,MAAM,kBAAkB,CAAA;AAkBzB,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAyOpC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,EAAE,cAAc,CAAC,OAAO,SAAS,CAyH5D,CAAA"}
1
+ {"version":3,"file":"definition.d.ts","sourceRoot":"","sources":["../../src/fence/definition.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,cAAc,EAEpB,MAAM,kBAAkB,CAAA;AAkBzB,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAgPpC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,EAAE,cAAc,CAAC,OAAO,SAAS,CAyH5D,CAAA"}
@@ -4,7 +4,7 @@ import { buildFenceFloorplan } from './floorplan';
4
4
  import { fenceControlPointAffordance, fenceCurveAffordance, fenceMoveEndpointAffordance, fenceTangentAffordance, } from './floorplan-affordances';
5
5
  import { fenceFloorplanMoveTarget } from './floorplan-move';
6
6
  import { buildFenceGeometry } from './geometry';
7
- import { resolveFenceLiftElevation } from './lift';
7
+ import { resolveFenceLiftElevation, resolveFenceLiftElevationForNodes } from './lift';
8
8
  import { fencePaint } from './paint';
9
9
  import { fenceParametrics } from './parametrics';
10
10
  import { FenceNode } from './schema';
@@ -16,7 +16,10 @@ const SIDE_HANDLE_MIN_HEIGHT = 0.4;
16
16
  const HEIGHT_HANDLE_OFFSET = 0.45;
17
17
  const MIN_FENCE_HEIGHT = 0.3;
18
18
  function fenceBaseElevation(n, sceneApi) {
19
- return resolveFenceLiftElevation(n, (id) => sceneApi?.get(id));
19
+ const nodes = sceneApi?.nodes();
20
+ return nodes
21
+ ? resolveFenceLiftElevationForNodes(n, nodes)
22
+ : resolveFenceLiftElevation(n, () => undefined);
20
23
  }
21
24
  function fenceMidpointFrame(n) {
22
25
  const dx = n.end[0] - n.start[0];
@@ -110,7 +113,11 @@ function fenceElevationHandle(currentBase) {
110
113
  onDrag: (n, sceneApi) => publishStructuralElevationGuide(fenceElevationGuideSource(n), fenceBaseElevation(n, sceneApi), sceneApi.nodes()),
111
114
  onDragEnd: (n) => clearStructuralElevationGuide(n.id),
112
115
  apply: (initial, newBase, sceneApi) => {
113
- const supportBase = resolveFenceLiftElevation({ ...initial, supportOffset: undefined }, (id) => sceneApi.get(id));
116
+ // The offset is measured from the support WITHOUT the current offset —
117
+ // including the ground, so dragging a fence's base to an absolute height
118
+ // on a hillside stores the delta from that hillside and the fence keeps
119
+ // following it.
120
+ const supportBase = resolveFenceLiftElevationForNodes({ ...initial, supportOffset: undefined }, sceneApi.nodes());
114
121
  const nextOffset = newBase - supportBase;
115
122
  return {
116
123
  supportOffset: Math.abs(nextOffset) < 1e-6 ? undefined : nextOffset,
@@ -1 +1 @@
1
- {"version":3,"file":"geometry.d.ts","sourceRoot":"","sources":["../../src/fence/geometry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,eAAe,EAA0B,MAAM,kBAAkB,CAAA;AAC/F,OAAO,EAEL,KAAK,WAAW,EAKhB,KAAK,aAAa,EAGnB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAa,KAAK,EAAqC,MAAM,OAAO,CAAA;AAE3E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAwFzC,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,SAAS,EACf,GAAG,CAAC,EAAE,eAAe,EACrB,OAAO,GAAE,aAA0B,EACnC,QAAQ,UAAO,EACf,WAAW,GAAE,WAAoB,EACjC,UAAU,CAAC,EAAE,MAAM,GAClB,KAAK,CAgCP"}
1
+ {"version":3,"file":"geometry.d.ts","sourceRoot":"","sources":["../../src/fence/geometry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,eAAe,EAA0B,MAAM,kBAAkB,CAAA;AAC/F,OAAO,EAEL,KAAK,WAAW,EAKhB,KAAK,aAAa,EAGnB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAa,KAAK,EAAqC,MAAM,OAAO,CAAA;AAE3E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAwFzC,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,SAAS,EACf,GAAG,CAAC,EAAE,eAAe,EACrB,OAAO,GAAE,aAA0B,EACnC,QAAQ,UAAO,EACf,WAAW,GAAE,WAAoB,EACjC,UAAU,CAAC,EAAE,MAAM,GAClB,KAAK,CA0CP"}
@@ -51,10 +51,16 @@ function getLegacyFenceMaterial(node, shading) {
51
51
  export function buildFenceGeometry(node, ctx, shading = 'rendered', textures = true, colorPreset = 'clay', sceneTheme) {
52
52
  const group = new Group();
53
53
  const geometries = generateFenceSlotGeometries(node);
54
- // A hosted railing (`supportSlabId`) stands on its slab's walking surface.
55
- // The builder emits local-space children, so the lift lives on an inner
56
- // group rather than the registered (React-transformed) root.
57
- const lift = ctx ? resolveFenceLiftElevation(node, (id) => ctx.resolve(id)) : 0;
54
+ // A hosted railing (`supportSlabId`) stands on its slab's walking surface;
55
+ // an unhosted one stands on the ground, which `ctx.levelBaseAt` resolves at
56
+ // the fence's own start point — the anchor its plan geometry is measured
57
+ // from, so the resolver and the mesh cannot disagree about where the ground
58
+ // is under this fence. The builder emits local-space children, so the lift
59
+ // lives on an inner group rather than the registered (React-transformed)
60
+ // root.
61
+ const lift = ctx
62
+ ? resolveFenceLiftElevation(node, (id) => ctx.resolve(id), ctx.levelBaseAt?.(node.start[0], node.start[1]) ?? 0)
63
+ : 0;
58
64
  const meshParent = new Group();
59
65
  meshParent.position.y = lift;
60
66
  group.add(meshParent);
@@ -1,4 +1,4 @@
1
- import type { AnyNode } from '@pascal-app/core';
1
+ import { type AnyNode } from '@pascal-app/core';
2
2
  import type { FenceNode } from './schema';
3
3
  /**
4
4
  * Elevation (meters above the level plane) a hosted fence stands at.
@@ -10,6 +10,29 @@ import type { FenceNode } from './schema';
10
10
  * falls back to the level floor, mirroring the read-path rules of the
11
11
  * other `supportSlabId` carriers. Pure so it is unit-testable and
12
12
  * callable from the geometry builder with `ctx.resolve`.
13
+ *
14
+ * `levelBase` is what "the level floor" means here — the sculpted ground
15
+ * under the fence (`levelBaseElevationAt`, reached through `ctx.levelBaseAt`
16
+ * in the builder and {@link resolveFenceLiftElevationForNodes} everywhere
17
+ * else), or 0 for a level with no terrain under it. A caller-resolved scalar
18
+ * rather than a terrain lookup inside, so this stays pure and the sample stays
19
+ * at the caller's chosen point; it substitutes for the literal `0` this used to
20
+ * return in every unhosted branch, and nowhere else — a host slab that still
21
+ * exists wins outright, so a railing on a deck pad keeps the pad's elevation
22
+ * instead of following the hillside around it.
23
+ */
24
+ export declare function resolveFenceLiftElevation(node: Pick<FenceNode, 'supportSlabId' | 'supportOffset' | 'parentId'>, resolve: (id: string) => AnyNode | undefined, levelBase?: number): number;
25
+ /**
26
+ * {@link resolveFenceLiftElevation} against a nodes record, with the level base
27
+ * resolved from the terrain under the fence's start point — the same anchor the
28
+ * geometry builder samples, so a handle, a snap guide and the rendered rail all
29
+ * agree about where the ground is.
30
+ *
31
+ * Exists so the callers that already hold the whole scene (handles via
32
+ * `sceneApi.nodes()`, the dependency tracker, the editor's elevation guides)
33
+ * don't each re-derive the level walk and the sample point. The builder keeps
34
+ * using the pure form, since `GeometryContext` deliberately exposes the ground
35
+ * as a closure rather than the store.
13
36
  */
14
- export declare function resolveFenceLiftElevation(node: Pick<FenceNode, 'supportSlabId' | 'supportOffset' | 'parentId'>, resolve: (id: string) => AnyNode | undefined): number;
37
+ export declare function resolveFenceLiftElevationForNodes(node: Pick<FenceNode, 'id' | 'start' | 'supportSlabId' | 'supportOffset' | 'parentId'>, nodes: Record<string, AnyNode>): number;
15
38
  //# sourceMappingURL=lift.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"lift.d.ts","sourceRoot":"","sources":["../../src/fence/lift.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAY,MAAM,kBAAkB,CAAA;AACzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAEzC;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,IAAI,CAAC,SAAS,EAAE,eAAe,GAAG,eAAe,GAAG,UAAU,CAAC,EACrE,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,GAAG,SAAS,GAC3C,MAAM,CAQR"}
1
+ {"version":3,"file":"lift.d.ts","sourceRoot":"","sources":["../../src/fence/lift.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EAKb,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAEzC;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,IAAI,CAAC,SAAS,EAAE,eAAe,GAAG,eAAe,GAAG,UAAU,CAAC,EACrE,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,GAAG,SAAS,EAC5C,SAAS,SAAI,GACZ,MAAM,CAQR;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iCAAiC,CAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,GAAG,OAAO,GAAG,eAAe,GAAG,eAAe,GAAG,UAAU,CAAC,EACtF,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,MAAM,CAIR"}
@@ -1,3 +1,4 @@
1
+ import { findLevelAncestorId, levelBaseElevationAt, } from '@pascal-app/core';
1
2
  /**
2
3
  * Elevation (meters above the level plane) a hosted fence stands at.
3
4
  *
@@ -8,16 +9,43 @@
8
9
  * falls back to the level floor, mirroring the read-path rules of the
9
10
  * other `supportSlabId` carriers. Pure so it is unit-testable and
10
11
  * callable from the geometry builder with `ctx.resolve`.
12
+ *
13
+ * `levelBase` is what "the level floor" means here — the sculpted ground
14
+ * under the fence (`levelBaseElevationAt`, reached through `ctx.levelBaseAt`
15
+ * in the builder and {@link resolveFenceLiftElevationForNodes} everywhere
16
+ * else), or 0 for a level with no terrain under it. A caller-resolved scalar
17
+ * rather than a terrain lookup inside, so this stays pure and the sample stays
18
+ * at the caller's chosen point; it substitutes for the literal `0` this used to
19
+ * return in every unhosted branch, and nowhere else — a host slab that still
20
+ * exists wins outright, so a railing on a deck pad keeps the pad's elevation
21
+ * instead of following the hillside around it.
11
22
  */
12
- export function resolveFenceLiftElevation(node, resolve) {
23
+ export function resolveFenceLiftElevation(node, resolve, levelBase = 0) {
13
24
  const offset = node.supportOffset ?? 0;
14
25
  if (!node.supportSlabId)
15
- return offset;
26
+ return levelBase + offset;
16
27
  const host = resolve(node.supportSlabId);
17
28
  if (host?.type !== 'slab')
18
- return offset;
29
+ return levelBase + offset;
19
30
  if ((host.parentId ?? null) !== (node.parentId ?? null))
20
- return offset;
31
+ return levelBase + offset;
21
32
  const elevation = host.elevation;
22
33
  return (Number.isFinite(elevation) ? elevation : 0) + offset;
23
34
  }
35
+ /**
36
+ * {@link resolveFenceLiftElevation} against a nodes record, with the level base
37
+ * resolved from the terrain under the fence's start point — the same anchor the
38
+ * geometry builder samples, so a handle, a snap guide and the rendered rail all
39
+ * agree about where the ground is.
40
+ *
41
+ * Exists so the callers that already hold the whole scene (handles via
42
+ * `sceneApi.nodes()`, the dependency tracker, the editor's elevation guides)
43
+ * don't each re-derive the level walk and the sample point. The builder keeps
44
+ * using the pure form, since `GeometryContext` deliberately exposes the ground
45
+ * as a closure rather than the store.
46
+ */
47
+ export function resolveFenceLiftElevationForNodes(node, nodes) {
48
+ const levelId = findLevelAncestorId(node.id, nodes);
49
+ const levelBase = levelId ? levelBaseElevationAt(nodes, levelId, node.start[0], node.start[1]) : 0;
50
+ return resolveFenceLiftElevation(node, (id) => nodes[id], levelBase);
51
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"system.d.ts","sourceRoot":"","sources":["../../src/fence/system.tsx"],"names":[],"mappings":"AA+BA,QAAA,MAAM,YAAY,YAgBjB,CAAA;AAED,eAAe,YAAY,CAAA"}
1
+ {"version":3,"file":"system.d.ts","sourceRoot":"","sources":["../../src/fence/system.tsx"],"names":[],"mappings":"AAiCA,QAAA,MAAM,YAAY,YAgBjB,CAAA;AAED,eAAe,YAAY,CAAA"}
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { useScene } from '@pascal-app/core';
3
3
  import { useEffect } from 'react';
4
- import { resolveFenceLiftElevation } from './lift';
4
+ import { resolveFenceLiftElevationForNodes } from './lift';
5
5
  /**
6
6
  * Hosted-railing dependency tracker. A fence with `supportSlabId` renders at
7
7
  * its host slab's elevation, but a store update only dirties the node that
@@ -17,9 +17,14 @@ function fenceLiftSignatures(nodes) {
17
17
  if (node.type !== 'fence')
18
18
  continue;
19
19
  const fence = node;
20
+ // Unhosted fences are covered too, and by a different mechanism: their lift
21
+ // is the ground, which only a sculpt moves, and `markTerrainSupportDependents`
22
+ // dirties them from the terrain change itself. Tracking them here would
23
+ // resolve every fence in the scene on every store update to catch an event
24
+ // this subscription never sees.
20
25
  if (!fence.supportSlabId)
21
26
  continue;
22
- signatures.set(fence.id, resolveFenceLiftElevation(fence, (id) => nodes[id]));
27
+ signatures.set(fence.id, resolveFenceLiftElevationForNodes(fence, nodes));
23
28
  }
24
29
  return signatures;
25
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pascal-app/nodes",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.3",
4
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",
@@ -23,9 +23,9 @@
23
23
  "prepublishOnly": "bun run build && bun test"
24
24
  },
25
25
  "peerDependencies": {
26
- "@pascal-app/core": "^1.0.0-beta.1",
27
- "@pascal-app/editor": "^1.0.0-beta.1",
28
- "@pascal-app/viewer": "^1.0.0-beta.1",
26
+ "@pascal-app/core": "^1.0.0-beta.3",
27
+ "@pascal-app/editor": "^1.0.0-beta.3",
28
+ "@pascal-app/viewer": "^1.0.0-beta.3",
29
29
  "@react-three/drei": "^10",
30
30
  "@react-three/fiber": "^9",
31
31
  "lucide-react": "^1",
@@ -34,9 +34,9 @@
34
34
  "zustand": "^5"
35
35
  },
36
36
  "devDependencies": {
37
- "@pascal-app/core": "^1.0.0-beta.1",
38
- "@pascal-app/editor": "^1.0.0-beta.1",
39
- "@pascal-app/viewer": "^1.0.0-beta.1",
37
+ "@pascal-app/core": "^1.0.0-beta.3",
38
+ "@pascal-app/editor": "^1.0.0-beta.3",
39
+ "@pascal-app/viewer": "^1.0.0-beta.3",
40
40
  "@pascal/typescript-config": "*",
41
41
  "@types/bun": "^1.3.0",
42
42
  "@types/node": "^22.19.12",