Restore inline zone label editing on canvas (#134)
* fix: harden viewer/editor renderer init and error handling * Bump @biomejs/biome and turbo versions Update devDependencies: bump @biomejs/biome from ^2.4.4 to ^2.4.6 and turbo from ^2.8.12 to ^2.8.13. Regenerated bun.lock to reflect the updated resolved package versions. * Restore inline zone label editing on canvas
This commit is contained in:
@@ -138,7 +138,7 @@ export default function Editor({ projectId }: EditorProps) {
|
|||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
<ErrorBoundary key={projectId} fallback={<EditorSceneCrashFallback />}>
|
<ErrorBoundary key={projectId} fallback={<EditorSceneCrashFallback />}>
|
||||||
<Viewer selectionManager="custom">
|
<Viewer selectionManager="custom" allowZoneLabelEditing>
|
||||||
<SelectionManager />
|
<SelectionManager />
|
||||||
<FloatingActionMenu />
|
<FloatingActionMenu />
|
||||||
<ExportManager />
|
<ExportManager />
|
||||||
|
|||||||
@@ -30,10 +30,14 @@ export const ZoneSystem = () => {
|
|||||||
// Hide label if zone layer is off OR if in solo mode on a different level
|
// Hide label if zone layer is off OR if in solo mode on a different level
|
||||||
const showLabel = visible && !hideInSoloMode
|
const showLabel = visible && !hideInSoloMode
|
||||||
const targetOpacity = showLabel ? '1' : '0'
|
const targetOpacity = showLabel ? '1' : '0'
|
||||||
|
const targetPointerEvents = showLabel ? 'auto' : 'none'
|
||||||
const labelEl = document.getElementById(`${zoneId}-label`)
|
const labelEl = document.getElementById(`${zoneId}-label`)
|
||||||
if (labelEl && labelEl.style.opacity !== targetOpacity) {
|
if (labelEl && labelEl.style.opacity !== targetOpacity) {
|
||||||
labelEl.style.opacity = targetOpacity
|
labelEl.style.opacity = targetOpacity
|
||||||
}
|
}
|
||||||
|
if (labelEl && labelEl.style.pointerEvents !== targetPointerEvents) {
|
||||||
|
labelEl.style.pointerEvents = targetPointerEvents
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -215,23 +215,23 @@
|
|||||||
|
|
||||||
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="],
|
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="],
|
||||||
|
|
||||||
"@biomejs/biome": ["@biomejs/biome@2.4.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.4", "@biomejs/cli-darwin-x64": "2.4.4", "@biomejs/cli-linux-arm64": "2.4.4", "@biomejs/cli-linux-arm64-musl": "2.4.4", "@biomejs/cli-linux-x64": "2.4.4", "@biomejs/cli-linux-x64-musl": "2.4.4", "@biomejs/cli-win32-arm64": "2.4.4", "@biomejs/cli-win32-x64": "2.4.4" }, "bin": { "biome": "bin/biome" } }, "sha512-tigwWS5KfJf0cABVd52NVaXyAVv4qpUXOWJ1rxFL8xF1RVoeS2q/LK+FHgYoKMclJCuRoCWAPy1IXaN9/mS61Q=="],
|
"@biomejs/biome": ["@biomejs/biome@2.4.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.6", "@biomejs/cli-darwin-x64": "2.4.6", "@biomejs/cli-linux-arm64": "2.4.6", "@biomejs/cli-linux-arm64-musl": "2.4.6", "@biomejs/cli-linux-x64": "2.4.6", "@biomejs/cli-linux-x64-musl": "2.4.6", "@biomejs/cli-win32-arm64": "2.4.6", "@biomejs/cli-win32-x64": "2.4.6" }, "bin": { "biome": "bin/biome" } }, "sha512-QnHe81PMslpy3mnpL8DnO2M4S4ZnYPkjlGCLWBZT/3R9M6b5daArWMMtEfP52/n174RKnwRIf3oT8+wc9ihSfQ=="],
|
||||||
|
|
||||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jZ+Xc6qvD6tTH5jM6eKX44dcbyNqJHssfl2nnwT6vma6B1sj7ZLTGIk6N5QwVBs5xGN52r3trk5fgd3sQ9We9A=="],
|
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NW18GSyxr+8sJIqgoGwVp5Zqm4SALH4b4gftIA0n62PTuBs6G2tHlwNAOj0Vq0KKSs7Sf88VjjmHh0O36EnzrQ=="],
|
||||||
|
|
||||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-Dh1a/+W+SUCXhEdL7TiX3ArPTFCQKJTI1mGncZNWfO+6suk+gYA4lNyJcBB+pwvF49uw0pEbUS49BgYOY4hzUg=="],
|
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-4uiE/9tuI7cnjtY9b07RgS7gGyYOAfIAGeVJWEfeCnAarOAS7qVmuRyX6d7JTKw28/mt+rUzMasYeZ+0R/U1Mw=="],
|
||||||
|
|
||||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-V/NFfbWhsUU6w+m5WYbBenlEAz8eYnSqRMDMAW3K+3v0tYVkNyZn8VU0XPxk/lOqNXLSCCrV7FmV/u3SjCBShg=="],
|
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-kMLaI7OF5GN1Q8Doymjro1P8rVEoy7BKQALNz6fiR8IC1WKduoNyteBtJlHT7ASIL0Cx2jR6VUOBIbcB1B8pew=="],
|
||||||
|
|
||||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+sPAXq3bxmFwhVFJnSwkSF5Rw2ZAJMH3MF6C9IveAEOdSpgajPhoQhbbAK12SehN9j2QrHpk4J/cHsa/HqWaYQ=="],
|
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-F/JdB7eN22txiTqHM5KhIVt0jVkzZwVYrdTR1O3Y4auBOQcXxHK4dxULf4z43QyZI5tsnQJrRBHZy7wwtL+B3A=="],
|
||||||
|
|
||||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.4", "", { "os": "linux", "cpu": "x64" }, "sha512-R4+ZCDtG9kHArasyBO+UBD6jr/FcFCTH8QkNTOCu0pRJzCWyWC4EtZa2AmUZB5h3e0jD7bRV2KvrENcf8rndBg=="],
|
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oHXmUFEoH8Lql1xfc3QkFLiC1hGR7qedv5eKNlC185or+o4/4HiaU7vYODAH3peRCfsuLr1g6v2fK9dFFOYdyw=="],
|
||||||
|
|
||||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gGvFTGpOIQDb5CQ2VC0n9Z2UEqlP46c4aNgHmAMytYieTGEcfqhfCFnhs6xjt0S3igE6q5GLuIXtdQt3Izok+g=="],
|
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-C9s98IPDu7DYarjlZNuzJKTjVHN03RUnmHV5htvqsx6vEUXCDSJ59DNwjKVD5XYoSS4N+BYhq3RTBAL8X6svEg=="],
|
||||||
|
|
||||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-trzCqM7x+Gn832zZHgr28JoYagQNX4CZkUZhMUac2YxvvyDRLJDrb5m9IA7CaZLlX6lTQmADVfLEKP1et1Ma4Q=="],
|
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-xzThn87Pf3YrOGTEODFGONmqXpTwUNxovQb72iaUOdcw8sBSY3+3WD8Hm9IhMYLnPi0n32s3L3NWU6+eSjfqFg=="],
|
||||||
|
|
||||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.4", "", { "os": "win32", "cpu": "x64" }, "sha512-gnOHKVPFAAPrpoPt2t+Q6FZ7RPry/FDV3GcpU53P3PtLNnQjBmKyN2Vh/JtqXet+H4pme8CC76rScwdjDcT1/A=="],
|
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.6", "", { "os": "win32", "cpu": "x64" }, "sha512-7++XhnsPlr1HDbor5amovPjOH6vsrFOCdp93iKXhFn6bcMUI6soodj3WWKfgEO6JosKU1W5n3uky3WW9RlRjTg=="],
|
||||||
|
|
||||||
"@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@10.5.0", "", { "dependencies": { "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw=="],
|
"@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@10.5.0", "", { "dependencies": { "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw=="],
|
||||||
|
|
||||||
@@ -1669,19 +1669,19 @@
|
|||||||
|
|
||||||
"tunnel-rat": ["tunnel-rat@0.1.2", "", { "dependencies": { "zustand": "^4.3.2" } }, "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ=="],
|
"tunnel-rat": ["tunnel-rat@0.1.2", "", { "dependencies": { "zustand": "^4.3.2" } }, "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ=="],
|
||||||
|
|
||||||
"turbo": ["turbo@2.8.12", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.12", "turbo-darwin-arm64": "2.8.12", "turbo-linux-64": "2.8.12", "turbo-linux-arm64": "2.8.12", "turbo-windows-64": "2.8.12", "turbo-windows-arm64": "2.8.12" }, "bin": { "turbo": "bin/turbo" } }, "sha512-auUAMLmi0eJhxDhQrxzvuhfEbICnVt0CTiYQYY8WyRJ5nwCDZxD0JG8bCSxT4nusI2CwJzmZAay5BfF6LmK7Hw=="],
|
"turbo": ["turbo@2.8.13", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.13", "turbo-darwin-arm64": "2.8.13", "turbo-linux-64": "2.8.13", "turbo-linux-arm64": "2.8.13", "turbo-windows-64": "2.8.13", "turbo-windows-arm64": "2.8.13" }, "bin": { "turbo": "bin/turbo" } }, "sha512-nyM99hwFB9/DHaFyKEqatdayGjsMNYsQ/XBNO6MITc7roncZetKb97MpHxWf3uiU+LB9c9HUlU3Jp2Ixei2k1A=="],
|
||||||
|
|
||||||
"turbo-darwin-64": ["turbo-darwin-64@2.8.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-EiHJmW2MeQQx+21x8hjMHw/uPhXt9PIxvDrxzOtyVwrXzL0tQmsxtO4qHf2l7uA+K6PUJ4+TjY1MHZDuCvWXrw=="],
|
"turbo-darwin-64": ["turbo-darwin-64@2.8.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-PmOvodQNiOj77+Zwoqku70vwVjKzL34RTNxxoARjp5RU5FOj/CGiC6vcDQhNtFPUOWSAaogHF5qIka9TBhX4XA=="],
|
||||||
|
|
||||||
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cbqqGN0vd7ly2TeuaM8k9AK9u1CABO4kBA5KPSqovTiLL3sORccn/mZzJSbvQf0EsYRfU34MgW5FotfwW3kx8Q=="],
|
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kI+anKcLIM4L8h+NsM7mtAUpElkCOxv5LgiQVQR8BASyDFfc8Efj5kCk3cqxuxOvIqx0sLfCX7atrHQ2kwuNJQ=="],
|
||||||
|
|
||||||
"turbo-linux-64": ["turbo-linux-64@2.8.12", "", { "os": "linux", "cpu": "x64" }, "sha512-jXKw9j4r4q6s0goSXuKI3aKbQK2qiNeP25lGGEnq018TM6SWRW1CCpPMxyG91aCKrub7wDm/K45sGNT4ZFBcFQ=="],
|
"turbo-linux-64": ["turbo-linux-64@2.8.13", "", { "os": "linux", "cpu": "x64" }, "sha512-j29KnQhHyzdzgCykBFeBqUPS4Wj7lWMnZ8CHqytlYDap4Jy70l4RNG46pOL9+lGu6DepK2s1rE86zQfo0IOdPw=="],
|
||||||
|
|
||||||
"turbo-linux-arm64": ["turbo-linux-arm64@2.8.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-BRJCMdyXjyBoL0GYpvj9d2WNfMHwc3tKmJG5ATn2Efvil9LsiOsd/93/NxDqW0jACtHFNVOPnd/CBwXRPiRbwA=="],
|
"turbo-linux-arm64": ["turbo-linux-arm64@2.8.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-OEl1YocXGZDRDh28doOUn49QwNe82kXljO1HXApjU0LapkDiGpfl3jkAlPKxEkGDSYWc8MH5Ll8S16Rf5tEBYg=="],
|
||||||
|
|
||||||
"turbo-windows-64": ["turbo-windows-64@2.8.12", "", { "os": "win32", "cpu": "x64" }, "sha512-vyFOlpFFzQFkikvSVhVkESEfzIopgs2J7J1rYvtSwSHQ4zmHxkC95Q8Kjkus8gg+8X2mZyP1GS5jirmaypGiPw=="],
|
"turbo-windows-64": ["turbo-windows-64@2.8.13", "", { "os": "win32", "cpu": "x64" }, "sha512-717bVk1+Pn2Jody7OmWludhEirEe0okoj1NpRbSm5kVZz/yNN/jfjbxWC6ilimXMz7xoMT3IDfQFJsFR3PMANA=="],
|
||||||
|
|
||||||
"turbo-windows-arm64": ["turbo-windows-arm64@2.8.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-9nRnlw5DF0LkJClkIws1evaIF36dmmMEO84J5Uj4oQ8C0QTHwlH7DNe5Kq2Jdmu8GXESCNDNuUYG8Cx6W/vm3g=="],
|
"turbo-windows-arm64": ["turbo-windows-arm64@2.8.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-R819HShLIT0Wj6zWVnIsYvSNtRNj1q9VIyaUz0P24SMcLCbQZIm1sV09F4SDbg+KCCumqD2lcaR2UViQ8SnUJA=="],
|
||||||
|
|
||||||
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -30,9 +30,9 @@
|
|||||||
"portless": "^0.4.2"
|
"portless": "^0.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.4.4",
|
"@biomejs/biome": "^2.4.6",
|
||||||
"supabase": "2.76.15",
|
"supabase": "2.76.15",
|
||||||
"turbo": "^2.8.12",
|
"turbo": "^2.8.13",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"ultracite": "^7.2.4"
|
"ultracite": "^7.2.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useRegistry, type ZoneNode } from '@pascal-app/core'
|
import { useRegistry, useScene, type ZoneNode } from '@pascal-app/core'
|
||||||
import { Html } from '@react-three/drei'
|
import { Html } from '@react-three/drei'
|
||||||
import { useMemo, useRef } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { BufferGeometry, Color, DoubleSide, Float32BufferAttribute, type Group, Shape } from 'three'
|
import { BufferGeometry, Color, DoubleSide, Float32BufferAttribute, type Group, Shape } from 'three'
|
||||||
import { color, float, uniform, uv } from 'three/tsl'
|
import { color, float, uniform, uv } from 'three/tsl'
|
||||||
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
||||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||||
|
import useViewer from '../../../store/use-viewer'
|
||||||
|
|
||||||
const Y_OFFSET = 0.01
|
const Y_OFFSET = 0.01
|
||||||
const WALL_HEIGHT = 2.3
|
const WALL_HEIGHT = 2.3
|
||||||
@@ -104,9 +105,75 @@ const createWallGeometry = (polygon: Array<[number, number]>): BufferGeometry =>
|
|||||||
|
|
||||||
export const ZoneRenderer = ({ node }: { node: ZoneNode }) => {
|
export const ZoneRenderer = ({ node }: { node: ZoneNode }) => {
|
||||||
const ref = useRef<Group>(null!)
|
const ref = useRef<Group>(null!)
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const ignoreBlurSaveRef = useRef(false)
|
||||||
|
const updateNode = useScene((state) => state.updateNode)
|
||||||
|
const setSelection = useViewer((state) => state.setSelection)
|
||||||
|
const setHoveredId = useViewer((state) => state.setHoveredId)
|
||||||
|
const allowZoneLabelEditing = useViewer((state) => state.allowZoneLabelEditing)
|
||||||
|
const [isEditingLabel, setIsEditingLabel] = useState(false)
|
||||||
|
const [isLabelHovered, setIsLabelHovered] = useState(false)
|
||||||
|
const [labelValue, setLabelValue] = useState(node.name || '')
|
||||||
|
|
||||||
useRegistry(node.id, 'zone', ref)
|
useRegistry(node.id, 'zone', ref)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isEditingLabel) {
|
||||||
|
setLabelValue(node.name || '')
|
||||||
|
}
|
||||||
|
}, [isEditingLabel, node.name])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isEditingLabel || !inputRef.current) return
|
||||||
|
inputRef.current.focus()
|
||||||
|
inputRef.current.select()
|
||||||
|
}, [isEditingLabel])
|
||||||
|
|
||||||
|
const stopLabelEditing = useCallback(
|
||||||
|
(save: boolean) => {
|
||||||
|
if (save) {
|
||||||
|
const trimmed = labelValue.trim()
|
||||||
|
if (trimmed !== (node.name || '')) {
|
||||||
|
updateNode(node.id, { name: trimmed || undefined })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setLabelValue(node.name || '')
|
||||||
|
}
|
||||||
|
setIsEditingLabel(false)
|
||||||
|
},
|
||||||
|
[labelValue, node.id, node.name, updateNode],
|
||||||
|
)
|
||||||
|
|
||||||
|
const startLabelEditing = useCallback(
|
||||||
|
(event?: { stopPropagation: () => void; preventDefault: () => void }) => {
|
||||||
|
if (!allowZoneLabelEditing) return
|
||||||
|
event?.stopPropagation()
|
||||||
|
event?.preventDefault()
|
||||||
|
setSelection({ zoneId: node.id })
|
||||||
|
ignoreBlurSaveRef.current = false
|
||||||
|
setIsEditingLabel(true)
|
||||||
|
setLabelValue(node.name || '')
|
||||||
|
},
|
||||||
|
[allowZoneLabelEditing, node.id, node.name, setSelection],
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isEditingLabel) return
|
||||||
|
|
||||||
|
const onPointerDownOutside = (event: PointerEvent) => {
|
||||||
|
const target = event.target
|
||||||
|
if (!(target instanceof Node)) return
|
||||||
|
if (inputRef.current?.contains(target)) return
|
||||||
|
ignoreBlurSaveRef.current = true
|
||||||
|
stopLabelEditing(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('pointerdown', onPointerDownOutside, true)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('pointerdown', onPointerDownOutside, true)
|
||||||
|
}
|
||||||
|
}, [isEditingLabel, stopLabelEditing])
|
||||||
|
|
||||||
// Create floor shape from polygon
|
// Create floor shape from polygon
|
||||||
const floorShape = useMemo(() => {
|
const floorShape = useMemo(() => {
|
||||||
if (!node?.polygon || node.polygon.length < 3) return null
|
if (!node?.polygon || node.polygon.length < 3) return null
|
||||||
@@ -176,13 +243,12 @@ 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: allowZoneLabelEditing ? 'auto' : 'none' }}
|
||||||
zIndexRange={[10, 0]}
|
zIndexRange={[10, 0]}
|
||||||
>
|
>
|
||||||
<div id={`${node.id}-label`} style={{
|
<div id={`${node.id}-label`} style={{
|
||||||
@@ -200,8 +266,112 @@ export const ZoneRenderer = ({ node }: { node: ZoneNode }) => {
|
|||||||
textShadow: `-1px -1px 0 ${node.color}, 1px -1px 0 ${node.color}, -1px 1px 0 ${node.color}, 1px 1px 0 ${node.color}`,
|
textShadow: `-1px -1px 0 ${node.color}, 1px -1px 0 ${node.color}, -1px 1px 0 ${node.color}, 1px 1px 0 ${node.color}`,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
}}
|
}}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
setHoveredId(node.id)
|
||||||
|
setIsLabelHovered(true)
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
setHoveredId(null)
|
||||||
|
setIsLabelHovered(false)
|
||||||
|
}}
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
if (allowZoneLabelEditing) {
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span>{node.name}</span>
|
{allowZoneLabelEditing && isEditingLabel ? (
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
value={labelValue}
|
||||||
|
onChange={(event) => setLabelValue(event.target.value)}
|
||||||
|
onBlur={() => {
|
||||||
|
if (ignoreBlurSaveRef.current) {
|
||||||
|
ignoreBlurSaveRef.current = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stopLabelEditing(true)
|
||||||
|
}}
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault()
|
||||||
|
ignoreBlurSaveRef.current = true
|
||||||
|
stopLabelEditing(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault()
|
||||||
|
ignoreBlurSaveRef.current = true
|
||||||
|
stopLabelEditing(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
width: `${Math.max((labelValue || node.name || '').length + 1, 4)}ch`,
|
||||||
|
maxWidth: '220px',
|
||||||
|
border: 'none',
|
||||||
|
borderBottom: `1px solid ${node.color}`,
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
color: 'white',
|
||||||
|
textShadow: `-1px -1px 0 ${node.color}, 1px -1px 0 ${node.color}, -1px 1px 0 ${node.color}, 1px 1px 0 ${node.color}`,
|
||||||
|
outline: 'none',
|
||||||
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
|
fontSize: 'inherit',
|
||||||
|
lineHeight: 'inherit',
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={startLabelEditing}
|
||||||
|
style={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '4px',
|
||||||
|
border: 'none',
|
||||||
|
padding: '0',
|
||||||
|
background: 'none',
|
||||||
|
color: 'inherit',
|
||||||
|
cursor: allowZoneLabelEditing ? 'text' : 'default',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{node.name}</span>
|
||||||
|
{allowZoneLabelEditing && (
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
style={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: '12px',
|
||||||
|
height: '12px',
|
||||||
|
opacity: isLabelHovered ? 1 : 0.55,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="none"
|
||||||
|
height="12"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="12"
|
||||||
|
>
|
||||||
|
<path d="M12 20h9" />
|
||||||
|
<path d="M16.5 3.5a2.1 2.1 0 1 1 3 3L7 19l-4 1 1-4z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="label-pin"
|
className="label-pin"
|
||||||
@@ -212,6 +382,7 @@ export const ZoneRenderer = ({ node }: { node: ZoneNode }) => {
|
|||||||
marginTop: '2px',
|
marginTop: '2px',
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
transition: 'opacity 0.5s ease-in-out',
|
transition: 'opacity 0.5s ease-in-out',
|
||||||
|
pointerEvents: 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
} from '@pascal-app/core'
|
} from '@pascal-app/core'
|
||||||
import { Bvh } from '@react-three/drei'
|
import { Bvh } from '@react-three/drei'
|
||||||
import { Canvas, extend, type ThreeToJSXElements, useFrame } from '@react-three/fiber'
|
import { Canvas, extend, type ThreeToJSXElements, useFrame } from '@react-three/fiber'
|
||||||
import { useMemo, useRef } from 'react'
|
import { useEffect, useMemo, useRef } from 'react'
|
||||||
import * as THREE from 'three/webgpu'
|
import * as THREE from 'three/webgpu'
|
||||||
import useViewer from '../../store/use-viewer'
|
import useViewer from '../../store/use-viewer'
|
||||||
import { GuideSystem } from '../../systems/guide/guide-system'
|
import { GuideSystem } from '../../systems/guide/guide-system'
|
||||||
@@ -62,10 +62,20 @@ extend(THREE as any)
|
|||||||
interface ViewerProps {
|
interface ViewerProps {
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
selectionManager?: 'default' | 'custom'
|
selectionManager?: 'default' | 'custom'
|
||||||
|
allowZoneLabelEditing?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Viewer: React.FC<ViewerProps> = ({ children, selectionManager = 'default' }) => {
|
const Viewer: React.FC<ViewerProps> = ({
|
||||||
|
children,
|
||||||
|
selectionManager = 'default',
|
||||||
|
allowZoneLabelEditing = false,
|
||||||
|
}) => {
|
||||||
const theme = useViewer((state) => state.theme)
|
const theme = useViewer((state) => state.theme)
|
||||||
|
const setAllowZoneLabelEditing = useViewer((state) => state.setAllowZoneLabelEditing)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setAllowZoneLabelEditing(allowZoneLabelEditing)
|
||||||
|
}, [allowZoneLabelEditing, setAllowZoneLabelEditing])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Canvas
|
<Canvas
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ type ViewerState = {
|
|||||||
selection: SelectionPath
|
selection: SelectionPath
|
||||||
hoveredId: AnyNode['id'] | ZoneNode['id'] | null
|
hoveredId: AnyNode['id'] | ZoneNode['id'] | null
|
||||||
setHoveredId: (id: AnyNode['id'] | ZoneNode['id'] | null) => void
|
setHoveredId: (id: AnyNode['id'] | ZoneNode['id'] | null) => void
|
||||||
|
allowZoneLabelEditing: boolean
|
||||||
|
setAllowZoneLabelEditing: (allow: boolean) => void
|
||||||
|
|
||||||
cameraMode: 'perspective' | 'orthographic'
|
cameraMode: 'perspective' | 'orthographic'
|
||||||
setCameraMode: (mode: 'perspective' | 'orthographic') => void
|
setCameraMode: (mode: 'perspective' | 'orthographic') => void
|
||||||
@@ -74,6 +76,8 @@ const useViewer = create<ViewerState>()(
|
|||||||
selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] },
|
selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] },
|
||||||
hoveredId: null,
|
hoveredId: null,
|
||||||
setHoveredId: (id) => set({ hoveredId: id }),
|
setHoveredId: (id) => set({ hoveredId: id }),
|
||||||
|
allowZoneLabelEditing: false,
|
||||||
|
setAllowZoneLabelEditing: (allow) => set({ allowZoneLabelEditing: allow }),
|
||||||
|
|
||||||
cameraMode: "perspective",
|
cameraMode: "perspective",
|
||||||
setCameraMode: (mode) => set({ cameraMode: mode }),
|
setCameraMode: (mode) => set({ cameraMode: mode }),
|
||||||
|
|||||||
Reference in New Issue
Block a user