Add procedural support column variants
This commit is contained in:
@@ -39,6 +39,7 @@ export {
|
||||
ColumnShaftDetail,
|
||||
ColumnShaftProfile,
|
||||
ColumnStyle,
|
||||
ColumnSupportStyle,
|
||||
} from './nodes/column'
|
||||
export { DoorNode, DoorSegment } from './nodes/door'
|
||||
export { FenceBaseStyle, FenceNode, FenceStyle } from './nodes/fence'
|
||||
|
||||
@@ -56,6 +56,20 @@ export const ColumnRingPlacement = z.enum(['ends', 'even', 'top', 'bottom'])
|
||||
|
||||
export const ColumnCarvingPlacement = z.enum(['shaft', 'base', 'capital', 'all'])
|
||||
|
||||
export const ColumnSupportStyle = z.enum([
|
||||
'vertical',
|
||||
'a-frame',
|
||||
'y-frame',
|
||||
'v-frame',
|
||||
'x-brace',
|
||||
'k-brace',
|
||||
'single-strut',
|
||||
'tripod',
|
||||
'trestle',
|
||||
'portal-frame',
|
||||
'box-frame',
|
||||
])
|
||||
|
||||
export type ColumnStyle = z.infer<typeof ColumnStyle>
|
||||
export type ColumnCrossSection = z.infer<typeof ColumnCrossSection>
|
||||
export type ColumnShaftProfile = z.infer<typeof ColumnShaftProfile>
|
||||
@@ -65,6 +79,7 @@ export type ColumnBaseStyle = z.infer<typeof ColumnBaseStyle>
|
||||
export type ColumnCapitalStyle = z.infer<typeof ColumnCapitalStyle>
|
||||
export type ColumnRingPlacement = z.infer<typeof ColumnRingPlacement>
|
||||
export type ColumnCarvingPlacement = z.infer<typeof ColumnCarvingPlacement>
|
||||
export type ColumnSupportStyle = z.infer<typeof ColumnSupportStyle>
|
||||
|
||||
export const ColumnNode = BaseNode.extend({
|
||||
id: objectId('column'),
|
||||
@@ -136,6 +151,12 @@ export const ColumnNode = BaseNode.extend({
|
||||
lowerBandCarvingLevel: z.number().int().min(0).max(4).default(0),
|
||||
dentilCount: z.number().int().min(0).max(48).default(0),
|
||||
beadCount: z.number().int().min(0).max(64).default(0),
|
||||
supportStyle: ColumnSupportStyle.default('vertical'),
|
||||
braceWidth: z.number().positive().default(0.16),
|
||||
braceDepth: z.number().positive().default(0.16),
|
||||
braceBottomSpread: z.number().min(0.2).default(1.2),
|
||||
braceTopSpread: z.number().min(0).default(0.12),
|
||||
bracePlateEnabled: z.boolean().default(true),
|
||||
material: MaterialSchema.optional(),
|
||||
materialPreset: z.string().optional(),
|
||||
}).describe(dedent`
|
||||
@@ -150,6 +171,7 @@ export const ColumnNode = BaseNode.extend({
|
||||
- baseStyle/capitalStyle: procedural base and top treatment with tier/detail controls
|
||||
- baseHeight/capitalHeight: bottom and top block proportions
|
||||
- ring/flute/spiral/panel/lathe/carving fields: procedural detail controls
|
||||
- supportStyle/brace fields: vertical column or procedural support assembly
|
||||
`)
|
||||
|
||||
export const COLUMN_PRESETS = {
|
||||
@@ -403,6 +425,566 @@ export const COLUMN_PRESETS = {
|
||||
dentilCount: 0,
|
||||
beadCount: 0,
|
||||
},
|
||||
aFrameSupport: {
|
||||
label: 'A-Frame Support',
|
||||
supportStyle: 'a-frame',
|
||||
style: 'faceted',
|
||||
crossSection: 'rectangular',
|
||||
height: 2.6,
|
||||
radius: 0.08,
|
||||
width: 0.16,
|
||||
depth: 0.16,
|
||||
edgeSoftness: 0.012,
|
||||
baseHeight: 0,
|
||||
capitalHeight: 0,
|
||||
shaftProfile: 'straight',
|
||||
shaftTaper: 0,
|
||||
shaftBulge: 0,
|
||||
shaftStartScale: 1,
|
||||
shaftEndScale: 1,
|
||||
shaftSegmentCount: 1,
|
||||
shaftTwistStep: 0,
|
||||
shaftCornerRadius: 0.012,
|
||||
shaftDetail: 'none',
|
||||
baseStyle: 'none',
|
||||
baseWidthScale: 1,
|
||||
baseDepthScale: 1,
|
||||
baseTierCount: 1,
|
||||
baseStepSpread: 0.34,
|
||||
basePlinthHeightRatio: 0.44,
|
||||
baseRoundBandScale: 0.92,
|
||||
baseNeckScale: 0.72,
|
||||
baseRoundBandCount: 0,
|
||||
baseRibCount: 0,
|
||||
baseCarvingLevel: 0,
|
||||
capitalStyle: 'none',
|
||||
capitalWidthScale: 1,
|
||||
capitalDepthScale: 1,
|
||||
capitalTierCount: 1,
|
||||
capitalStepSpread: 0.34,
|
||||
capitalBandCount: 0,
|
||||
capitalCarvingLevel: 0,
|
||||
ringCount: 0,
|
||||
ringSpread: 0.16,
|
||||
fluteCount: 0,
|
||||
spiralTwist: 0,
|
||||
spiralRibCount: 0,
|
||||
panelCount: 0,
|
||||
latheRingCount: 0,
|
||||
carvingLevel: 0,
|
||||
lowerBandEnabled: false,
|
||||
dentilCount: 0,
|
||||
beadCount: 0,
|
||||
braceWidth: 0.16,
|
||||
braceDepth: 0.16,
|
||||
braceBottomSpread: 1.2,
|
||||
braceTopSpread: 0.06,
|
||||
bracePlateEnabled: false,
|
||||
},
|
||||
yFrameSupport: {
|
||||
label: 'Y Support',
|
||||
supportStyle: 'y-frame',
|
||||
style: 'faceted',
|
||||
crossSection: 'rectangular',
|
||||
height: 2.7,
|
||||
radius: 0.08,
|
||||
width: 0.16,
|
||||
depth: 0.16,
|
||||
edgeSoftness: 0.012,
|
||||
baseHeight: 0,
|
||||
capitalHeight: 0,
|
||||
shaftProfile: 'straight',
|
||||
shaftTaper: 0,
|
||||
shaftBulge: 0,
|
||||
shaftStartScale: 1,
|
||||
shaftEndScale: 1,
|
||||
shaftSegmentCount: 1,
|
||||
shaftTwistStep: 0,
|
||||
shaftCornerRadius: 0.012,
|
||||
shaftDetail: 'none',
|
||||
baseStyle: 'none',
|
||||
baseWidthScale: 1,
|
||||
baseDepthScale: 1,
|
||||
baseTierCount: 1,
|
||||
baseStepSpread: 0.34,
|
||||
basePlinthHeightRatio: 0.44,
|
||||
baseRoundBandScale: 0.92,
|
||||
baseNeckScale: 0.72,
|
||||
baseRoundBandCount: 0,
|
||||
baseRibCount: 0,
|
||||
baseCarvingLevel: 0,
|
||||
capitalStyle: 'none',
|
||||
capitalWidthScale: 1,
|
||||
capitalDepthScale: 1,
|
||||
capitalTierCount: 1,
|
||||
capitalStepSpread: 0.34,
|
||||
capitalBandCount: 0,
|
||||
capitalCarvingLevel: 0,
|
||||
ringCount: 0,
|
||||
ringSpread: 0.16,
|
||||
fluteCount: 0,
|
||||
spiralTwist: 0,
|
||||
spiralRibCount: 0,
|
||||
panelCount: 0,
|
||||
latheRingCount: 0,
|
||||
carvingLevel: 0,
|
||||
lowerBandEnabled: false,
|
||||
dentilCount: 0,
|
||||
beadCount: 0,
|
||||
braceWidth: 0.16,
|
||||
braceDepth: 0.16,
|
||||
braceBottomSpread: 0.2,
|
||||
braceTopSpread: 1,
|
||||
bracePlateEnabled: false,
|
||||
},
|
||||
vFrameSupport: {
|
||||
label: 'V Support',
|
||||
supportStyle: 'v-frame',
|
||||
style: 'faceted',
|
||||
crossSection: 'rectangular',
|
||||
height: 2.6,
|
||||
radius: 0.08,
|
||||
width: 0.16,
|
||||
depth: 0.16,
|
||||
edgeSoftness: 0.012,
|
||||
baseHeight: 0,
|
||||
capitalHeight: 0,
|
||||
shaftProfile: 'straight',
|
||||
shaftTaper: 0,
|
||||
shaftBulge: 0,
|
||||
shaftStartScale: 1,
|
||||
shaftEndScale: 1,
|
||||
shaftSegmentCount: 1,
|
||||
shaftTwistStep: 0,
|
||||
shaftCornerRadius: 0.012,
|
||||
shaftDetail: 'none',
|
||||
baseStyle: 'none',
|
||||
baseWidthScale: 1,
|
||||
baseDepthScale: 1,
|
||||
baseTierCount: 1,
|
||||
baseStepSpread: 0.34,
|
||||
basePlinthHeightRatio: 0.44,
|
||||
baseRoundBandScale: 0.92,
|
||||
baseNeckScale: 0.72,
|
||||
baseRoundBandCount: 0,
|
||||
baseRibCount: 0,
|
||||
baseCarvingLevel: 0,
|
||||
capitalStyle: 'none',
|
||||
capitalWidthScale: 1,
|
||||
capitalDepthScale: 1,
|
||||
capitalTierCount: 1,
|
||||
capitalStepSpread: 0.34,
|
||||
capitalBandCount: 0,
|
||||
capitalCarvingLevel: 0,
|
||||
ringCount: 0,
|
||||
ringSpread: 0.16,
|
||||
fluteCount: 0,
|
||||
spiralTwist: 0,
|
||||
spiralRibCount: 0,
|
||||
panelCount: 0,
|
||||
latheRingCount: 0,
|
||||
carvingLevel: 0,
|
||||
lowerBandEnabled: false,
|
||||
dentilCount: 0,
|
||||
beadCount: 0,
|
||||
braceWidth: 0.16,
|
||||
braceDepth: 0.16,
|
||||
braceBottomSpread: 0.2,
|
||||
braceTopSpread: 1,
|
||||
bracePlateEnabled: false,
|
||||
},
|
||||
xBraceSupport: {
|
||||
label: 'X Brace',
|
||||
supportStyle: 'x-brace',
|
||||
style: 'faceted',
|
||||
crossSection: 'rectangular',
|
||||
height: 2.6,
|
||||
radius: 0.07,
|
||||
width: 0.14,
|
||||
depth: 0.14,
|
||||
edgeSoftness: 0.01,
|
||||
baseHeight: 0,
|
||||
capitalHeight: 0,
|
||||
shaftProfile: 'straight',
|
||||
shaftTaper: 0,
|
||||
shaftBulge: 0,
|
||||
shaftStartScale: 1,
|
||||
shaftEndScale: 1,
|
||||
shaftSegmentCount: 1,
|
||||
shaftTwistStep: 0,
|
||||
shaftCornerRadius: 0.01,
|
||||
shaftDetail: 'none',
|
||||
baseStyle: 'none',
|
||||
baseWidthScale: 1,
|
||||
baseDepthScale: 1,
|
||||
baseTierCount: 1,
|
||||
baseStepSpread: 0.34,
|
||||
basePlinthHeightRatio: 0.44,
|
||||
baseRoundBandScale: 0.92,
|
||||
baseNeckScale: 0.72,
|
||||
baseRoundBandCount: 0,
|
||||
baseRibCount: 0,
|
||||
baseCarvingLevel: 0,
|
||||
capitalStyle: 'none',
|
||||
capitalWidthScale: 1,
|
||||
capitalDepthScale: 1,
|
||||
capitalTierCount: 1,
|
||||
capitalStepSpread: 0.34,
|
||||
capitalBandCount: 0,
|
||||
capitalCarvingLevel: 0,
|
||||
ringCount: 0,
|
||||
ringSpread: 0.16,
|
||||
fluteCount: 0,
|
||||
spiralTwist: 0,
|
||||
spiralRibCount: 0,
|
||||
panelCount: 0,
|
||||
latheRingCount: 0,
|
||||
carvingLevel: 0,
|
||||
lowerBandEnabled: false,
|
||||
dentilCount: 0,
|
||||
beadCount: 0,
|
||||
braceWidth: 0.14,
|
||||
braceDepth: 0.14,
|
||||
braceBottomSpread: 1,
|
||||
braceTopSpread: 1,
|
||||
bracePlateEnabled: false,
|
||||
},
|
||||
kBraceSupport: {
|
||||
label: 'K Brace',
|
||||
supportStyle: 'k-brace',
|
||||
style: 'faceted',
|
||||
crossSection: 'rectangular',
|
||||
height: 2.6,
|
||||
radius: 0.07,
|
||||
width: 0.14,
|
||||
depth: 0.14,
|
||||
edgeSoftness: 0.01,
|
||||
baseHeight: 0,
|
||||
capitalHeight: 0,
|
||||
shaftProfile: 'straight',
|
||||
shaftTaper: 0,
|
||||
shaftBulge: 0,
|
||||
shaftStartScale: 1,
|
||||
shaftEndScale: 1,
|
||||
shaftSegmentCount: 1,
|
||||
shaftTwistStep: 0,
|
||||
shaftCornerRadius: 0.01,
|
||||
shaftDetail: 'none',
|
||||
baseStyle: 'none',
|
||||
baseWidthScale: 1,
|
||||
baseDepthScale: 1,
|
||||
baseTierCount: 1,
|
||||
baseStepSpread: 0.34,
|
||||
basePlinthHeightRatio: 0.44,
|
||||
baseRoundBandScale: 0.92,
|
||||
baseNeckScale: 0.72,
|
||||
baseRoundBandCount: 0,
|
||||
baseRibCount: 0,
|
||||
baseCarvingLevel: 0,
|
||||
capitalStyle: 'none',
|
||||
capitalWidthScale: 1,
|
||||
capitalDepthScale: 1,
|
||||
capitalTierCount: 1,
|
||||
capitalStepSpread: 0.34,
|
||||
capitalBandCount: 0,
|
||||
capitalCarvingLevel: 0,
|
||||
ringCount: 0,
|
||||
ringSpread: 0.16,
|
||||
fluteCount: 0,
|
||||
spiralTwist: 0,
|
||||
spiralRibCount: 0,
|
||||
panelCount: 0,
|
||||
latheRingCount: 0,
|
||||
carvingLevel: 0,
|
||||
lowerBandEnabled: false,
|
||||
dentilCount: 0,
|
||||
beadCount: 0,
|
||||
braceWidth: 0.14,
|
||||
braceDepth: 0.14,
|
||||
braceBottomSpread: 1,
|
||||
braceTopSpread: 1,
|
||||
bracePlateEnabled: false,
|
||||
},
|
||||
singleStrutSupport: {
|
||||
label: 'Single Strut',
|
||||
supportStyle: 'single-strut',
|
||||
style: 'faceted',
|
||||
crossSection: 'rectangular',
|
||||
height: 2.6,
|
||||
radius: 0.07,
|
||||
width: 0.14,
|
||||
depth: 0.14,
|
||||
edgeSoftness: 0.01,
|
||||
baseHeight: 0,
|
||||
capitalHeight: 0,
|
||||
shaftProfile: 'straight',
|
||||
shaftTaper: 0,
|
||||
shaftBulge: 0,
|
||||
shaftStartScale: 1,
|
||||
shaftEndScale: 1,
|
||||
shaftSegmentCount: 1,
|
||||
shaftTwistStep: 0,
|
||||
shaftCornerRadius: 0.01,
|
||||
shaftDetail: 'none',
|
||||
baseStyle: 'none',
|
||||
baseWidthScale: 1,
|
||||
baseDepthScale: 1,
|
||||
baseTierCount: 1,
|
||||
baseStepSpread: 0.34,
|
||||
basePlinthHeightRatio: 0.44,
|
||||
baseRoundBandScale: 0.92,
|
||||
baseNeckScale: 0.72,
|
||||
baseRoundBandCount: 0,
|
||||
baseRibCount: 0,
|
||||
baseCarvingLevel: 0,
|
||||
capitalStyle: 'none',
|
||||
capitalWidthScale: 1,
|
||||
capitalDepthScale: 1,
|
||||
capitalTierCount: 1,
|
||||
capitalStepSpread: 0.34,
|
||||
capitalBandCount: 0,
|
||||
capitalCarvingLevel: 0,
|
||||
ringCount: 0,
|
||||
ringSpread: 0.16,
|
||||
fluteCount: 0,
|
||||
spiralTwist: 0,
|
||||
spiralRibCount: 0,
|
||||
panelCount: 0,
|
||||
latheRingCount: 0,
|
||||
carvingLevel: 0,
|
||||
lowerBandEnabled: false,
|
||||
dentilCount: 0,
|
||||
beadCount: 0,
|
||||
braceWidth: 0.14,
|
||||
braceDepth: 0.14,
|
||||
braceBottomSpread: 1,
|
||||
braceTopSpread: 1,
|
||||
bracePlateEnabled: false,
|
||||
},
|
||||
tripodSupport: {
|
||||
label: 'Tripod Support',
|
||||
supportStyle: 'tripod',
|
||||
style: 'faceted',
|
||||
crossSection: 'rectangular',
|
||||
height: 2.6,
|
||||
radius: 0.07,
|
||||
width: 0.14,
|
||||
depth: 0.14,
|
||||
edgeSoftness: 0.01,
|
||||
baseHeight: 0,
|
||||
capitalHeight: 0,
|
||||
shaftProfile: 'straight',
|
||||
shaftTaper: 0,
|
||||
shaftBulge: 0,
|
||||
shaftStartScale: 1,
|
||||
shaftEndScale: 1,
|
||||
shaftSegmentCount: 1,
|
||||
shaftTwistStep: 0,
|
||||
shaftCornerRadius: 0.01,
|
||||
shaftDetail: 'none',
|
||||
baseStyle: 'none',
|
||||
baseWidthScale: 1,
|
||||
baseDepthScale: 1,
|
||||
baseTierCount: 1,
|
||||
baseStepSpread: 0.34,
|
||||
basePlinthHeightRatio: 0.44,
|
||||
baseRoundBandScale: 0.92,
|
||||
baseNeckScale: 0.72,
|
||||
baseRoundBandCount: 0,
|
||||
baseRibCount: 0,
|
||||
baseCarvingLevel: 0,
|
||||
capitalStyle: 'none',
|
||||
capitalWidthScale: 1,
|
||||
capitalDepthScale: 1,
|
||||
capitalTierCount: 1,
|
||||
capitalStepSpread: 0.34,
|
||||
capitalBandCount: 0,
|
||||
capitalCarvingLevel: 0,
|
||||
ringCount: 0,
|
||||
ringSpread: 0.16,
|
||||
fluteCount: 0,
|
||||
spiralTwist: 0,
|
||||
spiralRibCount: 0,
|
||||
panelCount: 0,
|
||||
latheRingCount: 0,
|
||||
carvingLevel: 0,
|
||||
lowerBandEnabled: false,
|
||||
dentilCount: 0,
|
||||
beadCount: 0,
|
||||
braceWidth: 0.14,
|
||||
braceDepth: 0.14,
|
||||
braceBottomSpread: 1.1,
|
||||
braceTopSpread: 1.1,
|
||||
bracePlateEnabled: false,
|
||||
},
|
||||
trestleSupport: {
|
||||
label: 'Trestle Frame',
|
||||
supportStyle: 'trestle',
|
||||
style: 'faceted',
|
||||
crossSection: 'rectangular',
|
||||
height: 2.6,
|
||||
radius: 0.07,
|
||||
width: 0.14,
|
||||
depth: 0.14,
|
||||
edgeSoftness: 0.01,
|
||||
baseHeight: 0,
|
||||
capitalHeight: 0,
|
||||
shaftProfile: 'straight',
|
||||
shaftTaper: 0,
|
||||
shaftBulge: 0,
|
||||
shaftStartScale: 1,
|
||||
shaftEndScale: 1,
|
||||
shaftSegmentCount: 1,
|
||||
shaftTwistStep: 0,
|
||||
shaftCornerRadius: 0.01,
|
||||
shaftDetail: 'none',
|
||||
baseStyle: 'none',
|
||||
baseWidthScale: 1,
|
||||
baseDepthScale: 1,
|
||||
baseTierCount: 1,
|
||||
baseStepSpread: 0.34,
|
||||
basePlinthHeightRatio: 0.44,
|
||||
baseRoundBandScale: 0.92,
|
||||
baseNeckScale: 0.72,
|
||||
baseRoundBandCount: 0,
|
||||
baseRibCount: 0,
|
||||
baseCarvingLevel: 0,
|
||||
capitalStyle: 'none',
|
||||
capitalWidthScale: 1,
|
||||
capitalDepthScale: 1,
|
||||
capitalTierCount: 1,
|
||||
capitalStepSpread: 0.34,
|
||||
capitalBandCount: 0,
|
||||
capitalCarvingLevel: 0,
|
||||
ringCount: 0,
|
||||
ringSpread: 0.16,
|
||||
fluteCount: 0,
|
||||
spiralTwist: 0,
|
||||
spiralRibCount: 0,
|
||||
panelCount: 0,
|
||||
latheRingCount: 0,
|
||||
carvingLevel: 0,
|
||||
lowerBandEnabled: false,
|
||||
dentilCount: 0,
|
||||
beadCount: 0,
|
||||
braceWidth: 0.14,
|
||||
braceDepth: 0.14,
|
||||
braceBottomSpread: 1.2,
|
||||
braceTopSpread: 1,
|
||||
bracePlateEnabled: false,
|
||||
},
|
||||
portalFrameSupport: {
|
||||
label: 'Portal Frame',
|
||||
supportStyle: 'portal-frame',
|
||||
style: 'faceted',
|
||||
crossSection: 'rectangular',
|
||||
height: 2.6,
|
||||
radius: 0.08,
|
||||
width: 0.16,
|
||||
depth: 0.16,
|
||||
edgeSoftness: 0.012,
|
||||
baseHeight: 0,
|
||||
capitalHeight: 0,
|
||||
shaftProfile: 'straight',
|
||||
shaftTaper: 0,
|
||||
shaftBulge: 0,
|
||||
shaftStartScale: 1,
|
||||
shaftEndScale: 1,
|
||||
shaftSegmentCount: 1,
|
||||
shaftTwistStep: 0,
|
||||
shaftCornerRadius: 0.012,
|
||||
shaftDetail: 'none',
|
||||
baseStyle: 'none',
|
||||
baseWidthScale: 1,
|
||||
baseDepthScale: 1,
|
||||
baseTierCount: 1,
|
||||
baseStepSpread: 0.34,
|
||||
basePlinthHeightRatio: 0.44,
|
||||
baseRoundBandScale: 0.92,
|
||||
baseNeckScale: 0.72,
|
||||
baseRoundBandCount: 0,
|
||||
baseRibCount: 0,
|
||||
baseCarvingLevel: 0,
|
||||
capitalStyle: 'none',
|
||||
capitalWidthScale: 1,
|
||||
capitalDepthScale: 1,
|
||||
capitalTierCount: 1,
|
||||
capitalStepSpread: 0.34,
|
||||
capitalBandCount: 0,
|
||||
capitalCarvingLevel: 0,
|
||||
ringCount: 0,
|
||||
ringSpread: 0.16,
|
||||
fluteCount: 0,
|
||||
spiralTwist: 0,
|
||||
spiralRibCount: 0,
|
||||
panelCount: 0,
|
||||
latheRingCount: 0,
|
||||
carvingLevel: 0,
|
||||
lowerBandEnabled: false,
|
||||
dentilCount: 0,
|
||||
beadCount: 0,
|
||||
braceWidth: 0.16,
|
||||
braceDepth: 0.16,
|
||||
braceBottomSpread: 1.4,
|
||||
braceTopSpread: 0.2,
|
||||
bracePlateEnabled: false,
|
||||
},
|
||||
boxFrameSupport: {
|
||||
label: 'Box Frame',
|
||||
supportStyle: 'box-frame',
|
||||
style: 'faceted',
|
||||
crossSection: 'rectangular',
|
||||
height: 2.6,
|
||||
radius: 0.07,
|
||||
width: 0.14,
|
||||
depth: 0.14,
|
||||
edgeSoftness: 0.01,
|
||||
baseHeight: 0,
|
||||
capitalHeight: 0,
|
||||
shaftProfile: 'straight',
|
||||
shaftTaper: 0,
|
||||
shaftBulge: 0,
|
||||
shaftStartScale: 1,
|
||||
shaftEndScale: 1,
|
||||
shaftSegmentCount: 1,
|
||||
shaftTwistStep: 0,
|
||||
shaftCornerRadius: 0.01,
|
||||
shaftDetail: 'none',
|
||||
baseStyle: 'none',
|
||||
baseWidthScale: 1,
|
||||
baseDepthScale: 1,
|
||||
baseTierCount: 1,
|
||||
baseStepSpread: 0.34,
|
||||
basePlinthHeightRatio: 0.44,
|
||||
baseRoundBandScale: 0.92,
|
||||
baseNeckScale: 0.72,
|
||||
baseRoundBandCount: 0,
|
||||
baseRibCount: 0,
|
||||
baseCarvingLevel: 0,
|
||||
capitalStyle: 'none',
|
||||
capitalWidthScale: 1,
|
||||
capitalDepthScale: 1,
|
||||
capitalTierCount: 1,
|
||||
capitalStepSpread: 0.34,
|
||||
capitalBandCount: 0,
|
||||
capitalCarvingLevel: 0,
|
||||
ringCount: 0,
|
||||
ringSpread: 0.16,
|
||||
fluteCount: 0,
|
||||
spiralTwist: 0,
|
||||
spiralRibCount: 0,
|
||||
panelCount: 0,
|
||||
latheRingCount: 0,
|
||||
carvingLevel: 0,
|
||||
lowerBandEnabled: false,
|
||||
dentilCount: 0,
|
||||
beadCount: 0,
|
||||
braceWidth: 0.14,
|
||||
braceDepth: 0.14,
|
||||
braceBottomSpread: 1.4,
|
||||
braceTopSpread: 1,
|
||||
bracePlateEnabled: false,
|
||||
},
|
||||
} as const satisfies Record<string, { label: string } & Partial<z.input<typeof ColumnNode>>>
|
||||
|
||||
export type ColumnPresetId = keyof typeof COLUMN_PRESETS
|
||||
|
||||
@@ -23,6 +23,7 @@ export const FenceNode = BaseNode.extend({
|
||||
groundClearance: z.number().default(0),
|
||||
edgeInset: z.number().default(0.015),
|
||||
baseStyle: FenceBaseStyle.default('grounded'),
|
||||
showInfill: z.boolean().default(true),
|
||||
color: z.string().default('#ffffff'),
|
||||
style: FenceStyle.default('slat'),
|
||||
}).describe(
|
||||
@@ -33,6 +34,7 @@ export const FenceNode = BaseNode.extend({
|
||||
- height/thickness: overall fence dimensions in meters
|
||||
- baseHeight/postSpacing/postSize/topRailHeight: exact geometric controls from the plan3D fence model
|
||||
- groundClearance/edgeInset/baseStyle: fence support and inset configuration
|
||||
- showInfill: whether to draw intermediate posts/slats between end posts
|
||||
- color/style: visual appearance options
|
||||
`,
|
||||
)
|
||||
|
||||
@@ -1768,6 +1768,56 @@ function getRotatedRectanglePolygon(
|
||||
|
||||
function getColumnPlanFootprint(column: ColumnNode): Point2D[] {
|
||||
const center = { x: column.position[0], y: column.position[2] }
|
||||
|
||||
if (
|
||||
column.supportStyle === 'a-frame' ||
|
||||
column.supportStyle === 'y-frame' ||
|
||||
column.supportStyle === 'v-frame' ||
|
||||
column.supportStyle === 'x-brace' ||
|
||||
column.supportStyle === 'k-brace' ||
|
||||
column.supportStyle === 'single-strut' ||
|
||||
column.supportStyle === 'tripod' ||
|
||||
column.supportStyle === 'trestle' ||
|
||||
column.supportStyle === 'portal-frame' ||
|
||||
column.supportStyle === 'box-frame'
|
||||
) {
|
||||
const width = Math.max(
|
||||
column.supportStyle === 'a-frame' ||
|
||||
column.supportStyle === 'x-brace' ||
|
||||
column.supportStyle === 'k-brace' ||
|
||||
column.supportStyle === 'single-strut' ||
|
||||
column.supportStyle === 'tripod' ||
|
||||
column.supportStyle === 'trestle' ||
|
||||
column.supportStyle === 'portal-frame' ||
|
||||
column.supportStyle === 'box-frame'
|
||||
? (column.braceBottomSpread ?? 1.2)
|
||||
: 0,
|
||||
column.braceTopSpread ??
|
||||
(column.supportStyle === 'y-frame' ||
|
||||
column.supportStyle === 'v-frame' ||
|
||||
column.supportStyle === 'x-brace' ||
|
||||
column.supportStyle === 'k-brace' ||
|
||||
column.supportStyle === 'single-strut' ||
|
||||
column.supportStyle === 'tripod' ||
|
||||
column.supportStyle === 'trestle' ||
|
||||
column.supportStyle === 'portal-frame' ||
|
||||
column.supportStyle === 'box-frame'
|
||||
? 1
|
||||
: 0),
|
||||
(column.braceWidth ?? column.width) * 2,
|
||||
)
|
||||
const depth = Math.max(
|
||||
column.supportStyle === 'tripod' ||
|
||||
column.supportStyle === 'trestle' ||
|
||||
column.supportStyle === 'box-frame'
|
||||
? (column.braceTopSpread ?? 1)
|
||||
: 0,
|
||||
column.braceDepth ?? column.depth,
|
||||
0.08,
|
||||
)
|
||||
return getRotatedRectanglePolygon(center, width, depth, column.rotation)
|
||||
}
|
||||
|
||||
const shaftWidth =
|
||||
column.crossSection === 'round' ||
|
||||
column.crossSection === 'octagonal' ||
|
||||
@@ -5742,6 +5792,12 @@ const FloorplanFenceLayer = memo(function FloorplanFenceLayer({
|
||||
const fenceGlowOpacity = isDeleteHovered ? 0.18 : isActive ? 0.22 : isHovered ? 0.14 : 0
|
||||
const fenceUnderlayWidth = isActive ? '6.5' : isHovered ? '6' : '5.2'
|
||||
const fenceStrokeWidth = isActive ? '2.6' : isHovered ? '2.35' : '2.05'
|
||||
const showFenceInfill = fence.showInfill ?? true
|
||||
const visibleMarkerFrames = showFenceInfill
|
||||
? markerFrames
|
||||
: markerFrames.filter(
|
||||
(_, markerIndex) => markerIndex === 0 || markerIndex === markerFrames.length - 1,
|
||||
)
|
||||
const privacyMarkerWidth = clamp(fence.postSize * 0.58, 0.038, 0.068)
|
||||
const privacyMarkerHeight = clamp(
|
||||
Math.max(fence.baseHeight * 0.5, fence.postSize * 1.4),
|
||||
@@ -5792,7 +5848,7 @@ const FloorplanFenceLayer = memo(function FloorplanFenceLayer({
|
||||
strokeWidth={fenceStrokeWidth}
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
{markerFrames.map(({ angleDeg, point }, markerIndex) => {
|
||||
{visibleMarkerFrames.map(({ angleDeg, point }, markerIndex) => {
|
||||
const svgPoint = toSvgPoint(point)
|
||||
|
||||
if (fence.style === 'privacy') {
|
||||
|
||||
@@ -15,6 +15,7 @@ import useEditor from '../../../store/use-editor'
|
||||
import { ActionButton, ActionGroup } from '../controls/action-button'
|
||||
import { PanelSection } from '../controls/panel-section'
|
||||
import { SliderControl } from '../controls/slider-control'
|
||||
import { ToggleControl } from '../controls/toggle-control'
|
||||
import { PanelWrapper } from './panel-wrapper'
|
||||
|
||||
const SELECT_CLASS =
|
||||
@@ -83,6 +84,7 @@ function presetUpdates(presetId: ColumnPresetId): Partial<ColumnNode> {
|
||||
const { label, ...preset } = COLUMN_PRESETS[presetId]
|
||||
return {
|
||||
name: label,
|
||||
supportStyle: 'supportStyle' in preset ? preset.supportStyle : 'vertical',
|
||||
...preset,
|
||||
}
|
||||
}
|
||||
@@ -199,6 +201,18 @@ export function ColumnPanel() {
|
||||
|
||||
if (!(node && node.type === 'column' && selectedId && selectedCount === 1)) return null
|
||||
const shaftProfile = node.shaftProfile ?? 'straight'
|
||||
const supportStyle = node.supportStyle ?? 'vertical'
|
||||
const isBraceSupport =
|
||||
supportStyle === 'a-frame' ||
|
||||
supportStyle === 'y-frame' ||
|
||||
supportStyle === 'v-frame' ||
|
||||
supportStyle === 'x-brace' ||
|
||||
supportStyle === 'k-brace' ||
|
||||
supportStyle === 'single-strut' ||
|
||||
supportStyle === 'tripod' ||
|
||||
supportStyle === 'trestle' ||
|
||||
supportStyle === 'portal-frame' ||
|
||||
supportStyle === 'box-frame'
|
||||
|
||||
return (
|
||||
<PanelWrapper icon="/icons/column.png" onClose={handleClose} title={node.name || 'Column'} width={300}>
|
||||
@@ -223,53 +237,115 @@ export function ColumnPanel() {
|
||||
<PanelSection title="Shape">
|
||||
<select
|
||||
className={SELECT_CLASS}
|
||||
onChange={(event) => handleUpdate({ crossSection: event.target.value as ColumnNode['crossSection'] })}
|
||||
value={node.crossSection}
|
||||
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="round">Round</option>
|
||||
<option value="square">Square</option>
|
||||
<option value="rectangular">Rectangular</option>
|
||||
<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>
|
||||
<SliderControl
|
||||
label="Edge Softness"
|
||||
max={0.12}
|
||||
min={0}
|
||||
onChange={(value) => handleUpdate({ edgeSoftness: value })}
|
||||
precision={3}
|
||||
step={0.005}
|
||||
unit="m"
|
||||
value={node.edgeSoftness ?? 0.025}
|
||||
/>
|
||||
{(node.crossSection === 'square' || node.crossSection === 'rectangular') && (
|
||||
<SliderControl
|
||||
label="Shaft Corner Radius"
|
||||
max={0.3}
|
||||
min={0}
|
||||
onChange={(value) => handleUpdate({ shaftCornerRadius: value })}
|
||||
precision={3}
|
||||
step={0.005}
|
||||
unit="m"
|
||||
value={node.shaftCornerRadius ?? 0.035}
|
||||
/>
|
||||
{isBraceSupport ? (
|
||||
<>
|
||||
<SliderControl
|
||||
label="Brace Width"
|
||||
max={0.8}
|
||||
min={0.04}
|
||||
onChange={(value) => handleUpdate({ braceWidth: value, width: value })}
|
||||
precision={2}
|
||||
step={0.01}
|
||||
unit="m"
|
||||
value={node.braceWidth ?? node.width}
|
||||
/>
|
||||
<SliderControl
|
||||
label="Brace Depth"
|
||||
max={0.8}
|
||||
min={0.04}
|
||||
onChange={(value) => handleUpdate({ braceDepth: value, depth: value })}
|
||||
precision={2}
|
||||
step={0.01}
|
||||
unit="m"
|
||||
value={node.braceDepth ?? node.depth}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<select
|
||||
className={SELECT_CLASS}
|
||||
onChange={(event) =>
|
||||
handleUpdate({ crossSection: event.target.value as ColumnNode['crossSection'] })
|
||||
}
|
||||
value={node.crossSection}
|
||||
>
|
||||
<option value="round">Round</option>
|
||||
<option value="square">Square</option>
|
||||
<option value="rectangular">Rectangular</option>
|
||||
</select>
|
||||
<SliderControl
|
||||
label="Edge Softness"
|
||||
max={0.12}
|
||||
min={0}
|
||||
onChange={(value) => handleUpdate({ edgeSoftness: value })}
|
||||
precision={3}
|
||||
step={0.005}
|
||||
unit="m"
|
||||
value={node.edgeSoftness ?? 0.025}
|
||||
/>
|
||||
{(node.crossSection === 'square' || node.crossSection === 'rectangular') && (
|
||||
<SliderControl
|
||||
label="Shaft Corner Radius"
|
||||
max={0.3}
|
||||
min={0}
|
||||
onChange={(value) => handleUpdate({ shaftCornerRadius: value })}
|
||||
precision={3}
|
||||
step={0.005}
|
||||
unit="m"
|
||||
value={node.shaftCornerRadius ?? 0.035}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Dimensions">
|
||||
<select
|
||||
className={SELECT_CLASS}
|
||||
onChange={(event) => {
|
||||
if (!event.target.value) return
|
||||
handleUpdate(proportionUpdates(node, event.target.value as ColumnProportionPresetId))
|
||||
}}
|
||||
value=""
|
||||
>
|
||||
<option value="">Apply proportion...</option>
|
||||
{COLUMN_PROPORTION_OPTIONS.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{!isBraceSupport && (
|
||||
<select
|
||||
className={SELECT_CLASS}
|
||||
onChange={(event) => {
|
||||
if (!event.target.value) return
|
||||
handleUpdate(proportionUpdates(node, event.target.value as ColumnProportionPresetId))
|
||||
}}
|
||||
value=""
|
||||
>
|
||||
<option value="">Apply proportion...</option>
|
||||
{COLUMN_PROPORTION_OPTIONS.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<SliderControl
|
||||
label="Height"
|
||||
max={6}
|
||||
@@ -280,202 +356,286 @@ export function ColumnPanel() {
|
||||
unit="m"
|
||||
value={node.height}
|
||||
/>
|
||||
<SliderControl
|
||||
label="Width"
|
||||
max={1.6}
|
||||
min={0.12}
|
||||
onChange={(value) =>
|
||||
handleUpdate({
|
||||
width: value,
|
||||
radius: value / 2,
|
||||
...(node.crossSection === 'rectangular' ? {} : { depth: value }),
|
||||
})
|
||||
}
|
||||
precision={2}
|
||||
step={0.02}
|
||||
unit="m"
|
||||
value={node.width}
|
||||
/>
|
||||
{node.crossSection === 'rectangular' && (
|
||||
<SliderControl
|
||||
label="Depth"
|
||||
max={1.6}
|
||||
min={0.12}
|
||||
onChange={(value) => handleUpdate({ depth: value })}
|
||||
precision={2}
|
||||
step={0.02}
|
||||
unit="m"
|
||||
value={node.depth}
|
||||
/>
|
||||
{isBraceSupport ? (
|
||||
<>
|
||||
{(supportStyle === 'a-frame' ||
|
||||
supportStyle === 'x-brace' ||
|
||||
supportStyle === 'k-brace' ||
|
||||
supportStyle === 'single-strut' ||
|
||||
supportStyle === 'tripod' ||
|
||||
supportStyle === 'trestle' ||
|
||||
supportStyle === 'portal-frame' ||
|
||||
supportStyle === 'box-frame') && (
|
||||
<SliderControl
|
||||
label="Bottom Spread"
|
||||
max={4}
|
||||
min={0.2}
|
||||
onChange={(value) =>
|
||||
handleUpdate({
|
||||
braceBottomSpread: value,
|
||||
braceTopSpread:
|
||||
supportStyle === 'a-frame'
|
||||
? Math.min(node.braceTopSpread ?? 0.12, value)
|
||||
: (node.braceTopSpread ?? 1),
|
||||
})
|
||||
}
|
||||
precision={2}
|
||||
step={0.05}
|
||||
unit="m"
|
||||
value={node.braceBottomSpread ?? 1.2}
|
||||
/>
|
||||
)}
|
||||
<SliderControl
|
||||
label={supportStyle === 'y-frame' ? 'Fork Spread' : 'Top Spread'}
|
||||
max={
|
||||
supportStyle === 'y-frame' ||
|
||||
supportStyle === 'v-frame' ||
|
||||
supportStyle === 'x-brace' ||
|
||||
supportStyle === 'k-brace' ||
|
||||
supportStyle === 'single-strut' ||
|
||||
supportStyle === 'tripod' ||
|
||||
supportStyle === 'trestle' ||
|
||||
supportStyle === 'box-frame'
|
||||
? 4
|
||||
: Math.max(0.2, node.braceBottomSpread ?? 1.2)
|
||||
}
|
||||
min={0}
|
||||
onChange={(value) => handleUpdate({ braceTopSpread: value })}
|
||||
precision={2}
|
||||
step={0.02}
|
||||
unit="m"
|
||||
value={
|
||||
node.braceTopSpread ??
|
||||
(supportStyle === 'y-frame' ||
|
||||
supportStyle === 'v-frame' ||
|
||||
supportStyle === 'x-brace' ||
|
||||
supportStyle === 'k-brace' ||
|
||||
supportStyle === 'single-strut' ||
|
||||
supportStyle === 'tripod' ||
|
||||
supportStyle === 'trestle' ||
|
||||
supportStyle === 'portal-frame' ||
|
||||
supportStyle === 'box-frame'
|
||||
? 1
|
||||
: 0.12)
|
||||
}
|
||||
/>
|
||||
<ToggleControl
|
||||
checked={node.bracePlateEnabled ?? true}
|
||||
label="Connector Plates"
|
||||
onChange={(checked) => handleUpdate({ bracePlateEnabled: checked })}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<SliderControl
|
||||
label="Width"
|
||||
max={1.6}
|
||||
min={0.12}
|
||||
onChange={(value) =>
|
||||
handleUpdate({
|
||||
width: value,
|
||||
radius: value / 2,
|
||||
...(node.crossSection === 'rectangular' ? {} : { depth: value }),
|
||||
})
|
||||
}
|
||||
precision={2}
|
||||
step={0.02}
|
||||
unit="m"
|
||||
value={node.width}
|
||||
/>
|
||||
{node.crossSection === 'rectangular' && (
|
||||
<SliderControl
|
||||
label="Depth"
|
||||
max={1.6}
|
||||
min={0.12}
|
||||
onChange={(value) => handleUpdate({ depth: value })}
|
||||
precision={2}
|
||||
step={0.02}
|
||||
unit="m"
|
||||
value={node.depth}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Shaft">
|
||||
<select
|
||||
className={SELECT_CLASS}
|
||||
onChange={(event) => handleUpdate(shaftProfileUpdates(event.target.value as ColumnNode['shaftProfile']))}
|
||||
value={shaftProfile}
|
||||
>
|
||||
<option value="straight">Straight</option>
|
||||
<option value="tapered">Tapered</option>
|
||||
<option value="bulged">Bulged</option>
|
||||
<option value="hourglass">Hourglass</option>
|
||||
</select>
|
||||
{shaftProfile === 'straight' && (
|
||||
{!isBraceSupport && (
|
||||
<PanelSection title="Shaft">
|
||||
<select
|
||||
className={SELECT_CLASS}
|
||||
onChange={(event) =>
|
||||
handleUpdate(
|
||||
shaftProfileUpdates(event.target.value as ColumnNode['shaftProfile']),
|
||||
)
|
||||
}
|
||||
value={shaftProfile}
|
||||
>
|
||||
<option value="straight">Straight</option>
|
||||
<option value="tapered">Tapered</option>
|
||||
<option value="bulged">Bulged</option>
|
||||
<option value="hourglass">Hourglass</option>
|
||||
</select>
|
||||
{shaftProfile === 'straight' && (
|
||||
<SliderControl
|
||||
label="Shaft Width"
|
||||
max={1.2}
|
||||
min={0.3}
|
||||
onChange={(value) => handleUpdate({ shaftStartScale: value, shaftEndScale: value })}
|
||||
precision={2}
|
||||
step={0.02}
|
||||
value={node.shaftStartScale ?? 0.72}
|
||||
/>
|
||||
)}
|
||||
{shaftProfile === 'tapered' && (
|
||||
<>
|
||||
<SliderControl
|
||||
label="Bottom Width"
|
||||
max={1.2}
|
||||
min={0.3}
|
||||
onChange={(value) => handleUpdate({ shaftStartScale: value })}
|
||||
precision={2}
|
||||
step={0.02}
|
||||
value={node.shaftStartScale ?? 0.82}
|
||||
/>
|
||||
<SliderControl
|
||||
label="Top Width"
|
||||
max={1.2}
|
||||
min={0.3}
|
||||
onChange={(value) => handleUpdate({ shaftEndScale: value })}
|
||||
precision={2}
|
||||
step={0.02}
|
||||
value={node.shaftEndScale ?? 0.72}
|
||||
/>
|
||||
<SliderControl
|
||||
label="Taper"
|
||||
max={0.45}
|
||||
min={0}
|
||||
onChange={(value) => handleUpdate({ shaftTaper: value })}
|
||||
precision={2}
|
||||
step={0.01}
|
||||
value={node.shaftTaper ?? 0.14}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{shaftProfile === 'bulged' && (
|
||||
<>
|
||||
<SliderControl
|
||||
label="End Width"
|
||||
max={1.2}
|
||||
min={0.3}
|
||||
onChange={(value) =>
|
||||
handleUpdate({ shaftStartScale: value, shaftEndScale: value })
|
||||
}
|
||||
precision={2}
|
||||
step={0.02}
|
||||
value={node.shaftStartScale ?? 0.68}
|
||||
/>
|
||||
<SliderControl
|
||||
label="Bulge"
|
||||
max={0.35}
|
||||
min={0}
|
||||
onChange={(value) => handleUpdate({ shaftBulge: value })}
|
||||
precision={2}
|
||||
step={0.01}
|
||||
value={node.shaftBulge ?? 0.12}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{shaftProfile === 'hourglass' && (
|
||||
<>
|
||||
<SliderControl
|
||||
label="End Width"
|
||||
max={1.2}
|
||||
min={0.3}
|
||||
onChange={(value) =>
|
||||
handleUpdate({ shaftStartScale: value, shaftEndScale: value })
|
||||
}
|
||||
precision={2}
|
||||
step={0.02}
|
||||
value={node.shaftStartScale ?? 0.84}
|
||||
/>
|
||||
<SliderControl
|
||||
label="Waist"
|
||||
max={0.35}
|
||||
min={0}
|
||||
onChange={(value) => handleUpdate({ shaftBulge: value })}
|
||||
precision={2}
|
||||
step={0.01}
|
||||
value={node.shaftBulge ?? 0.12}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<SliderControl
|
||||
label="Shaft Width"
|
||||
max={1.2}
|
||||
min={0.3}
|
||||
onChange={(value) => handleUpdate({ shaftStartScale: value, shaftEndScale: value })}
|
||||
precision={2}
|
||||
step={0.02}
|
||||
value={node.shaftStartScale ?? 0.72}
|
||||
label="Segment Twist"
|
||||
max={90}
|
||||
min={-90}
|
||||
onChange={(value) =>
|
||||
handleUpdate({
|
||||
shaftTwistStep: value,
|
||||
...(Math.abs(value) > 0.001 && (node.shaftSegmentCount ?? 1) < 8
|
||||
? { shaftSegmentCount: 12 }
|
||||
: {}),
|
||||
})
|
||||
}
|
||||
precision={0}
|
||||
step={5}
|
||||
unit="°"
|
||||
value={node.shaftTwistStep ?? 0}
|
||||
/>
|
||||
)}
|
||||
{shaftProfile === 'tapered' && (
|
||||
<>
|
||||
{Math.abs(node.shaftTwistStep ?? 0) > 0.001 && (
|
||||
<SliderControl
|
||||
label="Bottom Width"
|
||||
max={1.2}
|
||||
min={0.3}
|
||||
onChange={(value) => handleUpdate({ shaftStartScale: value })}
|
||||
precision={2}
|
||||
step={0.02}
|
||||
value={node.shaftStartScale ?? 0.82}
|
||||
label="Twist Segments"
|
||||
max={48}
|
||||
min={4}
|
||||
onChange={(value) => handleUpdate({ shaftSegmentCount: Math.round(value) })}
|
||||
precision={0}
|
||||
step={1}
|
||||
value={node.shaftSegmentCount ?? 12}
|
||||
/>
|
||||
<SliderControl
|
||||
label="Top Width"
|
||||
max={1.2}
|
||||
min={0.3}
|
||||
onChange={(value) => handleUpdate({ shaftEndScale: value })}
|
||||
precision={2}
|
||||
step={0.02}
|
||||
value={node.shaftEndScale ?? 0.72}
|
||||
/>
|
||||
<SliderControl
|
||||
label="Taper"
|
||||
max={0.45}
|
||||
min={0}
|
||||
onChange={(value) => handleUpdate({ shaftTaper: value })}
|
||||
precision={2}
|
||||
step={0.01}
|
||||
value={node.shaftTaper ?? 0.14}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{shaftProfile === 'bulged' && (
|
||||
<>
|
||||
<SliderControl
|
||||
label="End Width"
|
||||
max={1.2}
|
||||
min={0.3}
|
||||
onChange={(value) => handleUpdate({ shaftStartScale: value, shaftEndScale: value })}
|
||||
precision={2}
|
||||
step={0.02}
|
||||
value={node.shaftStartScale ?? 0.68}
|
||||
/>
|
||||
<SliderControl
|
||||
label="Bulge"
|
||||
max={0.35}
|
||||
min={0}
|
||||
onChange={(value) => handleUpdate({ shaftBulge: value })}
|
||||
precision={2}
|
||||
step={0.01}
|
||||
value={node.shaftBulge ?? 0.12}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{shaftProfile === 'hourglass' && (
|
||||
<>
|
||||
<SliderControl
|
||||
label="End Width"
|
||||
max={1.2}
|
||||
min={0.3}
|
||||
onChange={(value) => handleUpdate({ shaftStartScale: value, shaftEndScale: value })}
|
||||
precision={2}
|
||||
step={0.02}
|
||||
value={node.shaftStartScale ?? 0.84}
|
||||
/>
|
||||
<SliderControl
|
||||
label="Waist"
|
||||
max={0.35}
|
||||
min={0}
|
||||
onChange={(value) => handleUpdate({ shaftBulge: value })}
|
||||
precision={2}
|
||||
step={0.01}
|
||||
value={node.shaftBulge ?? 0.12}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<SliderControl
|
||||
label="Segment Twist"
|
||||
max={90}
|
||||
min={-90}
|
||||
onChange={(value) =>
|
||||
handleUpdate({
|
||||
shaftTwistStep: value,
|
||||
...(Math.abs(value) > 0.001 && (node.shaftSegmentCount ?? 1) < 8
|
||||
? { shaftSegmentCount: 12 }
|
||||
: {}),
|
||||
})
|
||||
}
|
||||
precision={0}
|
||||
step={5}
|
||||
unit="°"
|
||||
value={node.shaftTwistStep ?? 0}
|
||||
/>
|
||||
{Math.abs(node.shaftTwistStep ?? 0) > 0.001 && (
|
||||
)}
|
||||
<SliderControl
|
||||
label="Twist Segments"
|
||||
max={48}
|
||||
min={4}
|
||||
onChange={(value) => handleUpdate({ shaftSegmentCount: Math.round(value) })}
|
||||
label="Ring Pairs"
|
||||
max={4}
|
||||
min={0}
|
||||
onChange={(value) =>
|
||||
handleUpdate({
|
||||
ringCount: Math.round(value) * 2,
|
||||
ringPlacement: 'ends',
|
||||
ringSpread: node.ringSpread ?? 0.16,
|
||||
ringThickness: node.ringThickness ?? 0.055,
|
||||
})
|
||||
}
|
||||
precision={0}
|
||||
step={1}
|
||||
value={node.shaftSegmentCount ?? 12}
|
||||
value={Math.ceil((node.ringCount ?? 0) / 2)}
|
||||
/>
|
||||
)}
|
||||
<SliderControl
|
||||
label="Ring Pairs"
|
||||
max={4}
|
||||
min={0}
|
||||
onChange={(value) =>
|
||||
handleUpdate({
|
||||
ringCount: Math.round(value) * 2,
|
||||
ringPlacement: 'ends',
|
||||
ringSpread: node.ringSpread ?? 0.16,
|
||||
ringThickness: node.ringThickness ?? 0.055,
|
||||
})
|
||||
}
|
||||
precision={0}
|
||||
step={1}
|
||||
value={Math.ceil((node.ringCount ?? 0) / 2)}
|
||||
/>
|
||||
{(node.ringCount ?? 0) > 0 && (
|
||||
<SliderControl
|
||||
label="Ring Thickness"
|
||||
max={0.14}
|
||||
min={0.01}
|
||||
onChange={(value) => handleUpdate({ ringThickness: value })}
|
||||
precision={3}
|
||||
step={0.005}
|
||||
unit="m"
|
||||
value={node.ringThickness ?? 0.055}
|
||||
/>
|
||||
)}
|
||||
{(node.ringCount ?? 0) > 0 && (
|
||||
<SliderControl
|
||||
label="Ring Spread"
|
||||
max={0.45}
|
||||
min={0.04}
|
||||
onChange={(value) => handleUpdate({ ringSpread: value, ringPlacement: 'ends' })}
|
||||
precision={2}
|
||||
step={0.01}
|
||||
value={node.ringSpread ?? 0.16}
|
||||
/>
|
||||
)}
|
||||
</PanelSection>
|
||||
{(node.ringCount ?? 0) > 0 && (
|
||||
<SliderControl
|
||||
label="Ring Thickness"
|
||||
max={0.14}
|
||||
min={0.01}
|
||||
onChange={(value) => handleUpdate({ ringThickness: value })}
|
||||
precision={3}
|
||||
step={0.005}
|
||||
unit="m"
|
||||
value={node.ringThickness ?? 0.055}
|
||||
/>
|
||||
)}
|
||||
{(node.ringCount ?? 0) > 0 && (
|
||||
<SliderControl
|
||||
label="Ring Spread"
|
||||
max={0.45}
|
||||
min={0.04}
|
||||
onChange={(value) => handleUpdate({ ringSpread: value, ringPlacement: 'ends' })}
|
||||
precision={2}
|
||||
step={0.01}
|
||||
value={node.ringSpread ?? 0.16}
|
||||
/>
|
||||
)}
|
||||
</PanelSection>
|
||||
)}
|
||||
|
||||
{!isBraceSupport && (
|
||||
<PanelSection title="Ends">
|
||||
<select
|
||||
className={SELECT_CLASS}
|
||||
@@ -685,6 +845,7 @@ export function ColumnPanel() {
|
||||
/>
|
||||
)}
|
||||
</PanelSection>
|
||||
)}
|
||||
|
||||
<PanelSection title="Transform">
|
||||
<SliderControl
|
||||
|
||||
@@ -24,6 +24,7 @@ import { MaterialPicker } from '../controls/material-picker'
|
||||
import { PanelSection } from '../controls/panel-section'
|
||||
import { SegmentedControl } from '../controls/segmented-control'
|
||||
import { SliderControl } from '../controls/slider-control'
|
||||
import { ToggleControl } from '../controls/toggle-control'
|
||||
import { PanelWrapper } from './panel-wrapper'
|
||||
|
||||
type FenceStyleValue = 'slat' | 'rail' | 'privacy'
|
||||
@@ -115,6 +116,12 @@ export function FencePanel() {
|
||||
options={FENCE_BASE_STYLE_OPTIONS}
|
||||
value={node.baseStyle}
|
||||
/>
|
||||
<ToggleControl
|
||||
checked={node.showInfill ?? true}
|
||||
className="mt-2"
|
||||
label="Fence Infill"
|
||||
onChange={(checked) => handleUpdate({ showInfill: checked })}
|
||||
/>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Dimensions">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import {
|
||||
type AnyNodeId,
|
||||
type FenceNode,
|
||||
@@ -7,6 +6,7 @@ import {
|
||||
sceneRegistry,
|
||||
useScene,
|
||||
} from '@pascal-app/core'
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import * as THREE from 'three'
|
||||
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
||||
|
||||
@@ -69,7 +69,9 @@ function createFenceCurveSpanParts(
|
||||
const parts: FencePart[] = []
|
||||
const frameCount = Math.max(
|
||||
1,
|
||||
Math.ceil((getWallCurveLength(fence) * Math.max(1e-4, endT - startT)) / MIN_CURVE_SEGMENT_LENGTH),
|
||||
Math.ceil(
|
||||
(getWallCurveLength(fence) * Math.max(1e-4, endT - startT)) / MIN_CURVE_SEGMENT_LENGTH,
|
||||
),
|
||||
)
|
||||
|
||||
let previous = getFencePointAt(fence, startT)
|
||||
@@ -164,6 +166,7 @@ function createFenceParts(fence: FenceNode): FencePart[] {
|
||||
const spacing = Math.max(fence.postSpacing * styleDefaults.spacingFactor, postWidth * 1.2)
|
||||
const edgeInset = Math.max(fence.edgeInset ?? 0.015, 0.005)
|
||||
const isFloating = fence.baseStyle === 'floating'
|
||||
const showInfill = fence.showInfill ?? true
|
||||
const baseY = isFloating ? clearance : 0
|
||||
const effectiveBaseHeight = baseHeight
|
||||
const startInsetT = Math.min(0.499, edgeInset / length)
|
||||
@@ -192,18 +195,18 @@ function createFenceParts(fence: FenceNode): FencePart[] {
|
||||
)
|
||||
}
|
||||
|
||||
const count = Math.max(2, Math.floor((length - edgeInset * 2) / spacing) + 1)
|
||||
const count = showInfill ? Math.max(2, Math.floor((length - edgeInset * 2) / spacing) + 1) : 2
|
||||
const verticalY = baseY + effectiveBaseHeight + verticalHeight / 2
|
||||
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const t = count === 1 ? 0.5 : startInsetT + (endInsetT - startInsetT) * (index / (count - 1))
|
||||
const frame = getFencePointAt(fence, t)
|
||||
const isEdgePost = index === 0 || index === count - 1
|
||||
const postHeight =
|
||||
isFloating && isEdgePost
|
||||
? effectiveBaseHeight + verticalHeight + topRailHeight + clearance
|
||||
: verticalHeight
|
||||
const postY = isFloating && isEdgePost ? postHeight / 2 : verticalY
|
||||
const fullHeightPost = !showInfill || (isFloating && isEdgePost)
|
||||
const postHeight = fullHeightPost
|
||||
? effectiveBaseHeight + verticalHeight + topRailHeight + clearance
|
||||
: verticalHeight
|
||||
const postY = fullHeightPost ? postHeight / 2 : verticalY
|
||||
|
||||
parts.push({
|
||||
position: [frame.point.x, postY, frame.point.y],
|
||||
@@ -244,7 +247,9 @@ function generateFenceGeometry(fence: FenceNode) {
|
||||
const geometries = parts.map(createFencePartGeometry)
|
||||
|
||||
const merged = mergeGeometries(geometries, false) ?? new THREE.BufferGeometry()
|
||||
geometries.forEach((geometry) => geometry.dispose())
|
||||
geometries.forEach((geometry) => {
|
||||
geometry.dispose()
|
||||
})
|
||||
const mergedUv = merged.getAttribute('uv')
|
||||
if (mergedUv) {
|
||||
merged.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(mergedUv.array), 2))
|
||||
|
||||
Reference in New Issue
Block a user