Refine column support selection and polygon handling

This commit is contained in:
sudhir
2026-05-10 12:51:57 +05:30
parent 0514363136
commit 4956de4910
5 changed files with 65 additions and 83 deletions
@@ -11,6 +11,7 @@ import { useViewer } from '@pascal-app/viewer'
import { Move, Trash2 } from 'lucide-react'
import { useCallback } from 'react'
import { sfxEmitter } from '../../../lib/sfx-bus'
import { cn } from '../../../lib/utils'
import useEditor from '../../../store/use-editor'
import { ActionButton, ActionGroup } from '../controls/action-button'
import { PanelSection } from '../controls/panel-section'
@@ -76,6 +77,20 @@ const COLUMN_PROPORTION_OPTIONS = Object.entries(COLUMN_PROPORTION_PRESETS).map(
label: preset.label,
}))
const SUPPORT_STYLE_OPTIONS: Array<{ label: string; value: ColumnNode['supportStyle'] }> = [
{ label: 'Vertical', value: 'vertical' },
{ label: 'A-Frame', value: 'a-frame' },
{ label: 'Y Support', value: 'y-frame' },
{ label: 'V Support', value: 'v-frame' },
{ label: 'X Brace', value: 'x-brace' },
{ label: 'K Brace', value: 'k-brace' },
{ label: 'Single Strut', value: 'single-strut' },
{ label: 'Tripod', value: 'tripod' },
{ label: 'Trestle', value: 'trestle' },
{ label: 'Portal Frame', value: 'portal-frame' },
{ label: 'Box Frame', value: 'box-frame' },
]
function clamp(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value))
}
@@ -235,37 +250,39 @@ export function ColumnPanel() {
</PanelSection>
<PanelSection title="Shape">
<select
className={SELECT_CLASS}
onChange={(event) => {
const nextSupportStyle = event.target.value as ColumnNode['supportStyle']
handleUpdate({
supportStyle: nextSupportStyle,
...(nextSupportStyle !== 'vertical'
? {
crossSection: 'rectangular',
width: node.braceWidth ?? node.width,
depth: node.braceDepth ?? node.depth,
baseStyle: 'none',
capitalStyle: 'none',
}
: {}),
})
}}
value={supportStyle}
>
<option value="vertical">Vertical</option>
<option value="a-frame">A-Frame</option>
<option value="y-frame">Y Support</option>
<option value="v-frame">V Support</option>
<option value="x-brace">X Brace</option>
<option value="k-brace">K Brace</option>
<option value="single-strut">Single Strut</option>
<option value="tripod">Tripod Support</option>
<option value="trestle">Trestle Frame</option>
<option value="portal-frame">Portal Frame</option>
<option value="box-frame">Box Frame</option>
</select>
<div className="grid grid-cols-2 gap-1.5 px-1 pt-1">
{SUPPORT_STYLE_OPTIONS.map((option) => {
const isSelected = supportStyle === option.value
return (
<button
className={cn(
'flex min-h-12 items-center rounded-lg border px-2.5 text-left text-xs transition-colors',
isSelected
? 'border-orange-400/60 bg-orange-400/10 text-foreground'
: 'border-border/50 bg-[#2C2C2E] text-muted-foreground hover:bg-[#3e3e3e] hover:text-foreground',
)}
key={option.value}
onClick={() =>
handleUpdate({
supportStyle: option.value,
...(option.value !== 'vertical'
? {
crossSection: 'rectangular',
width: node.braceWidth ?? node.width,
depth: node.braceDepth ?? node.depth,
baseStyle: 'none',
capitalStyle: 'none',
}
: {}),
})
}
type="button"
>
<span className="truncate font-medium">{option.label}</span>
</button>
)
})}
</div>
{isBraceSupport ? (
<>
<SliderControl
-1
View File
@@ -29,7 +29,6 @@
"three": "^0.184"
},
"dependencies": {
"polygon-clipping": "^0.15.7",
"three-bvh-csg": "^0.0.18",
"three-mesh-bvh": "^0.9.8",
"zustand": "^5"
@@ -1,5 +1,4 @@
import { type SiteNode, type SlabNode, useRegistry, useScene } from '@pascal-app/core'
import polygonClipping from 'polygon-clipping'
import { useMemo, useRef } from 'react'
import { BufferGeometry, Float32BufferAttribute, type Group, Path, Shape } from 'three'
import { useNodeEvents } from '../../../hooks/use-node-events'
@@ -88,24 +87,16 @@ export const SiteRenderer = ({ node }: { node: SiteNode }) => {
for (let i = 1; i < pts.length; i++) shape.lineTo(pts[i]![0], -pts[i]![1])
shape.closePath()
if (slabPolygons.length > 0) {
const multiPolygons = slabPolygons.map((p) => [
p.map((pt) => [pt[0], -pt[1]] as [number, number]),
])
const unioned = polygonClipping.union(
multiPolygons[0] as polygonClipping.Polygon,
...(multiPolygons.slice(1) as polygonClipping.Polygon[]),
)
for (const geom of unioned) {
const ring = geom[0]
if (ring && ring.length > 0) {
const hole = new Path()
hole.moveTo(ring[0]![0], ring[0]![1])
for (let i = 1; i < ring.length; i++) hole.lineTo(ring[i]![0], ring[i]![1])
hole.closePath()
shape.holes.push(hole)
}
for (const polygon of slabPolygons) {
if (polygon.length < 3) continue
const hole = new Path()
hole.moveTo(polygon[0]![0], -polygon[0]![1])
for (let i = 1; i < polygon.length; i++) {
hole.lineTo(polygon[i]![0], -polygon[i]![1])
}
hole.closePath()
shape.holes.push(hole)
}
return shape
@@ -1,5 +1,4 @@
import { type LevelNode, useScene } from '@pascal-app/core'
import polygonClipping from 'polygon-clipping'
import { useMemo } from 'react'
import * as THREE from 'three'
import useViewer from '../../store/use-viewer'
@@ -63,33 +62,16 @@ export const GroundOccluder = () => {
polygons.push(node.polygon as [number, number][])
})
if (polygons.length > 0) {
// Format for polygon-clipping: [[[x, y], [x, y], ...]]
const multiPolygons = polygons.map((pts) => {
const ring = pts.map((p) => [p[0], -p[1]] as [number, number]) // Negate Y (which was Z)
return [ring]
})
for (const polygon of polygons) {
if (polygon.length < 3) continue
// Union all polygons together to prevent artifacts from overlapping
const unionedPolygons = polygonClipping.union(multiPolygons[0]!, ...multiPolygons.slice(1))
// Add each resulting unioned polygon as a hole
for (const geom of unionedPolygons) {
// First ring in each geometry is the exterior ring
if (geom.length > 0) {
const ring = geom[0]!
const hole = new THREE.Path()
if (ring.length > 0) {
hole.moveTo(ring[0]![0], ring[0]![1])
for (let i = 1; i < ring.length; i++) {
hole.lineTo(ring[i]![0], ring[i]![1])
}
hole.closePath()
s.holes.push(hole)
}
}
const hole = new THREE.Path()
hole.moveTo(polygon[0]![0], -polygon[0]![1])
for (let i = 1; i < polygon.length; i++) {
hole.lineTo(polygon[i]![0], -polygon[i]![1])
}
hole.closePath()
s.holes.push(hole)
}
return s