Merge remote-tracking branch 'origin/main' into feat/editor-ux-rendering-placement
This commit is contained in:
@@ -23,9 +23,9 @@
|
||||
"prepublishOnly": "bun run build && bun test"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@pascal-app/core": "^0.8.0",
|
||||
"@pascal-app/editor": "^0.8.0",
|
||||
"@pascal-app/viewer": "^0.8.0",
|
||||
"@pascal-app/core": "^0.9.0",
|
||||
"@pascal-app/editor": "^0.9.0",
|
||||
"@pascal-app/viewer": "^0.9.0",
|
||||
"@react-three/drei": "^10",
|
||||
"@react-three/fiber": "^9",
|
||||
"lucide-react": "^1",
|
||||
@@ -34,15 +34,15 @@
|
||||
"zustand": "^5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pascal-app/core": "^0.8.0",
|
||||
"@pascal-app/editor": "^0.8.0",
|
||||
"@pascal-app/viewer": "^0.8.0",
|
||||
"@pascal-app/core": "^0.9.0",
|
||||
"@pascal-app/editor": "^0.9.0",
|
||||
"@pascal-app/viewer": "^0.9.0",
|
||||
"@pascal/typescript-config": "*",
|
||||
"@types/bun": "^1.3.0",
|
||||
"@types/node": "^22.19.12",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/three": "^0.184.0",
|
||||
"typescript": "6.0.2"
|
||||
"typescript": "6.0.3"
|
||||
},
|
||||
"keywords": [
|
||||
"pascal",
|
||||
|
||||
@@ -126,9 +126,8 @@ export const MoveCeilingTool: React.FC<{ node: CeilingNode }> = ({ node }) => {
|
||||
deltaRef.current = [deltaX, deltaZ]
|
||||
setMeshOffset(ceilingId as AnyNodeId, deltaX, deltaZ, height)
|
||||
// Aligned with slab/fence: the delta matches the direct mesh
|
||||
// mutation. CeilingRenderer doesn't bind position via React, so
|
||||
// this entry isn't consumed for rendering, but kept consistent
|
||||
// in case other systems read it.
|
||||
// mutation. CeilingRenderer also consumes this store so external
|
||||
// movers can preview ceilings without rebuilding the polygon.
|
||||
useLiveTransforms.getState().set(ceilingId, {
|
||||
position: [deltaX, 0, deltaZ],
|
||||
rotation: 0,
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type CeilingNode,
|
||||
getMaterialPresetByRef,
|
||||
resolveMaterial,
|
||||
useLiveTransforms,
|
||||
useRegistry,
|
||||
useScene,
|
||||
} from '@pascal-app/core'
|
||||
@@ -79,6 +80,13 @@ export const CeilingRenderer = ({ node }: { node: CeilingNode }) => {
|
||||
const textures = useViewer((s) => s.textures)
|
||||
const colorPreset = useViewer((s) => s.colorPreset)
|
||||
const sceneTheme = useViewer((s) => s.sceneTheme)
|
||||
const liveTransform = useLiveTransforms((s) => s.get(node.id))
|
||||
const ceilingY = (node.height ?? 2.5) - 0.01 + (liveTransform?.position[1] ?? 0)
|
||||
const position: [number, number, number] = [
|
||||
liveTransform?.position[0] ?? 0,
|
||||
ceilingY,
|
||||
liveTransform?.position[2] ?? 0,
|
||||
]
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
@@ -124,7 +132,12 @@ export const CeilingRenderer = ({ node }: { node: CeilingNode }) => {
|
||||
])
|
||||
|
||||
return (
|
||||
<mesh geometry={placeholderGeometry} material={materials.bottomMaterial} ref={ref}>
|
||||
<mesh
|
||||
geometry={placeholderGeometry}
|
||||
material={materials.bottomMaterial}
|
||||
position={position}
|
||||
ref={ref}
|
||||
>
|
||||
<mesh
|
||||
geometry={gridPlaceholderGeometry}
|
||||
material={materials.topMaterial}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
EDITOR_LAYER,
|
||||
type FencePlanPoint,
|
||||
formatAngleRadians,
|
||||
formatLinearMeasurement,
|
||||
getAngleArcToSegmentReference,
|
||||
getAngleToSegmentReference,
|
||||
getSegmentAngleReferenceAtPoint,
|
||||
@@ -94,17 +95,6 @@ type AngleSource = {
|
||||
draftVector: FencePlanPoint
|
||||
}
|
||||
|
||||
function formatMeasurement(value: number, unit: 'metric' | 'imperial') {
|
||||
if (unit === 'imperial') {
|
||||
const feet = value * 3.280_84
|
||||
const wholeFeet = Math.floor(feet)
|
||||
const inches = Math.round((feet - wholeFeet) * 12)
|
||||
if (inches === 12) return `${wholeFeet + 1}'0"`
|
||||
return `${wholeFeet}'${inches}"`
|
||||
}
|
||||
return `${Number.parseFloat(value.toFixed(2))}m`
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number) {
|
||||
return Math.min(max, Math.max(min, value))
|
||||
}
|
||||
@@ -365,7 +355,7 @@ function getDraftMeasurementState(
|
||||
const length = Math.hypot(dx, dz)
|
||||
if (length < 0.01) return null
|
||||
return {
|
||||
lengthLabel: formatMeasurement(length, unit),
|
||||
lengthLabel: formatLinearMeasurement(length, unit),
|
||||
lengthPosition: [
|
||||
(start[0] + end[0]) / 2,
|
||||
baseY + previewHeight + DRAFT_LABEL_Y_OFFSET,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import {
|
||||
type AnimationEffect,
|
||||
type AnyNodeId,
|
||||
getScaledDimensions,
|
||||
type Interactive,
|
||||
type ItemNode,
|
||||
type LightEffect,
|
||||
@@ -92,17 +93,25 @@ export const ItemRenderer = ({ node: storeNode }: { node: ItemNode }) => {
|
||||
() => (liveOverrides ? ({ ...storeNode, ...liveOverrides } as ItemNode) : storeNode),
|
||||
[storeNode, liveOverrides],
|
||||
)
|
||||
const roomClearPreview =
|
||||
(node as ItemNode & { roomClearPreview?: unknown }).roomClearPreview === true
|
||||
|
||||
const content = (
|
||||
<group position={node.position} ref={ref} rotation={node.rotation} visible={node.visible}>
|
||||
<ErrorBoundary fallback={<BrokenItemFallback node={node} />}>
|
||||
<Suspense fallback={<PreviewModel node={node} />}>
|
||||
<ModelRenderer node={node} />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
{node.children?.map((childId) => (
|
||||
<NodeRenderer key={childId} nodeId={childId} />
|
||||
))}
|
||||
{roomClearPreview ? (
|
||||
<ClearPreviewModel node={node} />
|
||||
) : (
|
||||
<>
|
||||
<ErrorBoundary fallback={<BrokenItemFallback node={node} />}>
|
||||
<Suspense fallback={<PreviewModel node={node} />}>
|
||||
<ModelRenderer node={node} />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
{node.children?.map((childId) => (
|
||||
<NodeRenderer key={childId} nodeId={childId} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</group>
|
||||
)
|
||||
|
||||
@@ -141,6 +150,26 @@ const PreviewModel = ({ node }: { node: ItemNode }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const ClearPreviewModel = ({ node }: { node: ItemNode }) => {
|
||||
const shading = useViewer((s) => s.shading)
|
||||
const [w, h, d] = getScaledDimensions(node)
|
||||
const material = useMemo(() => {
|
||||
const next = createDefaultMaterial('#ef4444', 1, shading) as MutableMaterial
|
||||
next.depthTest = false
|
||||
next.opacity = 0.35
|
||||
next.transparent = true
|
||||
next.wireframe = true
|
||||
next.needsUpdate = true
|
||||
return next
|
||||
}, [shading])
|
||||
|
||||
return (
|
||||
<mesh material={material} position-y={h / 2}>
|
||||
<boxGeometry args={[w, h, d]} />
|
||||
</mesh>
|
||||
)
|
||||
}
|
||||
|
||||
const multiplyScales = (
|
||||
a: [number, number, number],
|
||||
b: [number, number, number],
|
||||
|
||||
@@ -76,5 +76,9 @@ export const shelfParametrics: ParametricDescriptor<ShelfNode> = {
|
||||
{ key: 'height', kind: 'number', unit: 'm', min: 0.05, max: 2.5, step: 0.05 },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Position',
|
||||
fields: [{ key: 'position', kind: 'vec3' }],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ import {
|
||||
import {
|
||||
ActionButton,
|
||||
ActionGroup,
|
||||
getLinearUnitLabel,
|
||||
linearControlValueToMeters,
|
||||
metersToLinearUnit,
|
||||
PanelSection,
|
||||
PanelWrapper,
|
||||
SliderControl,
|
||||
@@ -26,6 +29,7 @@ import { useCallback, useMemo, useRef } from 'react'
|
||||
|
||||
export default function WallPanel() {
|
||||
const selectedId = useViewer((s) => s.selection.selectedIds[0])
|
||||
const unit = useViewer((s) => s.unit)
|
||||
const setSelection = useViewer((s) => s.setSelection)
|
||||
const setCurvingWall = useEditor((s) => s.setCurvingWall)
|
||||
|
||||
@@ -117,14 +121,19 @@ export default function WallPanel() {
|
||||
|
||||
if (!(node && node.type === 'wall' && selectedId)) return null
|
||||
|
||||
const dx = node.end[0] - node.start[0]
|
||||
const dz = node.end[1] - node.start[1]
|
||||
const length = getWallCurveLength(node)
|
||||
|
||||
const height = node.height ?? 2.5
|
||||
const thickness = node.thickness ?? 0.1
|
||||
const curveOffset = getClampedWallCurveOffset(node)
|
||||
const maxCurveOffset = getMaxWallCurveOffset(node)
|
||||
const unitLabel = getLinearUnitLabel(unit)
|
||||
const displayLength = metersToLinearUnit(length, unit)
|
||||
const displayHeight = metersToLinearUnit(height, unit)
|
||||
const displayThickness = metersToLinearUnit(thickness, unit)
|
||||
const displayCurveOffset = metersToLinearUnit(curveOffset, unit)
|
||||
const displayMaxCurveOffset = metersToLinearUnit(maxCurveOffset, unit)
|
||||
const curveOffsetLimit = Math.max(0.01, maxCurveOffset)
|
||||
|
||||
return (
|
||||
<PanelWrapper
|
||||
@@ -136,44 +145,66 @@ export default function WallPanel() {
|
||||
<PanelSection title="Dimensions">
|
||||
<SliderControl
|
||||
label="Length"
|
||||
max={20}
|
||||
min={0.1}
|
||||
onChange={handleUpdateLength}
|
||||
max={metersToLinearUnit(20, unit)}
|
||||
min={metersToLinearUnit(0.1, unit)}
|
||||
onChange={(value) =>
|
||||
handleUpdateLength(
|
||||
linearControlValueToMeters(value, unit, { maxMeters: 20, minMeters: 0.1 }),
|
||||
)
|
||||
}
|
||||
precision={2}
|
||||
step={0.01}
|
||||
unit="m"
|
||||
value={length}
|
||||
step={unit === 'imperial' ? 0.1 : 0.01}
|
||||
unit={unitLabel}
|
||||
value={displayLength}
|
||||
/>
|
||||
<SliderControl
|
||||
label="Height"
|
||||
max={6}
|
||||
min={0.1}
|
||||
onChange={(v) => handleUpdate({ height: Math.max(0.1, v) })}
|
||||
max={metersToLinearUnit(6, unit)}
|
||||
min={metersToLinearUnit(0.1, unit)}
|
||||
onChange={(v) =>
|
||||
handleUpdate({
|
||||
height: linearControlValueToMeters(v, unit, { maxMeters: 6, minMeters: 0.1 }),
|
||||
})
|
||||
}
|
||||
precision={2}
|
||||
step={0.1}
|
||||
unit="m"
|
||||
value={Math.round(height * 100) / 100}
|
||||
unit={unitLabel}
|
||||
value={Math.round(displayHeight * 100) / 100}
|
||||
/>
|
||||
<SliderControl
|
||||
label="Thickness"
|
||||
max={1}
|
||||
min={0.05}
|
||||
onChange={(v) => handleUpdate({ thickness: Math.max(0.05, v) })}
|
||||
max={metersToLinearUnit(1, unit)}
|
||||
min={metersToLinearUnit(0.05, unit)}
|
||||
onChange={(v) =>
|
||||
handleUpdate({
|
||||
thickness: linearControlValueToMeters(v, unit, { maxMeters: 1, minMeters: 0.05 }),
|
||||
})
|
||||
}
|
||||
precision={3}
|
||||
step={0.01}
|
||||
unit="m"
|
||||
value={Math.round(thickness * 1000) / 1000}
|
||||
unit={unitLabel}
|
||||
value={Math.round(displayThickness * 1000) / 1000}
|
||||
/>
|
||||
{!hasWallChildrenBlockingCurve && (
|
||||
<SliderControl
|
||||
label="Curve"
|
||||
max={Math.max(0.01, maxCurveOffset)}
|
||||
min={-Math.max(0.01, maxCurveOffset)}
|
||||
onChange={(v) => handleUpdate({ curveOffset: normalizeWallCurveOffset(node, v) })}
|
||||
max={Math.max(metersToLinearUnit(0.01, unit), displayMaxCurveOffset)}
|
||||
min={-Math.max(metersToLinearUnit(0.01, unit), displayMaxCurveOffset)}
|
||||
onChange={(v) =>
|
||||
handleUpdate({
|
||||
curveOffset: normalizeWallCurveOffset(
|
||||
node,
|
||||
linearControlValueToMeters(v, unit, {
|
||||
maxMeters: curveOffsetLimit,
|
||||
minMeters: -curveOffsetLimit,
|
||||
}),
|
||||
),
|
||||
})
|
||||
}
|
||||
precision={2}
|
||||
step={0.1}
|
||||
unit="m"
|
||||
value={Math.round(curveOffset * 100) / 100}
|
||||
unit={unitLabel}
|
||||
value={Math.round(displayCurveOffset * 100) / 100}
|
||||
/>
|
||||
)}
|
||||
</PanelSection>
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
createWallOnCurrentLevel,
|
||||
EDITOR_LAYER,
|
||||
formatAngleRadians,
|
||||
formatLinearMeasurement,
|
||||
getAngleArcToSegmentReference,
|
||||
getAngleToSegmentReference,
|
||||
getSegmentAngleReferenceAtPoint,
|
||||
@@ -124,17 +125,6 @@ type AngleSource = {
|
||||
draftVector: WallPlanPoint
|
||||
}
|
||||
|
||||
function formatMeasurement(value: number, unit: 'metric' | 'imperial') {
|
||||
if (unit === 'imperial') {
|
||||
const feet = value * 3.280_84
|
||||
const wholeFeet = Math.floor(feet)
|
||||
const inches = Math.round((feet - wholeFeet) * 12)
|
||||
if (inches === 12) return `${wholeFeet + 1}'0"`
|
||||
return `${wholeFeet}'${inches}"`
|
||||
}
|
||||
return `${Number.parseFloat(value.toFixed(2))}m`
|
||||
}
|
||||
|
||||
function clamp(value: number, min: number, max: number) {
|
||||
return Math.min(max, Math.max(min, value))
|
||||
}
|
||||
@@ -426,7 +416,7 @@ function getDraftMeasurementState(
|
||||
const length = Math.hypot(dx, dz)
|
||||
if (length < 0.01) return null
|
||||
return {
|
||||
lengthLabel: formatMeasurement(length, unit),
|
||||
lengthLabel: formatLinearMeasurement(length, unit),
|
||||
lengthPosition: [
|
||||
(start[0] + end[0]) / 2,
|
||||
baseY + previewHeight + DRAFT_LABEL_Y_OFFSET,
|
||||
|
||||
Reference in New Issue
Block a user