Repository navigation
Site: lot as a block in mapped surroundings, straight front setbacks #1017
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import type { Material } from 'three' | ||
| import { float, mod, positionLocal, select } from 'three/tsl' | ||
| import type { Node, NodeMaterial } from 'three/webgpu' | ||
|
|
||
| type Point = readonly [number, number] | ||
|
|
||
| /** | ||
| * True for a fragment inside the lot polygon (even-odd crossing count on its | ||
| * local x, z). Unrolled edge by edge into the graph: a parcel has tens of | ||
| * corners, so a loop and a uniform array would buy nothing. | ||
| */ | ||
| export function insideLotNode(polygon: ReadonlyArray<Point>) { | ||
| const at = positionLocal.xz | ||
| let crossings: Node<'float'> = float(0) | ||
| for (let i = 0; i < polygon.length; i++) { | ||
| const [ax, az] = polygon[i] as Point | ||
| const [bx, bz] = polygon[(i + 1) % polygon.length] as Point | ||
| if (az === bz) continue | ||
| const slope = (bx - ax) / (bz - az) | ||
| const crossesAt = at.y.sub(az).mul(slope).add(ax) | ||
| const crosses = at.y | ||
| .greaterThanEqual(Math.min(az, bz)) | ||
| .and(at.y.lessThan(Math.max(az, bz))) | ||
| .and(at.x.lessThan(crossesAt)) | ||
| crossings = crossings.add(select(crosses, float(1), float(0))) | ||
| } | ||
| return mod(crossings, float(2)).greaterThan(0.5) | ||
| } | ||
|
|
||
| /** | ||
| * The ground material cut to the lot: the terrain field is the padded, | ||
| * north-up box around the parcel, and only the parcel itself is ground the | ||
| * project owns. Per fragment, so the edge is the property line exactly and | ||
| * follows a sculpt stroke with no rebuild; the shadow pass honours the mask too. | ||
| */ | ||
| export function lotMaskedMaterial(material: Material, polygon: ReadonlyArray<Point>): Material { | ||
| const masked = material.clone() as unknown as NodeMaterial | ||
| masked.maskNode = insideLotNode(polygon) | ||
| return masked as unknown as Material | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -60,7 +60,7 @@ import { | |
| terrainGridKey, | ||
| updateDrapedHeights, | ||
| } from './terrain-drape' | ||
| import { HORIZON_PLANE_Y, terrainFootprint } from './terrain-geometry' | ||
| import { HORIZON_PLANE_Y, terrainBlockBase } from './terrain-geometry' | ||
| import { TerrainRenderer } from './terrain-renderer' | ||
|
|
||
| const Y_OFFSET = 0.01 | ||
|
|
@@ -324,10 +324,15 @@ export const SiteRenderer = ({ node }: { node: SiteNode }) => { | |
| return shape | ||
| }, [polygonPoints, slabPolygons]) | ||
|
|
||
| // The terrain footprint is punched out alongside the recessed slabs, and for the | ||
| // same reason: the disc must not cap ground that is modelled below it. | ||
| // | ||
| // biome-ignore lint/correctness/useExhaustiveDependencies: `terrainKey` is the grid signature the footprint is a function of; depending on the field itself would rebuild an 800 m disc every dab. | ||
| // With terrain the lot is a block — its ground cut to the property line, its | ||
| // skirt dropping to one flat base — and the disc lies just under that base, so | ||
| // nothing of it caps the lot and nothing of the block stands proud of it from | ||
| // the far side. The recessed slabs are still punched: a basement can go deeper. | ||
| const horizonY = useMemo( | ||
| () => (terrainGrid ? terrainBlockBase(terrainGrid) - 0.02 : HORIZON_PLANE_Y), | ||
| [terrainGrid], | ||
| ) | ||
|
|
||
| const horizonGeometry = useMemo(() => { | ||
| if (!fadeBounds || groundReplaced) return null | ||
| const radius = Math.max(fadeBounds.radius * 8, 400) | ||
|
|
@@ -339,10 +344,9 @@ export const SiteRenderer = ({ node }: { node: SiteNode }) => { | |
| shape.lineTo(Math.cos(angle) * radius, Math.sin(angle) * radius) | ||
| } | ||
| shape.closePath() | ||
| const holes = terrainGrid ? [...slabPolygons, terrainFootprint(terrainGrid)] : slabPolygons | ||
| addSlabHoles(shape, holes, fadeBounds.cx, fadeBounds.cz) | ||
| addSlabHoles(shape, slabPolygons, fadeBounds.cx, fadeBounds.cz) | ||
| return new ShapeGeometry(shape) | ||
| }, [fadeBounds, slabPolygons, terrainKey, groundReplaced]) | ||
| }, [fadeBounds, slabPolygons, groundReplaced]) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Horizon disc caps live excavationMedium Severity The horizon disc no longer punches Reviewed by Cursor Bugbot for commit 27811f6. Configure here. |
||
| useEffect(() => () => horizonGeometry?.dispose(), [horizonGeometry]) | ||
| useEffect(() => () => horizonMaterial?.dispose(), [horizonMaterial]) | ||
|
|
||
|
|
@@ -558,7 +562,12 @@ export const SiteRenderer = ({ node }: { node: SiteNode }) => { | |
|
|
||
| {/* Sculpted ground, when the site has terrain */} | ||
| {showSiteSurfaces && showTerrain && ( | ||
| <TerrainRenderer holes={slabPolygons} material={groundMaterial} site={node} /> | ||
| <TerrainRenderer | ||
| holes={slabPolygons} | ||
| material={groundMaterial} | ||
| polygon={polygonPoints} | ||
| site={node} | ||
| /> | ||
| )} | ||
|
|
||
| {/* Ground fill: site polygon with slab holes, occludes below-grade geometry */} | ||
|
|
@@ -577,7 +586,7 @@ export const SiteRenderer = ({ node }: { node: SiteNode }) => { | |
| <mesh | ||
| geometry={horizonGeometry} | ||
| material={horizonMaterial} | ||
| position={[fadeBounds.cx, HORIZON_PLANE_Y, fadeBounds.cz]} | ||
| position={[fadeBounds.cx, horizonY, fadeBounds.cz]} | ||
| raycast={noopRaycast} | ||
| receiveShadow | ||
| rotation={[-Math.PI / 2, 0, 0]} | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
3D setbacks miss straight frontage
Medium Severity
The new nearly-straight frontage inheritance lives only in the editor
setbackEnvelope. The nodes copy, which the 3D dashed overlay uses and which is documented as the same rules, still treats the leftover half as a side yard, so a split frontage still draws a curved envelope in the scene.Additional Locations (1)
packages/nodes/src/site/renderer.tsx#L392-L393Reviewed by Cursor Bugbot for commit 27811f6. Configure here.