Merge pull request #131 from pascalorg/feat/zone-labels-polish
Feat/zone labels polish
This commit is contained in:
@@ -25,16 +25,10 @@ export const ViewerZoneSystem = () => {
|
|||||||
|
|
||||||
obj.visible = shouldShow
|
obj.visible = shouldShow
|
||||||
|
|
||||||
// Also hide the label
|
const targetOpacity = shouldShow ? '1' : '0'
|
||||||
const label = obj.getObjectByName('label')
|
const labelEl = document.getElementById(`${id}-label`)
|
||||||
if (label) {
|
if (labelEl && labelEl.style.opacity !== targetOpacity) {
|
||||||
// Hide label if zone layer is off OR if in solo mode on a different level
|
labelEl.style.opacity = targetOpacity
|
||||||
const labelPosition = obj.userData.labelPosition as [number, number, number] | undefined
|
|
||||||
if (shouldShow && labelPosition) {
|
|
||||||
label.position.set(...labelPosition)
|
|
||||||
} else {
|
|
||||||
label.position.set(-9999, -9999, -9999)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -27,20 +27,12 @@ export const ZoneSystem = () => {
|
|||||||
obj.visible = visible
|
obj.visible = visible
|
||||||
}
|
}
|
||||||
|
|
||||||
const label = obj.getObjectByName('label')
|
// Hide label if zone layer is off OR if in solo mode on a different level
|
||||||
if (label) {
|
const showLabel = visible && !hideInSoloMode
|
||||||
// Hide label if zone layer is off OR if in solo mode on a different level
|
const targetOpacity = showLabel ? '1' : '0'
|
||||||
const showLabel = visible && !hideInSoloMode;
|
const labelEl = document.getElementById(`${zoneId}-label`)
|
||||||
const labelPosition = obj.userData.labelPosition as [number, number, number] | undefined
|
if (labelEl && labelEl.style.opacity !== targetOpacity) {
|
||||||
|
labelEl.style.opacity = targetOpacity
|
||||||
const targetX = showLabel && labelPosition ? labelPosition[0] : -9999
|
|
||||||
if (label.position.x !== targetX) {
|
|
||||||
if (showLabel && labelPosition) {
|
|
||||||
label.position.set(...labelPosition)
|
|
||||||
} else {
|
|
||||||
label.position.set(-9999, -9999, -9999)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ const createWallGradientMaterial = (zoneColor: string) => {
|
|||||||
// Use UV y coordinate for vertical gradient (0 at bottom, 1 at top)
|
// Use UV y coordinate for vertical gradient (0 at bottom, 1 at top)
|
||||||
const gradientT = uv().y
|
const gradientT = uv().y
|
||||||
|
|
||||||
const opacity = uniform(0);
|
const opacity = uniform(0)
|
||||||
// Fade opacity from 0.6 at bottom to 0 at top
|
// Fade opacity from 0.6 at bottom to 0 at top
|
||||||
const finalOpacity = float(0.6).mul(float(1).sub(gradientT)).mul(opacity);
|
const finalOpacity = float(0.6).mul(float(1).sub(gradientT)).mul(opacity)
|
||||||
|
|
||||||
return new MeshBasicNodeMaterial({
|
return new MeshBasicNodeMaterial({
|
||||||
transparent: true,
|
transparent: true,
|
||||||
@@ -32,7 +32,7 @@ const createWallGradientMaterial = (zoneColor: string) => {
|
|||||||
depthTest: false,
|
depthTest: false,
|
||||||
userData: {
|
userData: {
|
||||||
uOpacity: opacity,
|
uOpacity: opacity,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ const createFloorMaterial = (zoneColor: string) => {
|
|||||||
side: DoubleSide,
|
side: DoubleSide,
|
||||||
depthWrite: false,
|
depthWrite: false,
|
||||||
depthTest: false,
|
depthTest: false,
|
||||||
userData: { uOpacity: opacity}
|
userData: { uOpacity: opacity },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,25 +176,70 @@ export const ZoneRenderer = ({ node }: { node: ZoneNode }) => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group ref={ref} {...handlers} userData={{ labelPosition: [centroid[0], 1, centroid[1]] }}>
|
<group ref={ref} {...handlers} userData={{ labelPosition: [centroid[0], 1, centroid[1]] }}>
|
||||||
<Html
|
<Html
|
||||||
name="label"
|
name="label"
|
||||||
position={[centroid[0], 1, centroid[1]]}
|
position={[centroid[0], 1, centroid[1]]}
|
||||||
style={{ pointerEvents: 'none' }}
|
style={{ pointerEvents: 'none', }}
|
||||||
zIndexRange={[10, 0]}
|
zIndexRange={[10, 0]}
|
||||||
>
|
>
|
||||||
<div style={{
|
<div id={`${node.id}-label`} style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
transform: 'translate3d(-50%, -50%, 0)',
|
transform: 'translate3d(-50%, -50%, 0)',
|
||||||
width: 'max-content',
|
opacity: 0,
|
||||||
color: 'white',
|
transition: 'opacity 0.3s ease-in-out',
|
||||||
textShadow: `-1px -1px 0 ${node.color}, 1px -1px 0 ${node.color}, -1px 1px 0 ${node.color}, 1px 1px 0 ${node.color}`,
|
|
||||||
}}>
|
}}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 'max-content',
|
||||||
|
color: 'white',
|
||||||
|
textShadow: `-1px -1px 0 ${node.color}, 1px -1px 0 ${node.color}, -1px 1px 0 ${node.color}, 1px 1px 0 ${node.color}`,
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<span>{node.name}</span>
|
<span>{node.name}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className="label-pin"
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: '2px',
|
||||||
|
opacity: 0,
|
||||||
|
transition: 'opacity 0.5s ease-in-out',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '2px',
|
||||||
|
height: '40px',
|
||||||
|
backgroundColor: node.color,
|
||||||
|
}}/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '10px',
|
||||||
|
height: '10px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: node.color,
|
||||||
|
border: `1px solid white`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Html>
|
</Html>
|
||||||
|
|
||||||
{/* Floor fill */}
|
{/* Floor fill */}
|
||||||
<mesh position={[0, Y_OFFSET, 0]} rotation={[-Math.PI / 2, 0, 0]} material={floorMaterial} name="floor">
|
<mesh
|
||||||
|
position={[0, Y_OFFSET, 0]}
|
||||||
|
rotation={[-Math.PI / 2, 0, 0]}
|
||||||
|
material={floorMaterial}
|
||||||
|
name="floor"
|
||||||
|
>
|
||||||
<shapeGeometry args={[floorShape]} />
|
<shapeGeometry args={[floorShape]} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
|
|||||||
@@ -6,25 +6,56 @@ import type { MeshBasicNodeMaterial } from 'three/webgpu'
|
|||||||
import useViewer from '../../store/use-viewer'
|
import useViewer from '../../store/use-viewer'
|
||||||
|
|
||||||
const TRANSITION_DURATION = 400 // ms
|
const TRANSITION_DURATION = 400 // ms
|
||||||
|
const EXIT_DEBOUNCE_MS = 50 // ignore rapid exit→re-enter within this window
|
||||||
|
|
||||||
export const ZoneSystem = () => {
|
export const ZoneSystem = () => {
|
||||||
const lastHighlightedZoneRef = useRef<string | null>(null)
|
const lastHighlightedZoneRef = useRef<string | null>(null)
|
||||||
const lastChangeTimeRef = useRef(0)
|
const lastChangeTimeRef = useRef(0)
|
||||||
const isTransitioningRef = useRef(false)
|
const isTransitioningRef = useRef(false)
|
||||||
|
// Debounce exit-to-null: track the raw pending value and when it last changed
|
||||||
|
const pendingZoneRef = useRef<string | null>(null)
|
||||||
|
const pendingZoneSinceRef = useRef(0)
|
||||||
|
|
||||||
useFrame(({clock}, delta) => {
|
useFrame(({clock}, delta) => {
|
||||||
const hoveredId = useViewer.getState().hoveredId
|
const hoveredId = useViewer.getState().hoveredId
|
||||||
let highlightedZone: string | null = null
|
let rawZone: string | null = null
|
||||||
|
|
||||||
if (hoveredId) {
|
if (hoveredId) {
|
||||||
const hoveredNode = useScene.getState().nodes[hoveredId]
|
const hoveredNode = useScene.getState().nodes[hoveredId]
|
||||||
if (hoveredNode?.type === 'zone') {
|
if (hoveredNode?.type === 'zone') {
|
||||||
highlightedZone = hoveredId
|
rawZone = hoveredId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect zone change
|
// Update pending zone when the raw value changes
|
||||||
|
if (rawZone !== pendingZoneRef.current) {
|
||||||
|
pendingZoneRef.current = rawZone
|
||||||
|
pendingZoneSinceRef.current = clock.elapsedTime * 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply non-null immediately; debounce null to filter out brief exits
|
||||||
|
const age = clock.elapsedTime * 1000 - pendingZoneSinceRef.current
|
||||||
|
const highlightedZone = rawZone !== null
|
||||||
|
? rawZone
|
||||||
|
: age >= EXIT_DEBOUNCE_MS ? null : lastHighlightedZoneRef.current
|
||||||
|
|
||||||
|
// Detect stable zone change
|
||||||
if (highlightedZone !== lastHighlightedZoneRef.current) {
|
if (highlightedZone !== lastHighlightedZoneRef.current) {
|
||||||
|
// Fade out previous zone label-pin
|
||||||
|
if (lastHighlightedZoneRef.current) {
|
||||||
|
const prevLabel = document.getElementById(`${lastHighlightedZoneRef.current}-label`)
|
||||||
|
const pin = prevLabel?.querySelector('.label-pin') as HTMLElement | null
|
||||||
|
if (pin) pin.style.opacity = '0'
|
||||||
|
console.log('setting opacity to 0 for', lastHighlightedZoneRef.current)
|
||||||
|
}
|
||||||
|
// Fade in new zone label-pin
|
||||||
|
if (highlightedZone) {
|
||||||
|
const label = document.getElementById(`${highlightedZone}-label`)
|
||||||
|
const pin = label?.querySelector('.label-pin') as HTMLElement | null
|
||||||
|
if (pin) pin.style.opacity = '1'
|
||||||
|
console.log('setting opacity to 1 for', highlightedZone)
|
||||||
|
}
|
||||||
|
|
||||||
lastHighlightedZoneRef.current = highlightedZone
|
lastHighlightedZoneRef.current = highlightedZone
|
||||||
lastChangeTimeRef.current = clock.elapsedTime * 1000
|
lastChangeTimeRef.current = clock.elapsedTime * 1000
|
||||||
isTransitioningRef.current = true
|
isTransitioningRef.current = true
|
||||||
@@ -63,6 +94,7 @@ export const ZoneSystem = () => {
|
|||||||
const currentOpacity = material.userData.uOpacity.value
|
const currentOpacity = material.userData.uOpacity.value
|
||||||
material.userData.uOpacity.value = MathUtils.lerp(currentOpacity, targetOpacity, lerpSpeed)
|
material.userData.uOpacity.value = MathUtils.lerp(currentOpacity, targetOpacity, lerpSpeed)
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user