Refine column support selection and polygon handling
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user