floorplan/slab: lower fill opacity so zones show through
The slab body covered the underlying zone fill almost completely — `opacity: 0.85` (gray) and `opacity: 0.95` (white-on-select) drowned out any zone color sitting beneath. Switched to independent `fillOpacity` (0.6 unselected / 0.45 selected) and `strokeOpacity` (0.85 / 0.96) so the outline stays crisp while the zone color reads through the fill. On the selected state the hatch overlay carries the visual weight; no need for an opaque white background underneath. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
7a92641baa
commit
3419cf8587
@@ -1,38 +1,49 @@
|
|||||||
import {
|
import {
|
||||||
type FloorplanGeometry,
|
type FloorplanGeometry,
|
||||||
type FloorplanPoint,
|
type FloorplanPoint,
|
||||||
|
type GeometryContext,
|
||||||
getRenderableSlabPolygon,
|
getRenderableSlabPolygon,
|
||||||
type SlabNode,
|
type SlabNode,
|
||||||
} from '@pascal-app/core'
|
} from '@pascal-app/core'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stage C floor-plan builder for slab. Renders the slab polygon as a
|
* Stage C floor-plan builder for slab. Renders the slab polygon as a
|
||||||
* filled path with holes cut out.
|
* filled path with holes cut out; when selected, overlays themed
|
||||||
|
* chrome (accent stroke, hatch fill) plus the full boundary editor:
|
||||||
*
|
*
|
||||||
* Uses `getRenderableSlabPolygon` (the same helper the legacy
|
* - Vertex handles on every polygon corner (orange dots).
|
||||||
* floorplan-panel.tsx uses) to compute the visual polygon — accounts
|
* - Midpoint `+` handles between vertices to insert a new vertex.
|
||||||
* for wall-clipping when a slab is auto-generated from walls.
|
* - Edge handles along each edge so the user can drag the whole
|
||||||
|
* edge perpendicular.
|
||||||
|
* - Same three handle sets for every hole in `node.holes`, with the
|
||||||
|
* `holeIndex` carried in each handle's payload.
|
||||||
|
*
|
||||||
|
* Uses `getRenderableSlabPolygon` for the visible fill (auto-slabs
|
||||||
|
* generated from walls clip to wall footprints), but vertex / edge /
|
||||||
|
* midpoint handles live on the **raw** `node.polygon` — matches the
|
||||||
|
* legacy slab boundary editor which always operates on raw data.
|
||||||
*/
|
*/
|
||||||
export function buildSlabFloorplan(node: SlabNode): FloorplanGeometry | null {
|
export function buildSlabFloorplan(node: SlabNode, ctx: GeometryContext): FloorplanGeometry | null {
|
||||||
const polygon = node.polygon
|
const polygon = node.polygon
|
||||||
if (!polygon || polygon.length < 3) return null
|
if (!polygon || polygon.length < 3) return null
|
||||||
|
|
||||||
const visualPolygon = getRenderableSlabPolygon(node)
|
const visualPolygon = getRenderableSlabPolygon(node)
|
||||||
if (!visualPolygon || visualPolygon.length < 3) return null
|
if (!visualPolygon || visualPolygon.length < 3) return null
|
||||||
|
|
||||||
|
const view = ctx.viewState
|
||||||
|
const palette = view?.palette
|
||||||
|
const isSelected = view?.selected ?? false
|
||||||
|
const isHighlighted = view?.highlighted ?? false
|
||||||
|
const showSelectedChrome = isSelected || isHighlighted
|
||||||
|
|
||||||
const outer: FloorplanPoint[] = visualPolygon.map(([x, z]) => [x, z] as FloorplanPoint)
|
const outer: FloorplanPoint[] = visualPolygon.map(([x, z]) => [x, z] as FloorplanPoint)
|
||||||
|
|
||||||
// SVG path with outer ring + hole subpaths. Each subpath uses M/L
|
|
||||||
// commands + Z to close. Holes follow the outer ring; FloorplanGeometry
|
|
||||||
// 'path' kind supports this natively (renderer passes the `d` string
|
|
||||||
// straight to the SVG <path>).
|
|
||||||
const segments: string[] = []
|
|
||||||
const ring = (points: FloorplanPoint[]) => {
|
const ring = (points: FloorplanPoint[]) => {
|
||||||
const [first, ...rest] = points
|
const [first, ...rest] = points
|
||||||
if (!first) return ''
|
if (!first) return ''
|
||||||
return [`M ${first[0]} ${first[1]}`, ...rest.map(([x, y]) => `L ${x} ${y}`), 'Z'].join(' ')
|
return [`M ${first[0]} ${first[1]}`, ...rest.map(([x, y]) => `L ${x} ${y}`), 'Z'].join(' ')
|
||||||
}
|
}
|
||||||
segments.push(ring(outer))
|
const segments: string[] = [ring(outer)]
|
||||||
|
|
||||||
const holes = node.holes ?? []
|
const holes = node.holes ?? []
|
||||||
for (const hole of holes) {
|
for (const hole of holes) {
|
||||||
@@ -41,12 +52,90 @@ export function buildSlabFloorplan(node: SlabNode): FloorplanGeometry | null {
|
|||||||
segments.push(ring(holePts))
|
segments.push(ring(holePts))
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const stroke = showSelectedChrome && palette ? palette.selectedStroke : '#475569'
|
||||||
kind: 'path',
|
const fill = showSelectedChrome ? '#ffffff' : '#cbd5e1'
|
||||||
d: segments.join(' '),
|
|
||||||
fill: '#cbd5e1',
|
// Slab body. Uses `fillOpacity` / `strokeOpacity` independently so the
|
||||||
stroke: '#475569',
|
// outline stays crisp while the fill stays translucent — zones under
|
||||||
strokeWidth: 0.03,
|
// the slab read through, and on the selected state the hatch overlay
|
||||||
opacity: 0.85,
|
// (`{ kind: 'hatch' }` below) carries the visual weight without the
|
||||||
|
// background going opaque-white.
|
||||||
|
const children: FloorplanGeometry[] = [
|
||||||
|
{
|
||||||
|
kind: 'path',
|
||||||
|
d: segments.join(' '),
|
||||||
|
fill,
|
||||||
|
fillOpacity: showSelectedChrome ? 0.45 : 0.6,
|
||||||
|
stroke,
|
||||||
|
strokeWidth: showSelectedChrome ? 0.04 : 0.03,
|
||||||
|
strokeOpacity: showSelectedChrome ? 0.96 : 0.85,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// Hatch overlay on selected — same `<defs>` pattern as the wall.
|
||||||
|
if (isSelected && palette) {
|
||||||
|
children.push({
|
||||||
|
kind: 'hatch',
|
||||||
|
points: outer,
|
||||||
|
color: palette.selectedHatch,
|
||||||
|
opacity: 0.7,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Boundary editor — visible only when the slab is the active selection.
|
||||||
|
if (isSelected) {
|
||||||
|
appendRingEditor(children, polygon, undefined)
|
||||||
|
holes.forEach((hole, holeIndex) => {
|
||||||
|
if (hole.length >= 3) appendRingEditor(children, hole, holeIndex)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { kind: 'group', children }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push vertex / midpoint / edge handles for a single ring (boundary or
|
||||||
|
* hole). `holeIndex === undefined` targets `node.polygon`; otherwise
|
||||||
|
* `node.holes[holeIndex]`. Emits in this order so the hit-test
|
||||||
|
* priority is sensible: edges first (largest hit area, lowest z),
|
||||||
|
* then midpoints, then vertices on top.
|
||||||
|
*/
|
||||||
|
function appendRingEditor(
|
||||||
|
children: FloorplanGeometry[],
|
||||||
|
ring: ReadonlyArray<readonly [number, number]>,
|
||||||
|
holeIndex: number | undefined,
|
||||||
|
): void {
|
||||||
|
for (let i = 0; i < ring.length; i++) {
|
||||||
|
const a = ring[i]!
|
||||||
|
const b = ring[(i + 1) % ring.length]!
|
||||||
|
children.push({
|
||||||
|
kind: 'edge-handle',
|
||||||
|
x1: a[0],
|
||||||
|
y1: a[1],
|
||||||
|
x2: b[0],
|
||||||
|
y2: b[1],
|
||||||
|
affordance: 'move-edge',
|
||||||
|
payload: { holeIndex, edgeIndex: i },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for (let i = 0; i < ring.length; i++) {
|
||||||
|
const a = ring[i]!
|
||||||
|
const b = ring[(i + 1) % ring.length]!
|
||||||
|
children.push({
|
||||||
|
kind: 'midpoint-handle',
|
||||||
|
point: [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2],
|
||||||
|
affordance: 'add-vertex',
|
||||||
|
payload: { holeIndex, edgeIndex: i },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for (let i = 0; i < ring.length; i++) {
|
||||||
|
const [x, z] = ring[i]!
|
||||||
|
children.push({
|
||||||
|
kind: 'endpoint-handle',
|
||||||
|
point: [x, z],
|
||||||
|
state: 'idle',
|
||||||
|
affordance: 'move-vertex',
|
||||||
|
payload: { holeIndex, vertexIndex: i },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user