Refine column support selection and polygon handling
This commit is contained in:
@@ -193,7 +193,6 @@
|
|||||||
"name": "@pascal-app/viewer",
|
"name": "@pascal-app/viewer",
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"polygon-clipping": "^0.15.7",
|
|
||||||
"three-bvh-csg": "^0.0.18",
|
"three-bvh-csg": "^0.0.18",
|
||||||
"three-mesh-bvh": "^0.9.8",
|
"three-mesh-bvh": "^0.9.8",
|
||||||
"zustand": "^5",
|
"zustand": "^5",
|
||||||
@@ -1289,8 +1288,6 @@
|
|||||||
|
|
||||||
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
|
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
|
||||||
|
|
||||||
"polygon-clipping": ["polygon-clipping@0.15.7", "", { "dependencies": { "robust-predicates": "^3.0.2", "splaytree": "^3.1.0" } }, "sha512-nhfdr83ECBg6xtqOAJab1tbksbBAOMUltN60bU+llHVOL0e5Onm1WpAXXWXVB39L8AJFssoIhEVuy/S90MmotA=="],
|
|
||||||
|
|
||||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||||
|
|
||||||
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
||||||
@@ -1351,8 +1348,6 @@
|
|||||||
|
|
||||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||||
|
|
||||||
"robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
|
|
||||||
|
|
||||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||||
|
|
||||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||||
@@ -1409,8 +1404,6 @@
|
|||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
"splaytree": ["splaytree@3.2.3", "", {}, "sha512-7OXrNWzy6CK+r7Ch9OLPBDTKfB6XlWHjX4P0RU5B3IgFuWPeYN0XtRtlexGRjgbQxpfaUve6jTAwBGWuGntz/w=="],
|
|
||||||
|
|
||||||
"stats-gl": ["stats-gl@2.4.2", "", { "dependencies": { "@types/three": "*", "three": "^0.170.0" } }, "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ=="],
|
"stats-gl": ["stats-gl@2.4.2", "", { "dependencies": { "@types/three": "*", "three": "^0.170.0" } }, "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ=="],
|
||||||
|
|
||||||
"stats.js": ["stats.js@0.17.0", "", {}, "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw=="],
|
"stats.js": ["stats.js@0.17.0", "", {}, "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw=="],
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { useViewer } from '@pascal-app/viewer'
|
|||||||
import { Move, Trash2 } from 'lucide-react'
|
import { Move, Trash2 } from 'lucide-react'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { sfxEmitter } from '../../../lib/sfx-bus'
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
||||||
|
import { cn } from '../../../lib/utils'
|
||||||
import useEditor from '../../../store/use-editor'
|
import useEditor from '../../../store/use-editor'
|
||||||
import { ActionButton, ActionGroup } from '../controls/action-button'
|
import { ActionButton, ActionGroup } from '../controls/action-button'
|
||||||
import { PanelSection } from '../controls/panel-section'
|
import { PanelSection } from '../controls/panel-section'
|
||||||
@@ -76,6 +77,20 @@ const COLUMN_PROPORTION_OPTIONS = Object.entries(COLUMN_PROPORTION_PRESETS).map(
|
|||||||
label: preset.label,
|
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) {
|
function clamp(value: number, min: number, max: number) {
|
||||||
return Math.min(max, Math.max(min, value))
|
return Math.min(max, Math.max(min, value))
|
||||||
}
|
}
|
||||||
@@ -235,37 +250,39 @@ export function ColumnPanel() {
|
|||||||
</PanelSection>
|
</PanelSection>
|
||||||
|
|
||||||
<PanelSection title="Shape">
|
<PanelSection title="Shape">
|
||||||
<select
|
<div className="grid grid-cols-2 gap-1.5 px-1 pt-1">
|
||||||
className={SELECT_CLASS}
|
{SUPPORT_STYLE_OPTIONS.map((option) => {
|
||||||
onChange={(event) => {
|
const isSelected = supportStyle === option.value
|
||||||
const nextSupportStyle = event.target.value as ColumnNode['supportStyle']
|
return (
|
||||||
handleUpdate({
|
<button
|
||||||
supportStyle: nextSupportStyle,
|
className={cn(
|
||||||
...(nextSupportStyle !== 'vertical'
|
'flex min-h-12 items-center rounded-lg border px-2.5 text-left text-xs transition-colors',
|
||||||
? {
|
isSelected
|
||||||
crossSection: 'rectangular',
|
? 'border-orange-400/60 bg-orange-400/10 text-foreground'
|
||||||
width: node.braceWidth ?? node.width,
|
: 'border-border/50 bg-[#2C2C2E] text-muted-foreground hover:bg-[#3e3e3e] hover:text-foreground',
|
||||||
depth: node.braceDepth ?? node.depth,
|
)}
|
||||||
baseStyle: 'none',
|
key={option.value}
|
||||||
capitalStyle: 'none',
|
onClick={() =>
|
||||||
}
|
handleUpdate({
|
||||||
: {}),
|
supportStyle: option.value,
|
||||||
})
|
...(option.value !== 'vertical'
|
||||||
}}
|
? {
|
||||||
value={supportStyle}
|
crossSection: 'rectangular',
|
||||||
>
|
width: node.braceWidth ?? node.width,
|
||||||
<option value="vertical">Vertical</option>
|
depth: node.braceDepth ?? node.depth,
|
||||||
<option value="a-frame">A-Frame</option>
|
baseStyle: 'none',
|
||||||
<option value="y-frame">Y Support</option>
|
capitalStyle: 'none',
|
||||||
<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>
|
type="button"
|
||||||
<option value="trestle">Trestle Frame</option>
|
>
|
||||||
<option value="portal-frame">Portal Frame</option>
|
<span className="truncate font-medium">{option.label}</span>
|
||||||
<option value="box-frame">Box Frame</option>
|
</button>
|
||||||
</select>
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
{isBraceSupport ? (
|
{isBraceSupport ? (
|
||||||
<>
|
<>
|
||||||
<SliderControl
|
<SliderControl
|
||||||
|
|||||||
@@ -29,7 +29,6 @@
|
|||||||
"three": "^0.184"
|
"three": "^0.184"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"polygon-clipping": "^0.15.7",
|
|
||||||
"three-bvh-csg": "^0.0.18",
|
"three-bvh-csg": "^0.0.18",
|
||||||
"three-mesh-bvh": "^0.9.8",
|
"three-mesh-bvh": "^0.9.8",
|
||||||
"zustand": "^5"
|
"zustand": "^5"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { type SiteNode, type SlabNode, useRegistry, useScene } from '@pascal-app/core'
|
import { type SiteNode, type SlabNode, useRegistry, useScene } from '@pascal-app/core'
|
||||||
import polygonClipping from 'polygon-clipping'
|
|
||||||
import { useMemo, useRef } from 'react'
|
import { useMemo, useRef } from 'react'
|
||||||
import { BufferGeometry, Float32BufferAttribute, type Group, Path, Shape } from 'three'
|
import { BufferGeometry, Float32BufferAttribute, type Group, Path, Shape } from 'three'
|
||||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
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])
|
for (let i = 1; i < pts.length; i++) shape.lineTo(pts[i]![0], -pts[i]![1])
|
||||||
shape.closePath()
|
shape.closePath()
|
||||||
|
|
||||||
if (slabPolygons.length > 0) {
|
for (const polygon of slabPolygons) {
|
||||||
const multiPolygons = slabPolygons.map((p) => [
|
if (polygon.length < 3) continue
|
||||||
p.map((pt) => [pt[0], -pt[1]] as [number, number]),
|
|
||||||
])
|
const hole = new Path()
|
||||||
const unioned = polygonClipping.union(
|
hole.moveTo(polygon[0]![0], -polygon[0]![1])
|
||||||
multiPolygons[0] as polygonClipping.Polygon,
|
for (let i = 1; i < polygon.length; i++) {
|
||||||
...(multiPolygons.slice(1) as polygonClipping.Polygon[]),
|
hole.lineTo(polygon[i]![0], -polygon[i]![1])
|
||||||
)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
hole.closePath()
|
||||||
|
shape.holes.push(hole)
|
||||||
}
|
}
|
||||||
|
|
||||||
return shape
|
return shape
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { type LevelNode, useScene } from '@pascal-app/core'
|
import { type LevelNode, useScene } from '@pascal-app/core'
|
||||||
import polygonClipping from 'polygon-clipping'
|
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
import useViewer from '../../store/use-viewer'
|
import useViewer from '../../store/use-viewer'
|
||||||
@@ -63,33 +62,16 @@ export const GroundOccluder = () => {
|
|||||||
polygons.push(node.polygon as [number, number][])
|
polygons.push(node.polygon as [number, number][])
|
||||||
})
|
})
|
||||||
|
|
||||||
if (polygons.length > 0) {
|
for (const polygon of polygons) {
|
||||||
// Format for polygon-clipping: [[[x, y], [x, y], ...]]
|
if (polygon.length < 3) continue
|
||||||
const multiPolygons = polygons.map((pts) => {
|
|
||||||
const ring = pts.map((p) => [p[0], -p[1]] as [number, number]) // Negate Y (which was Z)
|
|
||||||
return [ring]
|
|
||||||
})
|
|
||||||
|
|
||||||
// Union all polygons together to prevent artifacts from overlapping
|
const hole = new THREE.Path()
|
||||||
const unionedPolygons = polygonClipping.union(multiPolygons[0]!, ...multiPolygons.slice(1))
|
hole.moveTo(polygon[0]![0], -polygon[0]![1])
|
||||||
|
for (let i = 1; i < polygon.length; i++) {
|
||||||
// Add each resulting unioned polygon as a hole
|
hole.lineTo(polygon[i]![0], -polygon[i]![1])
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
hole.closePath()
|
||||||
|
s.holes.push(hole)
|
||||||
}
|
}
|
||||||
|
|
||||||
return s
|
return s
|
||||||
|
|||||||
Reference in New Issue
Block a user