Merge pull request #116 from pascalorg/fix/community-feedback-pass-3
Fix/community feedback pass 3
This commit is contained in:
@@ -201,6 +201,21 @@ export function wallOverlapsPolygon(
|
||||
const nz = (dz / len) * step
|
||||
if (pointInPolygon(start[0] + nx, start[1] + nz, polygon)) return true
|
||||
if (pointInPolygon(end[0] - nx, end[1] - nz, polygon)) return true
|
||||
|
||||
// Also nudge perpendicular to the wall (into the slab interior) for walls that
|
||||
// lie exactly on the slab boundary. The along-wall nudge keeps points on the
|
||||
// boundary where pointInPolygon is unreliable; a perpendicular inward nudge
|
||||
// moves the point clearly inside (or outside) the polygon.
|
||||
// Sample the wall at 1/4, 1/2, 3/4 positions with a perpendicular nudge.
|
||||
const PERP_STEP = 1e-4
|
||||
const pnx = (-nz / step) * PERP_STEP // perpendicular left
|
||||
const pnz = (nx / step) * PERP_STEP
|
||||
for (const t of [0.25, 0.5, 0.75]) {
|
||||
const bx = start[0] + dx * t
|
||||
const bz = start[1] + dz * t
|
||||
if (pointInPolygon(bx + pnx, bz + pnz, polygon)) return true
|
||||
if (pointInPolygon(bx - pnx, bz - pnz, polygon)) return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check if midpoint is inside (catches walls crossing through)
|
||||
@@ -557,26 +572,42 @@ export class SpatialGridManager {
|
||||
let maxElevation = -Infinity
|
||||
for (const slab of slabMap.values()) {
|
||||
if (slab.polygon.length < 3) continue
|
||||
if (wallOverlapsPolygon(start, end, slab.polygon)) {
|
||||
// Check if wall midpoint is in a hole (if so, ignore this slab)
|
||||
if (!wallOverlapsPolygon(start, end, slab.polygon)) continue
|
||||
|
||||
const holes = slab.holes || []
|
||||
if (holes.length === 0) {
|
||||
// No holes: wall is on this slab
|
||||
const elevation = slab.elevation ?? 0.05
|
||||
if (elevation > maxElevation) maxElevation = elevation
|
||||
continue
|
||||
}
|
||||
|
||||
// Sample multiple points along the wall to check whether any portion lies on
|
||||
// solid slab (not inside any hole). Checking only the midpoint fails when the
|
||||
// midpoint falls in a staircase hole but the wall's endpoints are on solid slab.
|
||||
const dx = end[0] - start[0]
|
||||
const dz = end[1] - start[1]
|
||||
let hasValidPoint = false
|
||||
for (const t of [0, 0.25, 0.5, 0.75, 1]) {
|
||||
const px = start[0] + dx * t
|
||||
const pz = start[1] + dz * t
|
||||
let inHole = false
|
||||
const midX = (start[0] + end[0]) / 2
|
||||
const midZ = (start[1] + end[1]) / 2
|
||||
const holes = slab.holes || []
|
||||
for (const hole of holes) {
|
||||
if (hole.length >= 3 && pointInPolygon(midX, midZ, hole)) {
|
||||
if (hole.length >= 3 && pointInPolygon(px, pz, hole)) {
|
||||
inHole = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!inHole) {
|
||||
const elevation = slab.elevation ?? 0.05
|
||||
if (elevation > maxElevation) {
|
||||
maxElevation = elevation
|
||||
}
|
||||
hasValidPoint = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (hasValidPoint) {
|
||||
const elevation = slab.elevation ?? 0.05
|
||||
if (elevation > maxElevation) maxElevation = elevation
|
||||
}
|
||||
}
|
||||
return maxElevation === -Infinity ? 0 : maxElevation
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ export function initSpatialGridSync() {
|
||||
}
|
||||
}
|
||||
} else if (node.type === 'slab' && prev.type === 'slab') {
|
||||
if (node.polygon !== prev.polygon || node.elevation !== prev.elevation) {
|
||||
if (node.polygon !== prev.polygon || node.elevation !== prev.elevation || node.holes !== prev.holes) {
|
||||
const levelId = resolveLevelId(node, state.nodes)
|
||||
spatialGridManager.handleNodeUpdated(node, levelId)
|
||||
|
||||
|
||||
@@ -152,6 +152,10 @@ export const deleteNodesAction = (
|
||||
return { nodes: nextNodes, rootNodeIds: nextRootIds }
|
||||
})
|
||||
|
||||
// Notify systems that the parent has changed (e.g. Wall needs to fill a window hole)
|
||||
parentsToMarkDirty.forEach((pId) => get().markDirty(pId))
|
||||
|
||||
// Trigger a full scene re-validation after deleting node (as deleting a slab can cause widespread changes to level elevations)
|
||||
const currentNodes = get().nodes
|
||||
Object.values(currentNodes).forEach((node) => {
|
||||
get().markDirty(node.id)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -170,7 +170,6 @@ const useScene: UseSceneStore = create<SceneState>()(
|
||||
rootNodeIds: state.rootNodeIds,
|
||||
}),
|
||||
merge: (persistedState, currentState) => {
|
||||
console.log('merge calling...', persistedState, currentState)
|
||||
const persisted = persistedState as Partial<SceneState>
|
||||
// Backward compat: add default scale to item nodes saved before scale was added
|
||||
if (persisted.nodes) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type * as THREE from 'three'
|
||||
import { sceneRegistry } from '../../hooks/scene-registry/scene-registry'
|
||||
import { spatialGridManager } from '../../hooks/spatial-grid/spatial-grid-manager'
|
||||
import { resolveLevelId } from '../../hooks/spatial-grid/spatial-grid-sync'
|
||||
import { getScaledDimensions, type AnyNodeId, type ItemNode, type WallNode } from '../../schema'
|
||||
import { type AnyNodeId, getScaledDimensions, type ItemNode, type WallNode } from '../../schema'
|
||||
import useScene from '../../store/use-scene'
|
||||
|
||||
// ============================================================================
|
||||
@@ -32,7 +32,7 @@ export const ItemSystem = () => {
|
||||
if (parentWall && parentWall.type === 'wall') {
|
||||
const wallThickness = (parentWall as WallNode).thickness ?? 0.1
|
||||
const side = item.side === 'front' ? 1 : -1
|
||||
mesh.position.z = (wallThickness / 2) * side;
|
||||
mesh.position.z = (wallThickness / 2) * side
|
||||
}
|
||||
} else if (!item.asset.attachTo) {
|
||||
// If parented to another item (surface placement), R3F handles positioning via the hierarchy
|
||||
@@ -51,8 +51,8 @@ export const ItemSystem = () => {
|
||||
}
|
||||
|
||||
clearDirty(id as AnyNodeId)
|
||||
}, 2)
|
||||
})
|
||||
})
|
||||
}, 2)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type CeilingNode, useRegistry } from '@pascal-app/core'
|
||||
import { useRef } from 'react'
|
||||
import { faceDirection, float, mix, positionWorld, smoothstep, step } from 'three/tsl'
|
||||
import { DoubleSide, type Mesh, MeshBasicNodeMaterial } from 'three/webgpu'
|
||||
import { type Mesh, MeshBasicNodeMaterial } from 'three/webgpu'
|
||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||
import { NodeRenderer } from '../node-renderer'
|
||||
|
||||
@@ -10,7 +10,6 @@ import { NodeRenderer } from '../node-renderer'
|
||||
// - Front face (looking down at ceiling from above): 30% opacity
|
||||
const ceilingMaterial = new MeshBasicNodeMaterial({
|
||||
color: 0x999999,
|
||||
side: DoubleSide,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
})
|
||||
@@ -30,8 +29,8 @@ const lineY = smoothstep(lineWidth, 0, gridY).add(smoothstep(1.0 - lineWidth, 1.
|
||||
// Combine: if either X or Y is a line, show the line
|
||||
const gridPattern = lineX.max(lineY)
|
||||
|
||||
// Grid lines at 0.8 opacity, spaces at 0.1 opacity
|
||||
const gridOpacity = mix(float(0.1), float(0.8), gridPattern)
|
||||
// Grid lines at 0.5 opacity, spaces at 0 opacity
|
||||
const gridOpacity = mix(float(0.0), float(0.5), gridPattern)
|
||||
|
||||
// faceDirection is 1.0 for front face, -1.0 for back face
|
||||
// Front face (top, looking down): grid pattern, Back face (bottom, looking up): solid
|
||||
|
||||
@@ -177,7 +177,9 @@ export const ZoneRenderer = ({ node }: { node: ZoneNode }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<group ref={ref} {...handlers}>
|
||||
<group ref={ref} {...handlers} userData={{
|
||||
labelPosition: [centroid[0], 1, centroid[1]]
|
||||
}}>
|
||||
<Html name="label" position={[centroid[0], 1, centroid[1]]} style={{
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
import type { Object3D } from "three";
|
||||
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
type SelectionPath = {
|
||||
buildingId: BuildingNode["id"] | null;
|
||||
@@ -57,62 +58,76 @@ type ViewerState = {
|
||||
setCameraDragging: (dragging: boolean) => void
|
||||
}
|
||||
|
||||
const useViewer = create<ViewerState>()((set, get) => ({
|
||||
selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] },
|
||||
hoveredId: null,
|
||||
setHoveredId: (id) => set({ hoveredId: id }),
|
||||
const useViewer = create<ViewerState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] },
|
||||
hoveredId: null,
|
||||
setHoveredId: (id) => set({ hoveredId: id }),
|
||||
|
||||
cameraMode: "perspective",
|
||||
setCameraMode: (mode) => set({ cameraMode: mode }),
|
||||
cameraMode: "perspective",
|
||||
setCameraMode: (mode) => set({ cameraMode: mode }),
|
||||
|
||||
levelMode: "stacked",
|
||||
setLevelMode: (mode) => set({ levelMode: mode }),
|
||||
levelMode: "stacked",
|
||||
setLevelMode: (mode) => set({ levelMode: mode }),
|
||||
|
||||
wallMode: 'cutaway',
|
||||
setWallMode: (mode) => set({ wallMode: mode }),
|
||||
wallMode: 'cutaway',
|
||||
setWallMode: (mode) => set({ wallMode: mode }),
|
||||
|
||||
showScans: true,
|
||||
setShowScans: (show) => set({ showScans: show }),
|
||||
showScans: true,
|
||||
setShowScans: (show) => set({ showScans: show }),
|
||||
|
||||
showGuides: true,
|
||||
setShowGuides: (show) => set({ showGuides: show }),
|
||||
showGuides: true,
|
||||
setShowGuides: (show) => set({ showGuides: show }),
|
||||
|
||||
setSelection: (updates) =>
|
||||
set((state) => {
|
||||
const newSelection = { ...state.selection, ...updates };
|
||||
setSelection: (updates) =>
|
||||
set((state) => {
|
||||
const newSelection = { ...state.selection, ...updates };
|
||||
|
||||
// Hierarchy Guard: If we change a high-level parent, reset the children
|
||||
if (updates.buildingId !== undefined) {
|
||||
newSelection.levelId = null;
|
||||
newSelection.zoneId = null;
|
||||
newSelection.selectedIds = [];
|
||||
} else if (updates.levelId !== undefined) {
|
||||
newSelection.zoneId = null;
|
||||
newSelection.selectedIds = [];
|
||||
} else if (updates.zoneId !== undefined) {
|
||||
newSelection.selectedIds = [];
|
||||
}
|
||||
// Hierarchy Guard: If we change a high-level parent, reset the children
|
||||
if (updates.buildingId !== undefined) {
|
||||
newSelection.levelId = null;
|
||||
newSelection.zoneId = null;
|
||||
newSelection.selectedIds = [];
|
||||
} else if (updates.levelId !== undefined) {
|
||||
newSelection.zoneId = null;
|
||||
newSelection.selectedIds = [];
|
||||
} else if (updates.zoneId !== undefined) {
|
||||
newSelection.selectedIds = [];
|
||||
}
|
||||
|
||||
return { selection: newSelection };
|
||||
return { selection: newSelection };
|
||||
}),
|
||||
|
||||
resetSelection: () =>
|
||||
set({
|
||||
selection: {
|
||||
buildingId: null,
|
||||
levelId: null,
|
||||
zoneId: null,
|
||||
selectedIds: [],
|
||||
},
|
||||
}),
|
||||
|
||||
outliner: { selectedObjects: [], hoveredObjects: [] },
|
||||
|
||||
exportScene: null,
|
||||
setExportScene: (fn) => set({ exportScene: fn }),
|
||||
|
||||
cameraDragging: false,
|
||||
setCameraDragging: (dragging) => set({ cameraDragging: dragging }),
|
||||
}),
|
||||
|
||||
resetSelection: () =>
|
||||
set({
|
||||
selection: {
|
||||
buildingId: null,
|
||||
levelId: null,
|
||||
zoneId: null,
|
||||
selectedIds: [],
|
||||
},
|
||||
}),
|
||||
|
||||
outliner: { selectedObjects: [], hoveredObjects: [] },
|
||||
|
||||
exportScene: null,
|
||||
setExportScene: (fn) => set({ exportScene: fn }),
|
||||
|
||||
cameraDragging: false,
|
||||
setCameraDragging: (dragging) => set({ cameraDragging: dragging }),
|
||||
}));
|
||||
{
|
||||
name: 'viewer-preferences',
|
||||
partialize: (state) => ({
|
||||
cameraMode: state.cameraMode,
|
||||
levelMode: state.levelMode,
|
||||
wallMode: state.wallMode,
|
||||
showScans: state.showScans,
|
||||
showGuides: state.showGuides,
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
export default useViewer;
|
||||
|
||||
Reference in New Issue
Block a user