Merge pull request #140 from pascalorg/feat/zone-fix-layer
Feat/zone fix layer
This commit is contained in:
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../.cursor/rules/layers.mdc
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
description: How to create and maintain project rules
|
description: How to create and maintain project rules
|
||||||
globs:
|
globs: .cursor/rules/**
|
||||||
alwaysApply: false
|
alwaysApply: false
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -76,3 +76,4 @@ Concrete guidance with examples.
|
|||||||
| `events` | Typed event bus — emitting and listening to node and grid events |
|
| `events` | Typed event bus — emitting and listening to node and grid events |
|
||||||
| `node-schemas` | Zod schema pattern for node types, createNode, updateNode |
|
| `node-schemas` | Zod schema pattern for node types, createNode, updateNode |
|
||||||
| `spatial-queries` | Placement validation (canPlaceOnFloor/Wall/Ceiling) for tools |
|
| `spatial-queries` | Placement validation (canPlaceOnFloor/Wall/Ceiling) for tools |
|
||||||
|
| `layers` | Three.js layer constants, ownership, and rendering separation |
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
description: Three.js layer conventions — which layer each object type lives on and why
|
||||||
|
globs: packages/viewer/**,apps/editor/**
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Three.js Layers
|
||||||
|
|
||||||
|
Three.js `Layers` control which objects each camera and render pass sees. We use them to separate scene geometry, editor helpers, and zone overlays into distinct rendering buckets without duplicating scene structure.
|
||||||
|
|
||||||
|
## Layer Map
|
||||||
|
|
||||||
|
| Constant | Value | Package | Purpose |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `SCENE_LAYER` | `0` | `@pascal-app/viewer` | Default Three.js layer — all regular scene geometry |
|
||||||
|
| `EDITOR_LAYER` | `1` | `apps/editor` | Editor-only helpers: grid, tool previews, cursor meshes, snap guides |
|
||||||
|
| `ZONE_LAYER` | `2` | `@pascal-app/viewer` | Zone floor fills and wall borders — composited in a separate post-processing pass |
|
||||||
|
|
||||||
|
Import the constants from their owning packages:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// In viewer code
|
||||||
|
import { SCENE_LAYER, ZONE_LAYER } from '@pascal-app/viewer'
|
||||||
|
|
||||||
|
// In editor code
|
||||||
|
import { EDITOR_LAYER } from '@/lib/constants'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why Separate Zones onto Layer 2
|
||||||
|
|
||||||
|
Zones use semi-transparent, `depthTest: false` materials that must be composited *on top of* the scene without being fed into SSGI or TRAA. The post-processing pipeline in `post-processing.tsx` renders a dedicated `zonePass` with a `Layers` mask that enables only `ZONE_LAYER` (and disables `SCENE_LAYER`), then blends its output into the final composite manually:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const zoneLayers = useMemo(() => {
|
||||||
|
const l = new Layers()
|
||||||
|
l.enable(ZONE_LAYER)
|
||||||
|
l.disable(SCENE_LAYER)
|
||||||
|
return l
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
zonePass.setLayers(zoneLayers)
|
||||||
|
```
|
||||||
|
|
||||||
|
This keeps zones out of the SSGI depth/normal buffers (which would produce incorrect AO on transparent surfaces) while still letting them appear correctly over the scene.
|
||||||
|
|
||||||
|
## Why Separate Editor Helpers onto Layer 1
|
||||||
|
|
||||||
|
The editor camera enables `EDITOR_LAYER` so tools and helpers are visible during editing. The thumbnail generator disables `EDITOR_LAYER` so exports show clean geometry without snap lines or cursor spheres.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- **Never hardcode layer numbers.** Always use the named constants.
|
||||||
|
- **`SCENE_LAYER` and `ZONE_LAYER` belong in `@pascal-app/viewer`** — they are renderer concerns, not editor concerns.
|
||||||
|
- **`EDITOR_LAYER` belongs in `apps/editor`** — the viewer must never import it; editor behaviour is injected via props/children.
|
||||||
|
- **Zone meshes must set `layers={ZONE_LAYER}`** so they are picked up by `zonePass` and excluded from `scenePass` depth buffers.
|
||||||
|
- **Editor helper meshes must set `layers={EDITOR_LAYER}`** so they are invisible to the thumbnail camera and the viewer's render passes.
|
||||||
|
- **Do not add new layers without updating this rule** and the post-processing pipeline accordingly.
|
||||||
@@ -26,6 +26,7 @@ export const CustomCameraControls = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
camera.layers.enable(EDITOR_LAYER)
|
camera.layers.enable(EDITOR_LAYER)
|
||||||
raycaster.layers.enable(EDITOR_LAYER)
|
raycaster.layers.enable(EDITOR_LAYER)
|
||||||
|
raycaster.layers.enable(2)
|
||||||
}, [camera, raycaster])
|
}, [camera, raycaster])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -29,13 +29,11 @@ function ZoneLabelEditor({ zoneId }: { zoneId: ZoneNode['id'] }) {
|
|||||||
const el = document.getElementById(`${zoneId}-label`)
|
const el = document.getElementById(`${zoneId}-label`)
|
||||||
if (!el) return
|
if (!el) return
|
||||||
setLabelEl(el)
|
setLabelEl(el)
|
||||||
el.style.pointerEvents = 'auto'
|
|
||||||
|
|
||||||
const textEl = el.children[0] as HTMLElement | undefined
|
const textEl = el.children[0] as HTMLElement | undefined
|
||||||
if (textEl) textEl.style.display = 'none'
|
if (textEl) textEl.style.display = 'none'
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
el.style.pointerEvents = ''
|
|
||||||
if (textEl) textEl.style.display = ''
|
if (textEl) textEl.style.display = ''
|
||||||
}
|
}
|
||||||
}, [zoneId])
|
}, [zoneId])
|
||||||
@@ -79,6 +77,7 @@ function ZoneLabelEditor({ zoneId }: { zoneId: ZoneNode['id'] }) {
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontFamily: 'sans-serif',
|
fontFamily: 'sans-serif',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
|
pointerEvents: 'auto',
|
||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { BufferGeometry, Color, DoubleSide, Float32BufferAttribute, type Group,
|
|||||||
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 { ZONE_LAYER } from '../../../lib/layers'
|
||||||
|
|
||||||
const Y_OFFSET = 0.01
|
const Y_OFFSET = 0.01
|
||||||
const WALL_HEIGHT = 2.3
|
const WALL_HEIGHT = 2.3
|
||||||
@@ -28,7 +29,7 @@ const createWallGradientMaterial = (zoneColor: string) => {
|
|||||||
colorNode: baseColor,
|
colorNode: baseColor,
|
||||||
opacityNode: finalOpacity,
|
opacityNode: finalOpacity,
|
||||||
side: DoubleSide,
|
side: DoubleSide,
|
||||||
depthWrite: false,
|
depthWrite: true,
|
||||||
depthTest: false,
|
depthTest: false,
|
||||||
userData: {
|
userData: {
|
||||||
uOpacity: opacity,
|
uOpacity: opacity,
|
||||||
@@ -239,12 +240,13 @@ export const ZoneRenderer = ({ node }: { node: ZoneNode }) => {
|
|||||||
rotation={[-Math.PI / 2, 0, 0]}
|
rotation={[-Math.PI / 2, 0, 0]}
|
||||||
material={floorMaterial}
|
material={floorMaterial}
|
||||||
name="floor"
|
name="floor"
|
||||||
|
layers={ZONE_LAYER}
|
||||||
>
|
>
|
||||||
<shapeGeometry args={[floorShape]} />
|
<shapeGeometry args={[floorShape]} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* Wall borders with gradient */}
|
{/* Wall borders with gradient */}
|
||||||
<mesh geometry={wallGeometry} material={wallMaterial} name="walls" />
|
<mesh geometry={wallGeometry} material={wallMaterial} name="walls" layers={ZONE_LAYER} />
|
||||||
</group>
|
</group>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useScene } from '@pascal-app/core'
|
import { useScene } from '@pascal-app/core'
|
||||||
|
import polygonClipping from 'polygon-clipping'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
import useViewer from '../../store/use-viewer'
|
import useViewer from '../../store/use-viewer'
|
||||||
import polygonClipping from 'polygon-clipping'
|
|
||||||
|
|
||||||
export const GroundOccluder = () => {
|
export const GroundOccluder = () => {
|
||||||
const theme = useViewer((state) => state.theme)
|
const theme = useViewer((state) => state.theme)
|
||||||
const bgColor = theme === 'dark' ? '#1f2433' : '#fafafa'
|
const bgColor = theme === 'dark' ? '#1f2433' : '#fafafa'
|
||||||
|
|
||||||
const nodes = useScene((state) => state.nodes)
|
const nodes = useScene((state) => state.nodes)
|
||||||
|
|
||||||
const shape = useMemo(() => {
|
const shape = useMemo(() => {
|
||||||
@@ -22,17 +22,17 @@ export const GroundOccluder = () => {
|
|||||||
|
|
||||||
// Collect all polygons for slabs and zones
|
// Collect all polygons for slabs and zones
|
||||||
const polygons: [number, number][][] = []
|
const polygons: [number, number][][] = []
|
||||||
|
|
||||||
Object.values(nodes).forEach((node) => {
|
Object.values(nodes).forEach((node) => {
|
||||||
if ((node.type === 'slab' || node.type === 'zone') && node.polygon && node.polygon.length >= 3) {
|
if (node.type === 'slab' && node.polygon && node.polygon.length >= 3) {
|
||||||
polygons.push(node.polygon as [number, number][])
|
polygons.push(node.polygon as [number, number][])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (polygons.length > 0) {
|
if (polygons.length > 0) {
|
||||||
// Format for polygon-clipping: [[[x, y], [x, y], ...]]
|
// Format for polygon-clipping: [[[x, y], [x, y], ...]]
|
||||||
const multiPolygons = polygons.map(pts => {
|
const multiPolygons = polygons.map((pts) => {
|
||||||
const ring = pts.map(p => [p[0], -p[1]] as [number, number]) // Negate Y (which was Z)
|
const ring = pts.map((p) => [p[0], -p[1]] as [number, number]) // Negate Y (which was Z)
|
||||||
return [ring]
|
return [ring]
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ export const GroundOccluder = () => {
|
|||||||
if (geom.length > 0) {
|
if (geom.length > 0) {
|
||||||
const ring = geom[0]!
|
const ring = geom[0]!
|
||||||
const hole = new THREE.Path()
|
const hole = new THREE.Path()
|
||||||
|
|
||||||
if (ring.length > 0) {
|
if (ring.length > 0) {
|
||||||
hole.moveTo(ring[0]![0], ring[0]![1])
|
hole.moveTo(ring[0]![0], ring[0]![1])
|
||||||
for (let i = 1; i < ring.length; i++) {
|
for (let i = 1; i < ring.length; i++) {
|
||||||
@@ -64,8 +64,8 @@ export const GroundOccluder = () => {
|
|||||||
return (
|
return (
|
||||||
<mesh rotation-x={-Math.PI / 2} position-y={-0.05}>
|
<mesh rotation-x={-Math.PI / 2} position-y={-0.05}>
|
||||||
<shapeGeometry args={[shape]} />
|
<shapeGeometry args={[shape]} />
|
||||||
<meshBasicMaterial
|
<meshBasicMaterial
|
||||||
color={bgColor}
|
color={bgColor}
|
||||||
depthWrite={true}
|
depthWrite={true}
|
||||||
polygonOffset={true}
|
polygonOffset={true}
|
||||||
polygonOffsetFactor={1}
|
polygonOffsetFactor={1}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ const Viewer: React.FC<ViewerProps> = ({ children, selectionManager = 'default'
|
|||||||
}}
|
}}
|
||||||
camera={{ position: [50, 50, 50], fov: 50 }}
|
camera={{ position: [50, 50, 50], fov: 50 }}
|
||||||
>
|
>
|
||||||
<AnimatedBackground isDark={theme === 'dark'} />
|
{/* <AnimatedBackground isDark={theme === 'dark'} /> */}
|
||||||
<GroundOccluder />
|
<GroundOccluder />
|
||||||
<ViewerCamera />
|
<ViewerCamera />
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useFrame, useThree } from '@react-three/fiber'
|
import { useFrame, useThree } from '@react-three/fiber'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Color, UnsignedByteType } from 'three'
|
import { Color, Layers, UnsignedByteType } from 'three'
|
||||||
import { outline } from 'three/addons/tsl/display/OutlineNode.js'
|
import { outline } from 'three/addons/tsl/display/OutlineNode.js'
|
||||||
import { ssgi } from 'three/addons/tsl/display/SSGINode.js'
|
import { ssgi } from 'three/addons/tsl/display/SSGINode.js'
|
||||||
import { traa } from 'three/addons/tsl/display/TRAANode.js'
|
import { traa } from 'three/addons/tsl/display/TRAANode.js'
|
||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
colorToDirection,
|
colorToDirection,
|
||||||
diffuseColor,
|
diffuseColor,
|
||||||
directionToColor,
|
directionToColor,
|
||||||
|
float,
|
||||||
|
mix,
|
||||||
mrt,
|
mrt,
|
||||||
normalView,
|
normalView,
|
||||||
oscSine,
|
oscSine,
|
||||||
@@ -23,6 +25,7 @@ import {
|
|||||||
|
|
||||||
import { RenderPipeline, type WebGPURenderer } from 'three/webgpu'
|
import { RenderPipeline, type WebGPURenderer } from 'three/webgpu'
|
||||||
import useViewer from '../../store/use-viewer'
|
import useViewer from '../../store/use-viewer'
|
||||||
|
import { SCENE_LAYER, ZONE_LAYER } from '../../lib/layers'
|
||||||
|
|
||||||
// SSGI Parameters - adjust these to fine-tune global illumination and ambient occlusion
|
// SSGI Parameters - adjust these to fine-tune global illumination and ambient occlusion
|
||||||
export const SSGI_PARAMS = {
|
export const SSGI_PARAMS = {
|
||||||
@@ -40,12 +43,29 @@ export const SSGI_PARAMS = {
|
|||||||
useTemporalFiltering: true,
|
useTemporalFiltering: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DARK_BG = '#1f2433'
|
||||||
|
const LIGHT_BG = '#ffffff'
|
||||||
|
|
||||||
const PostProcessingPasses = () => {
|
const PostProcessingPasses = () => {
|
||||||
const { gl: renderer, scene, camera } = useThree()
|
const { gl: renderer, scene, camera } = useThree()
|
||||||
const renderPipelineRef = useRef<RenderPipeline | null>(null)
|
const renderPipelineRef = useRef<RenderPipeline | null>(null)
|
||||||
const hasPipelineErrorRef = useRef(false)
|
const hasPipelineErrorRef = useRef(false)
|
||||||
const [isInitialized, setIsInitialized] = useState(false)
|
const [isInitialized, setIsInitialized] = useState(false)
|
||||||
|
|
||||||
|
// Background color uniform — updated every frame via lerp, read by the TSL pipeline.
|
||||||
|
// Initialised from the current theme so there's no flash on first render.
|
||||||
|
const initBg = useViewer.getState().theme === 'dark' ? DARK_BG : LIGHT_BG
|
||||||
|
const bgUniform = useRef(uniform(new Color(initBg)))
|
||||||
|
const bgCurrent = useRef(new Color(initBg))
|
||||||
|
const bgTarget = useRef(new Color())
|
||||||
|
|
||||||
|
const zoneLayers = useMemo(() => {
|
||||||
|
const l = new Layers()
|
||||||
|
l.enable(ZONE_LAYER)
|
||||||
|
l.disable(SCENE_LAYER)
|
||||||
|
return l
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let mounted = true
|
let mounted = true
|
||||||
|
|
||||||
@@ -111,6 +131,8 @@ const PostProcessingPasses = () => {
|
|||||||
return colorToDirection(scenePassNormal.sample(uv))
|
return colorToDirection(scenePassNormal.sample(uv))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const zonePass = pass(scene, camera)
|
||||||
|
zonePass.setLayers(zoneLayers)
|
||||||
// SSGI Pass (cast to PerspectiveCamera for SSGI)
|
// SSGI Pass (cast to PerspectiveCamera for SSGI)
|
||||||
const giPass = ssgi(scenePassColor, scenePassDepth, sceneNormal, camera as any)
|
const giPass = ssgi(scenePassColor, scenePassDepth, sceneNormal, camera as any)
|
||||||
|
|
||||||
@@ -130,10 +152,17 @@ const PostProcessingPasses = () => {
|
|||||||
const gi = giPass.rgb
|
const gi = giPass.rgb
|
||||||
const ao = giPass.a
|
const ao = giPass.a
|
||||||
|
|
||||||
|
// Background detection via alpha: renderer clears with alpha=0 (setClearAlpha(0) in useFrame),
|
||||||
|
// so background pixels have scenePassColor.a=0 while geometry pixels have output.a=1.
|
||||||
|
// WebGPU only applies clearColorValue to MRT attachment 0 (output), so scenePassColor.a
|
||||||
|
// is the reliable geometry mask — no normals, no flicker.
|
||||||
|
const hasGeometry = scenePassColor.a
|
||||||
|
const contentAlpha = hasGeometry.max(zonePass.a)
|
||||||
|
|
||||||
// Composite: scene * AO + diffuse * GI
|
// Composite: scene * AO + diffuse * GI
|
||||||
const compositePass = vec4(
|
const compositePass = vec4(
|
||||||
add(scenePassColor.rgb.mul(ao), scenePassDiffuse.rgb.mul(gi)),
|
add(scenePassColor.rgb.mul(ao), add(zonePass.rgb, scenePassDiffuse.rgb.mul(gi))),
|
||||||
scenePassColor.a,
|
contentAlpha,
|
||||||
)
|
)
|
||||||
|
|
||||||
function generateSelectedOutlinePass() {
|
function generateSelectedOutlinePass() {
|
||||||
@@ -194,7 +223,17 @@ const PostProcessingPasses = () => {
|
|||||||
: vec4(add(scenePassColor.rgb, selectedOutlinePass.add(hoverOutlinePass)), scenePassColor.a)
|
: vec4(add(scenePassColor.rgb, selectedOutlinePass.add(hoverOutlinePass)), scenePassColor.a)
|
||||||
|
|
||||||
// TRAA (Temporal Reprojection Anti-Aliasing) - applied AFTER combining everything
|
// TRAA (Temporal Reprojection Anti-Aliasing) - applied AFTER combining everything
|
||||||
const finalOutput = traa(compositeWithOutlines, scenePassDepth, scenePassVelocity, camera)
|
const traaOutput = traa(compositeWithOutlines, scenePassDepth, scenePassVelocity, camera)
|
||||||
|
|
||||||
|
// For zone-over-background pixels, scenePassDepth=1.0 (no scene geometry) causes TRAA
|
||||||
|
// to output black. Use hasGeometry to blend: geometry pixels use traaRgb, all others
|
||||||
|
// (zones over background, pure background) use compositePass.rgb directly.
|
||||||
|
const traaRgb = (traaOutput as any).rgb
|
||||||
|
const colorSource = mix(compositePass.rgb, traaRgb, hasGeometry)
|
||||||
|
const finalOutput = vec4(
|
||||||
|
mix(bgUniform.current, colorSource, contentAlpha),
|
||||||
|
float(1),
|
||||||
|
)
|
||||||
|
|
||||||
const renderPipeline = new RenderPipeline(renderer as unknown as WebGPURenderer)
|
const renderPipeline = new RenderPipeline(renderer as unknown as WebGPURenderer)
|
||||||
renderPipeline.outputNode = finalOutput
|
renderPipeline.outputNode = finalOutput
|
||||||
@@ -217,14 +256,22 @@ const PostProcessingPasses = () => {
|
|||||||
}
|
}
|
||||||
renderPipelineRef.current = null
|
renderPipelineRef.current = null
|
||||||
}
|
}
|
||||||
}, [renderer, scene, camera, isInitialized])
|
}, [renderer, scene, camera, isInitialized, zoneLayers])
|
||||||
|
|
||||||
|
useFrame((_, delta) => {
|
||||||
|
// Animate background colour toward the current theme target (same lerp as AnimatedBackground)
|
||||||
|
bgTarget.current.set(useViewer.getState().theme === 'dark' ? DARK_BG : LIGHT_BG)
|
||||||
|
bgCurrent.current.lerp(bgTarget.current, Math.min(delta, 0.1) * 4)
|
||||||
|
bgUniform.current.value.copy(bgCurrent.current)
|
||||||
|
|
||||||
useFrame(() => {
|
|
||||||
if (hasPipelineErrorRef.current || !renderPipelineRef.current) {
|
if (hasPipelineErrorRef.current || !renderPipelineRef.current) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Clear alpha=0 so background pixels in the output MRT attachment (index 0) get a=0,
|
||||||
|
// making scenePassColor.a a reliable geometry mask (geometry pixels write a=1 via output node).
|
||||||
|
;(renderer as any).setClearAlpha(0)
|
||||||
renderPipelineRef.current.render()
|
renderPipelineRef.current.render()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
hasPipelineErrorRef.current = true
|
hasPipelineErrorRef.current = true
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export { default as Viewer } from './components/viewer'
|
export { default as Viewer } from './components/viewer'
|
||||||
export { default as useViewer } from './store/use-viewer'
|
export { default as useViewer } from './store/use-viewer'
|
||||||
export { ASSETS_CDN_URL, resolveAssetUrl, resolveCdnUrl } from './lib/asset-url'
|
export { ASSETS_CDN_URL, resolveAssetUrl, resolveCdnUrl } from './lib/asset-url'
|
||||||
|
export { SCENE_LAYER, ZONE_LAYER } from './lib/layers'
|
||||||
export { InteractiveSystem } from './systems/interactive/interactive-system'
|
export { InteractiveSystem } from './systems/interactive/interactive-system'
|
||||||
export { snapLevelsToTruePositions } from './systems/level/level-utils'
|
export { snapLevelsToTruePositions } from './systems/level/level-utils'
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/** Default Three.js layer for main scene geometry. */
|
||||||
|
export const SCENE_LAYER = 0
|
||||||
|
|
||||||
|
/** Layer used for zone rendering (floor fills and wall borders). */
|
||||||
|
export const ZONE_LAYER = 2
|
||||||
Reference in New Issue
Block a user