lecodes-sdk 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -0
- package/dist/global.d.ts +503 -0
- package/dist/host.d.ts +61 -0
- package/dist/inject.js +4730 -0
- package/dist/types/animate/animate.d.ts +20 -0
- package/dist/types/animate/bezier.d.ts +2 -0
- package/dist/types/animate/easings.d.ts +21 -0
- package/dist/types/canvas/Canvas.d.ts +168 -0
- package/dist/types/core/Aspect.d.ts +171 -0
- package/dist/types/core/InspectorUI.d.ts +88 -0
- package/dist/types/core/StateMachine.d.ts +81 -0
- package/dist/types/core/color.d.ts +13 -0
- package/dist/types/core/compWrite.d.ts +11 -0
- package/dist/types/core/events.d.ts +11 -0
- package/dist/types/core/fields.d.ts +58 -0
- package/dist/types/core/registry.d.ts +7 -0
- package/dist/types/core/signals.d.ts +29 -0
- package/dist/types/core/time.d.ts +31 -0
- package/dist/types/g2/Camera2D.d.ts +19 -0
- package/dist/types/g2/CharacterController2D.d.ts +134 -0
- package/dist/types/g2/Node2D.d.ts +91 -0
- package/dist/types/g2/OneWay2D.d.ts +24 -0
- package/dist/types/g2/Physics2D.d.ts +147 -0
- package/dist/types/g2/Scene2D.d.ts +72 -0
- package/dist/types/g2/Shape2D.d.ts +58 -0
- package/dist/types/g2/Sprite.d.ts +51 -0
- package/dist/types/g2/SpriteAnimation.d.ts +50 -0
- package/dist/types/g2/SpriteSheet.d.ts +69 -0
- package/dist/types/g2/Texture2D.d.ts +16 -0
- package/dist/types/g2/Tilemap.d.ts +20 -0
- package/dist/types/g2/Tileset.d.ts +32 -0
- package/dist/types/g2/Trigger2D.d.ts +20 -0
- package/dist/types/g2/autotile.d.ts +133 -0
- package/dist/types/g2/cells.d.ts +14 -0
- package/dist/types/g2/defineScene2d.d.ts +141 -0
- package/dist/types/g2/groups2d.d.ts +16 -0
- package/dist/types/g2/loop.d.ts +4 -0
- package/dist/types/g2/scenarios2d.d.ts +30 -0
- package/dist/types/g2/touch.d.ts +2 -0
- package/dist/types/gl/Camera.d.ts +79 -0
- package/dist/types/gl/CameraPlace.d.ts +26 -0
- package/dist/types/gl/CharacterController.d.ts +98 -0
- package/dist/types/gl/Gearbox.d.ts +86 -0
- package/dist/types/gl/Geometry.d.ts +39 -0
- package/dist/types/gl/IK.d.ts +53 -0
- package/dist/types/gl/InstancedMesh.d.ts +46 -0
- package/dist/types/gl/Light.d.ts +73 -0
- package/dist/types/gl/Lightmap.d.ts +85 -0
- package/dist/types/gl/Locomotion.d.ts +279 -0
- package/dist/types/gl/Material.d.ts +100 -0
- package/dist/types/gl/Mesh.d.ts +33 -0
- package/dist/types/gl/Model.d.ts +53 -0
- package/dist/types/gl/NavAgent.d.ts +99 -0
- package/dist/types/gl/NavMesh.d.ts +169 -0
- package/dist/types/gl/Node.d.ts +99 -0
- package/dist/types/gl/Noise.d.ts +20 -0
- package/dist/types/gl/Particles.d.ts +277 -0
- package/dist/types/gl/Physics.d.ts +91 -0
- package/dist/types/gl/Plane.d.ts +19 -0
- package/dist/types/gl/Ragdoll.d.ts +86 -0
- package/dist/types/gl/Ray.d.ts +8 -0
- package/dist/types/gl/Scene.d.ts +221 -0
- package/dist/types/gl/Shape.d.ts +122 -0
- package/dist/types/gl/Terrain.d.ts +280 -0
- package/dist/types/gl/Texture.d.ts +27 -0
- package/dist/types/gl/Trigger.d.ts +10 -0
- package/dist/types/gl/Vehicle.d.ts +191 -0
- package/dist/types/gl/Wheel.d.ts +95 -0
- package/dist/types/gl/animation/AnimationClip.d.ts +60 -0
- package/dist/types/gl/animation/Animator.d.ts +219 -0
- package/dist/types/gl/animation/Layer.d.ts +31 -0
- package/dist/types/gl/animation/Loop.d.ts +17 -0
- package/dist/types/gl/animation/Playback.d.ts +36 -0
- package/dist/types/gl/animation/core.d.ts +367 -0
- package/dist/types/gl/controls.d.ts +19 -0
- package/dist/types/gl/physicsEvents.d.ts +1 -0
- package/dist/types/gl/scenarios.d.ts +98 -0
- package/dist/types/gl/state.d.ts +5 -0
- package/dist/types/gl/terrainMesh.d.ts +46 -0
- package/dist/types/gl/touch.d.ts +2 -0
- package/dist/types/inject.d.ts +123 -0
- package/dist/types/math/Mathf.d.ts +39 -0
- package/dist/types/math/mat4.d.ts +66 -0
- package/dist/types/math/quat.d.ts +53 -0
- package/dist/types/math/vec.d.ts +129 -0
- package/dist/types/net/codec.d.ts +25 -0
- package/dist/types/net/core.d.ts +106 -0
- package/dist/types/net/index.d.ts +80 -0
- package/dist/types/net/replication.d.ts +101 -0
- package/dist/types/plugins/camera.d.ts +25 -0
- package/dist/types/plugins/geolocation.d.ts +39 -0
- package/dist/types/plugins/oauth.d.ts +25 -0
- package/dist/types/plugins/permission.d.ts +1 -0
- package/dist/types/plugins/push.d.ts +46 -0
- package/dist/types/plugins/qr.d.ts +21 -0
- package/dist/types/plugins/service.d.ts +33 -0
- package/dist/types/runtime/app.d.ts +67 -0
- package/dist/types/runtime/appEvents.d.ts +7 -0
- package/dist/types/runtime/channel.d.ts +1 -0
- package/dist/types/runtime/clipboard.d.ts +7 -0
- package/dist/types/runtime/datetime.d.ts +80 -0
- package/dist/types/runtime/device.d.ts +126 -0
- package/dist/types/runtime/fetch.d.ts +31 -0
- package/dist/types/runtime/files.d.ts +33 -0
- package/dist/types/runtime/input.d.ts +121 -0
- package/dist/types/runtime/media.d.ts +46 -0
- package/dist/types/runtime/misc.d.ts +11 -0
- package/dist/types/runtime/net.d.ts +15 -0
- package/dist/types/runtime/rpc.d.ts +44 -0
- package/dist/types/runtime/service.d.ts +1 -0
- package/dist/types/runtime/share.d.ts +2 -0
- package/dist/types/runtime/storage.d.ts +5 -0
- package/dist/types/runtime/touch.d.ts +56 -0
- package/dist/types/scene/defineScene.d.ts +296 -0
- package/dist/types/scene/editorPlugins.d.ts +82 -0
- package/dist/types/scene/gizmos.d.ts +56 -0
- package/dist/types/scene/grammar.d.ts +56 -0
- package/dist/types/scene/level.d.ts +98 -0
- package/dist/types/scene/material.d.ts +76 -0
- package/dist/types/server/auth/appConfig.d.ts +16 -0
- package/dist/types/server/auth/global.d.ts +56 -0
- package/dist/types/server/auth/models.d.ts +164 -0
- package/dist/types/server/auth/types.d.ts +56 -0
- package/dist/types/server/channel.d.ts +28 -0
- package/dist/types/server/context.d.ts +27 -0
- package/dist/types/server/db/defineDb.d.ts +36 -0
- package/dist/types/server/db/fields.d.ts +146 -0
- package/dist/types/server/db/index.d.ts +6 -0
- package/dist/types/server/db/marci/query.d.ts +326 -0
- package/dist/types/server/db/types.d.ts +163 -0
- package/dist/types/server/errors.d.ts +8 -0
- package/dist/types/server/inject.d.ts +5 -0
- package/dist/types/ui/NativeView.d.ts +38 -0
- package/dist/types/ui/UI.d.ts +27 -0
- package/dist/types/ui/UIBottomSheet.d.ts +71 -0
- package/dist/types/ui/UIButton.d.ts +30 -0
- package/dist/types/ui/UIContainer.d.ts +33 -0
- package/dist/types/ui/UIImage.d.ts +35 -0
- package/dist/types/ui/UIInput.d.ts +110 -0
- package/dist/types/ui/UIModal.d.ts +70 -0
- package/dist/types/ui/UINode.d.ts +363 -0
- package/dist/types/ui/UIPager.d.ts +123 -0
- package/dist/types/ui/UIPopover.d.ts +39 -0
- package/dist/types/ui/UIScreen.d.ts +57 -0
- package/dist/types/ui/UIScrollable.d.ts +40 -0
- package/dist/types/ui/UISpacer.d.ts +9 -0
- package/dist/types/ui/UITabs.d.ts +96 -0
- package/dist/types/ui/UIText.d.ts +19 -0
- package/dist/types/ui/UIVideo.d.ts +28 -0
- package/dist/types/ui/UIVirtualizedList.d.ts +97 -0
- package/dist/types/ui/UIWidget.d.ts +65 -0
- package/dist/types/ui/fonts.d.ts +9 -0
- package/dist/types/ui/presentable.d.ts +68 -0
- package/dist/types/ui/router.d.ts +40 -0
- package/dist/types/ui/theme.d.ts +26 -0
- package/dist/types.json +1 -0
- package/package.json +46 -0
- package/prompts/2d.md +395 -0
- package/prompts/3d-scene-files.md +113 -0
- package/prompts/3d-scene.md +119 -0
- package/prompts/3d.md +303 -0
- package/prompts/README.md +142 -0
- package/prompts/ar.md +109 -0
- package/prompts/canvas.md +27 -0
- package/prompts/compose.ts +46 -0
- package/prompts/concept.md +49 -0
- package/prompts/core-design.md +75 -0
- package/prompts/core.md +264 -0
- package/prompts/design.md +218 -0
- package/prompts/dist/2d-game.md +1530 -0
- package/prompts/dist/3d-app.md +1672 -0
- package/prompts/dist/ar-app.md +1548 -0
- package/prompts/dist/concept.md +49 -0
- package/prompts/dist/design.md +498 -0
- package/prompts/dist/ui-app.md +1134 -0
- package/prompts/index.md +37 -0
- package/prompts/intake-prompt.md +33 -0
- package/prompts/namer-prompt.md +12 -0
- package/prompts/router-prompt.md +50 -0
- package/prompts/select.ts +111 -0
- package/prompts/ui-design.md +203 -0
- package/prompts/ui.md +803 -0
- package/src/animate/animate.ts +238 -0
- package/src/animate/bezier.ts +138 -0
- package/src/animate/easings.ts +126 -0
- package/src/bridges.d.ts +1345 -0
- package/src/canvas/Canvas.ts +305 -0
- package/src/compile/__tests__/assetIconMacro.test.ts +219 -0
- package/src/compile/__tests__/assetMacro.test.ts +74 -0
- package/src/compile/__tests__/assetName.test.ts +55 -0
- package/src/compile/__tests__/compile.test.ts +287 -0
- package/src/compile/__tests__/detectEntry.test.ts +132 -0
- package/src/compile/__tests__/fontMacro.test.ts +199 -0
- package/src/compile/aspectMacro.ts +86 -0
- package/src/compile/assetIconMacro.ts +384 -0
- package/src/compile/assetMacro.ts +146 -0
- package/src/compile/assetName.ts +50 -0
- package/src/compile/bundler.ts +287 -0
- package/src/compile/compileProject.ts +127 -0
- package/src/compile/detectEntry.ts +128 -0
- package/src/compile/fontMacro.ts +459 -0
- package/src/compile/fontRegistry.ts +78 -0
- package/src/compile/header.ts +67 -0
- package/src/compile/index.ts +100 -0
- package/src/compile/libraryImports.ts +52 -0
- package/src/compile/liteMaterial.ts +247 -0
- package/src/compile/sceneEditor.ts +88 -0
- package/src/compile/serverSplit.ts +233 -0
- package/src/compile/serverTypes.ts +227 -0
- package/src/compile/sfnt.ts +98 -0
- package/src/compile/shaderSchema.ts +202 -0
- package/src/compile/shaderTargets.ts +77 -0
- package/src/compile/sourcemap.ts +25 -0
- package/src/core/Aspect.ts +568 -0
- package/src/core/InspectorUI.ts +212 -0
- package/src/core/StateMachine.ts +308 -0
- package/src/core/__tests__/stateMachine.test.ts +132 -0
- package/src/core/color.ts +66 -0
- package/src/core/compWrite.ts +42 -0
- package/src/core/events.ts +38 -0
- package/src/core/fields.ts +120 -0
- package/src/core/registry.ts +23 -0
- package/src/core/signals.ts +277 -0
- package/src/core/time.ts +84 -0
- package/src/g2/Camera2D.ts +40 -0
- package/src/g2/CharacterController2D.ts +276 -0
- package/src/g2/Node2D.ts +267 -0
- package/src/g2/OneWay2D.ts +66 -0
- package/src/g2/Physics2D.ts +346 -0
- package/src/g2/Scene2D.ts +209 -0
- package/src/g2/Shape2D.ts +259 -0
- package/src/g2/Sprite.ts +89 -0
- package/src/g2/SpriteAnimation.ts +171 -0
- package/src/g2/SpriteSheet.ts +166 -0
- package/src/g2/Texture2D.ts +47 -0
- package/src/g2/Tilemap.ts +41 -0
- package/src/g2/Tileset.ts +71 -0
- package/src/g2/Trigger2D.ts +77 -0
- package/src/g2/autotile.ts +433 -0
- package/src/g2/cells.ts +91 -0
- package/src/g2/defineScene2d.ts +381 -0
- package/src/g2/groups2d.ts +106 -0
- package/src/g2/loop.ts +50 -0
- package/src/g2/scenarios2d.ts +69 -0
- package/src/g2/touch.ts +83 -0
- package/src/gl/Camera.ts +160 -0
- package/src/gl/CameraPlace.ts +52 -0
- package/src/gl/CharacterController.ts +238 -0
- package/src/gl/Gearbox.ts +212 -0
- package/src/gl/Geometry.ts +279 -0
- package/src/gl/IK.ts +193 -0
- package/src/gl/InstancedMesh.ts +132 -0
- package/src/gl/Light.ts +135 -0
- package/src/gl/Lightmap.ts +402 -0
- package/src/gl/Locomotion.ts +498 -0
- package/src/gl/Material.ts +279 -0
- package/src/gl/Mesh.ts +83 -0
- package/src/gl/Model.ts +124 -0
- package/src/gl/NavAgent.ts +337 -0
- package/src/gl/NavMesh.ts +397 -0
- package/src/gl/Node.ts +350 -0
- package/src/gl/Noise.ts +30 -0
- package/src/gl/Particles.ts +676 -0
- package/src/gl/Physics.ts +222 -0
- package/src/gl/Plane.ts +53 -0
- package/src/gl/Ragdoll.ts +270 -0
- package/src/gl/Ray.ts +16 -0
- package/src/gl/Scene.ts +505 -0
- package/src/gl/Shape.ts +409 -0
- package/src/gl/Terrain.ts +1092 -0
- package/src/gl/Texture.ts +63 -0
- package/src/gl/Trigger.ts +45 -0
- package/src/gl/Vehicle.ts +473 -0
- package/src/gl/Wheel.ts +240 -0
- package/src/gl/animation/AnimationClip.ts +204 -0
- package/src/gl/animation/Animator.ts +329 -0
- package/src/gl/animation/Layer.ts +39 -0
- package/src/gl/animation/Loop.ts +31 -0
- package/src/gl/animation/Playback.ts +52 -0
- package/src/gl/animation/core.ts +670 -0
- package/src/gl/controls.ts +95 -0
- package/src/gl/physicsEvents.ts +20 -0
- package/src/gl/scenarios.ts +291 -0
- package/src/gl/state.ts +6 -0
- package/src/gl/terrainMesh.ts +219 -0
- package/src/gl/touch.ts +68 -0
- package/src/host.d.ts +61 -0
- package/src/inject.ts +203 -0
- package/src/math/Mathf.ts +118 -0
- package/src/math/mat4.ts +278 -0
- package/src/math/quat.ts +232 -0
- package/src/math/vec.ts +255 -0
- package/src/net/codec.ts +119 -0
- package/src/net/core.ts +384 -0
- package/src/net/index.ts +181 -0
- package/src/net/replication.ts +622 -0
- package/src/plugins/camera.ts +81 -0
- package/src/plugins/geolocation.ts +123 -0
- package/src/plugins/oauth.ts +61 -0
- package/src/plugins/permission.ts +7 -0
- package/src/plugins/push.ts +132 -0
- package/src/plugins/qr.ts +73 -0
- package/src/plugins/service.ts +47 -0
- package/src/runtime/app.ts +101 -0
- package/src/runtime/appEvents.ts +54 -0
- package/src/runtime/channel.ts +61 -0
- package/src/runtime/clipboard.ts +20 -0
- package/src/runtime/datetime.ts +329 -0
- package/src/runtime/device.ts +293 -0
- package/src/runtime/fetch.ts +77 -0
- package/src/runtime/files.ts +108 -0
- package/src/runtime/input.ts +175 -0
- package/src/runtime/media.ts +111 -0
- package/src/runtime/misc.ts +16 -0
- package/src/runtime/net.ts +36 -0
- package/src/runtime/rpc.ts +218 -0
- package/src/runtime/service.ts +83 -0
- package/src/runtime/share.ts +9 -0
- package/src/runtime/storage.ts +13 -0
- package/src/runtime/touch.ts +76 -0
- package/src/scene/defineScene.ts +1396 -0
- package/src/scene/editorPlugins.ts +110 -0
- package/src/scene/gizmos.ts +148 -0
- package/src/scene/grammar.ts +120 -0
- package/src/scene/level.ts +296 -0
- package/src/scene/material.ts +188 -0
- package/src/server/auth/appConfig.ts +12 -0
- package/src/server/auth/global.ts +80 -0
- package/src/server/auth/host.ts +318 -0
- package/src/server/auth/models.ts +83 -0
- package/src/server/auth/types.ts +50 -0
- package/src/server/channel.ts +56 -0
- package/src/server/context.ts +36 -0
- package/src/server/db/defineDb.ts +237 -0
- package/src/server/db/fields.ts +132 -0
- package/src/server/db/httpTransport.ts +93 -0
- package/src/server/db/index.ts +7 -0
- package/src/server/db/marci/query.ts +412 -0
- package/src/server/db/types.ts +202 -0
- package/src/server/errors.ts +12 -0
- package/src/server/host.ts +74 -0
- package/src/server/inject.ts +13 -0
- package/src/server/runtime.ts +133 -0
- package/src/server/validate.ts +87 -0
- package/src/ui/NativeView.ts +142 -0
- package/src/ui/UI.ts +39 -0
- package/src/ui/UIBottomSheet.ts +139 -0
- package/src/ui/UIButton.ts +101 -0
- package/src/ui/UIContainer.ts +60 -0
- package/src/ui/UIImage.ts +83 -0
- package/src/ui/UIInput.ts +185 -0
- package/src/ui/UIModal.ts +139 -0
- package/src/ui/UINode.ts +830 -0
- package/src/ui/UIPager.ts +362 -0
- package/src/ui/UIPopover.ts +100 -0
- package/src/ui/UIScreen.ts +123 -0
- package/src/ui/UIScrollable.ts +87 -0
- package/src/ui/UISpacer.ts +14 -0
- package/src/ui/UITabs.ts +236 -0
- package/src/ui/UIText.ts +51 -0
- package/src/ui/UIVideo.ts +88 -0
- package/src/ui/UIVirtualizedList.ts +241 -0
- package/src/ui/UIWidget.ts +127 -0
- package/src/ui/fonts.ts +13 -0
- package/src/ui/presentable.ts +117 -0
- package/src/ui/router.ts +132 -0
- package/src/ui/theme.ts +84 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// AR object-placement controls — the classic "drop a model in the room and manipulate it" gesture
|
|
2
|
+
// recipe: ONE finger drags the target across the ground plane (camera-relative), TWO fingers pinch to
|
|
3
|
+
// scale and twist to rotate (yaw). Lives on ARScene (see Scene.ts) because it orchestrates the scene's
|
|
4
|
+
// camera + touch input against a target node; it's built entirely on the public API (no native calls
|
|
5
|
+
// the user couldn't make), so it's a convenience, not a primitive.
|
|
6
|
+
|
|
7
|
+
import { Vec2, Vec3 } from "../math/vec"
|
|
8
|
+
import { Quat } from "../math/quat"
|
|
9
|
+
import type { Node } from "./Node"
|
|
10
|
+
import type { Scene } from "./Scene"
|
|
11
|
+
import type { TouchStartEvent } from "../runtime/touch"
|
|
12
|
+
|
|
13
|
+
export type ControlsOptions = {
|
|
14
|
+
/** Smallest uniform scale the pinch gesture allows (default 0.08). */
|
|
15
|
+
minScale?: number
|
|
16
|
+
/** Largest uniform scale the pinch gesture allows (default 2). */
|
|
17
|
+
maxScale?: number
|
|
18
|
+
/** One-finger drag moves the target across the ground plane (default true). */
|
|
19
|
+
pan?: boolean
|
|
20
|
+
/** Two-finger pinch scales the target (default true). */
|
|
21
|
+
pinch?: boolean
|
|
22
|
+
/** Two-finger twist rotates the target around its Y axis (default true). */
|
|
23
|
+
twist?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Returned by `addControls` — call `remove()` to detach the gesture listeners. */
|
|
27
|
+
export type ControlsHandle = { remove(): void }
|
|
28
|
+
|
|
29
|
+
type ActiveTouch = { pointerId: number, x: number, y: number }
|
|
30
|
+
|
|
31
|
+
const clamp = (v: number, lo: number, hi: number): number => (v < lo ? lo : v > hi ? hi : v)
|
|
32
|
+
|
|
33
|
+
export const attachControls = (scene: Scene, target: Node, options: ControlsOptions = {}): ControlsHandle => {
|
|
34
|
+
const minScale = options.minScale ?? 0.08
|
|
35
|
+
const maxScale = options.maxScale ?? 2
|
|
36
|
+
const pan = options.pan ?? true
|
|
37
|
+
const pinch = options.pinch ?? true
|
|
38
|
+
const twist = options.twist ?? true
|
|
39
|
+
|
|
40
|
+
const active: ActiveTouch[] = []
|
|
41
|
+
|
|
42
|
+
const onTouchStart = (e: TouchStartEvent<Node | null>): void => {
|
|
43
|
+
const touch: ActiveTouch = { pointerId: e.pointerId, x: e.clientX, y: e.clientY }
|
|
44
|
+
active.push(touch)
|
|
45
|
+
e.track({
|
|
46
|
+
onMove(m) {
|
|
47
|
+
if (active.length === 2) {
|
|
48
|
+
// --- two-finger: pinch-scale + twist-rotate about the gesture axis ---
|
|
49
|
+
const other = active.find((t) => t !== touch)
|
|
50
|
+
if (other) {
|
|
51
|
+
const axis = new Vec2(other.x - touch.x, other.y - touch.y)
|
|
52
|
+
const span = axis.length()
|
|
53
|
+
if (span > 1e-3) {
|
|
54
|
+
const dir = axis.scale(1 / span)
|
|
55
|
+
const delta = new Vec2(m.deltaX, m.deltaY)
|
|
56
|
+
if (pinch) {
|
|
57
|
+
const along = -dir.dot(delta) // movement along the pinch axis
|
|
58
|
+
target.scale = clamp(target.scale.x * (span + along) / span, minScale * 0.4, maxScale * 1.5)
|
|
59
|
+
}
|
|
60
|
+
if (twist) {
|
|
61
|
+
const tangent = dir.perp().dot(delta) // movement across the axis
|
|
62
|
+
target.quaternion = target.quaternion.mul(Quat.fromAxisAngle(Vec3.up, Math.atan2(tangent, span)))
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} else if (active.length === 1 && pan) {
|
|
67
|
+
// --- one-finger: drag across the ground plane, relative to where the camera looks ---
|
|
68
|
+
let dir = scene.camera.forward
|
|
69
|
+
const parent = target.parent
|
|
70
|
+
if (parent) dir = dir.rotate(parent.quaternion.invert()) // into the parent's local frame
|
|
71
|
+
dir = dir.withY(0)
|
|
72
|
+
const k = Math.hypot(dir.x, dir.z)
|
|
73
|
+
const screen = Math.max(...scene.camera.displaySize)
|
|
74
|
+
if (k > 1e-4 && screen > 0) {
|
|
75
|
+
const dist = target.worldPosition.distanceTo(scene.camera.worldPosition)
|
|
76
|
+
const right = new Vec3(-dir.z, 0, dir.x).scale(m.deltaX / k / screen * dist)
|
|
77
|
+
const fwd = dir.scale(-m.deltaY / k / screen * dist)
|
|
78
|
+
const move = right.add(fwd)
|
|
79
|
+
const p = target.position
|
|
80
|
+
target.position = new Vec3(p.x + move.x, p.y, p.z + move.z) // keep height fixed
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
touch.x = m.clientX
|
|
84
|
+
touch.y = m.clientY
|
|
85
|
+
},
|
|
86
|
+
onEnd() {
|
|
87
|
+
const i = active.indexOf(touch)
|
|
88
|
+
if (i >= 0) active.splice(i, 1)
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
scene.addEventListener("touchstart", onTouchStart)
|
|
94
|
+
return { remove() { scene.removeEventListener("touchstart", onTouchStart); active.length = 0 } }
|
|
95
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Routes native physics contact/sensor events to the owning nodes' 'enter'/'exit' events. Each event
|
|
2
|
+
// is delivered to BOTH nodes with the other as the argument. type: 0 = enter (contact/overlap began),
|
|
3
|
+
// 1 = exit (ended). Registered lazily on the first trigger/physics body or 'enter'/'exit' listener.
|
|
4
|
+
|
|
5
|
+
import { nodeRegistry } from "./Node"
|
|
6
|
+
|
|
7
|
+
let registered = false
|
|
8
|
+
export const ensurePhysicsEvents = (): void => {
|
|
9
|
+
if (registered) return
|
|
10
|
+
if (!_creator.setOnPhysicsEvent) return // physics-less build / bridge without the hook
|
|
11
|
+
registered = true
|
|
12
|
+
_creator.setOnPhysicsEvent((a, b, type) => {
|
|
13
|
+
const na = nodeRegistry.get(a)
|
|
14
|
+
const nb = nodeRegistry.get(b)
|
|
15
|
+
if (!na || !nb) return
|
|
16
|
+
const channel = type === 1 ? "exit" : "enter"
|
|
17
|
+
na._emitCollision(channel, nb)
|
|
18
|
+
nb._emitCollision(channel, na)
|
|
19
|
+
})
|
|
20
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// No-code scenario aspects: ready-made behaviors a scene builder attaches in the editor without
|
|
2
|
+
// writing any code — move to a target, travel a waypoint path, spin, face something, start a GLB
|
|
3
|
+
// animation. They are ordinary aspects (work from hand-written code too); in the scene editor the
|
|
4
|
+
// movement ones double as `static editor` generators that draw their paths as editor gizmos
|
|
5
|
+
// (`Gizmos.*` in rebuild() — overlay lines, never scene content; play mode never calls rebuild(),
|
|
6
|
+
// so they cost nothing at runtime).
|
|
7
|
+
//
|
|
8
|
+
// Conventions shared by the family:
|
|
9
|
+
// • targets/waypoints are scene nodes — place an Empty, then pick it in the inspector (`ref()`);
|
|
10
|
+
// • times are seconds, angles degrees; all motion runs in `update(dt)` (play mode only);
|
|
11
|
+
// • orientation is quaternion-composed — never euler-incremented (see math/quat.ts).
|
|
12
|
+
|
|
13
|
+
import { Aspect } from "../core/Aspect"
|
|
14
|
+
import type { FieldMeta } from "../core/fields"
|
|
15
|
+
import type { InspectorUI } from "../core/InspectorUI"
|
|
16
|
+
import { Vec3 } from "../math/vec"
|
|
17
|
+
import { Quat } from "../math/quat"
|
|
18
|
+
import { Node } from "./Node"
|
|
19
|
+
import { Model } from "./Model"
|
|
20
|
+
import type { Playback } from "./animation/Playback"
|
|
21
|
+
import { Gizmos } from "../scene/gizmos"
|
|
22
|
+
|
|
23
|
+
const MOVE_LINE = "#5b8ef0"
|
|
24
|
+
const LOOK_LINE = "#d9a13f"
|
|
25
|
+
const DEG2RAD = Math.PI / 180
|
|
26
|
+
|
|
27
|
+
type LoopMode = "once" | "loop" | "pingpong"
|
|
28
|
+
|
|
29
|
+
/** Normalized phase for elapsed time `t` over one trip of `duration`: once clamps at 1, loop
|
|
30
|
+
* wraps, pingpong triangles 0→1→0. */
|
|
31
|
+
const phase = (t: number, duration: number, mode: LoopMode): number => {
|
|
32
|
+
if (duration <= 0) return 1
|
|
33
|
+
const u = t / duration
|
|
34
|
+
if (mode === "once") return Math.min(u, 1)
|
|
35
|
+
if (mode === "loop") return u - Math.floor(u)
|
|
36
|
+
const v = u % 2
|
|
37
|
+
return v <= 1 ? v : 2 - v
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ease = (k: number, easing: "smooth" | "linear"): number =>
|
|
41
|
+
easing === "smooth" ? k * k * (3 - 2 * k) : k
|
|
42
|
+
|
|
43
|
+
/** A world point in `node`'s PARENT space — what `node.position` is expressed in. */
|
|
44
|
+
const toParentLocal = (node: Node, world: Vec3): Vec3 => {
|
|
45
|
+
const parent = node.parent
|
|
46
|
+
return parent ? parent.worldMatrix.invert().transformPoint(world) : world
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A path node's waypoints: its children in scene-FILE order (`_sceneChildOrder`, stamped by
|
|
50
|
+
* defineScene — the engine's live child order is insertion-based and may differ), editor helper
|
|
51
|
+
* nodes (`__`-prefixed) excluded. Runtime-added children append in engine order. */
|
|
52
|
+
const pathWaypoints = (path: Node): Node[] => {
|
|
53
|
+
const children = path.children.filter((c) => !(c.name ?? "").startsWith("__"))
|
|
54
|
+
const order = (path as { _sceneChildOrder?: string[] })._sceneChildOrder
|
|
55
|
+
if (!order) return children
|
|
56
|
+
const byName = new Map(children.map((c) => [ c.name, c ]))
|
|
57
|
+
const sorted: Node[] = []
|
|
58
|
+
for (const n of order) { const c = byName.get(n); if (c) sorted.push(c) }
|
|
59
|
+
const seen = new Set(sorted)
|
|
60
|
+
for (const c of children) if (!seen.has(c)) sorted.push(c)
|
|
61
|
+
return sorted
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---- the aspects ---------------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
/** Travel from the node's starting position to a target node in `duration` seconds. */
|
|
67
|
+
export class MoveTo extends Aspect<"moveTo"> {
|
|
68
|
+
/** Where to travel — place an Empty node and pick it. */
|
|
69
|
+
target: Node | null = null
|
|
70
|
+
|
|
71
|
+
/** Seconds for the full trip. */
|
|
72
|
+
duration = 2
|
|
73
|
+
|
|
74
|
+
/** Seconds to wait before starting. */
|
|
75
|
+
delay = 0
|
|
76
|
+
|
|
77
|
+
/** once = stop at the target · loop = restart from the start point · pingpong = back and forth. */
|
|
78
|
+
mode: LoopMode = "once"
|
|
79
|
+
|
|
80
|
+
/** Motion curve. */
|
|
81
|
+
easing: "smooth" | "linear" = "smooth"
|
|
82
|
+
|
|
83
|
+
static readonly aspect = "moveTo"
|
|
84
|
+
static editor = { rebuild: true }
|
|
85
|
+
static fields: FieldMeta<MoveTo> = {
|
|
86
|
+
target: { editor: "node" },
|
|
87
|
+
duration: { min: 0.05, max: 120, step: 0.05 },
|
|
88
|
+
delay: { min: 0, max: 120, step: 0.05 },
|
|
89
|
+
mode: { options: [ "once", "loop", "pingpong" ] },
|
|
90
|
+
easing: { options: [ "smooth", "linear" ] },
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private _t = 0
|
|
94
|
+
private _start: Vec3 | null = null
|
|
95
|
+
|
|
96
|
+
update(dt: number): void {
|
|
97
|
+
if (!this.target) return
|
|
98
|
+
this._start ??= this.node.position
|
|
99
|
+
this._t += dt
|
|
100
|
+
const k = ease(phase(Math.max(0, this._t - this.delay), this.duration, this.mode), this.easing)
|
|
101
|
+
const end = toParentLocal(this.node, this.target.worldPosition)
|
|
102
|
+
this.node.position = new Vec3(this._start).add(end.sub(this._start).scale(k))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Editor-only path line (edit mode; play mode never calls this). */
|
|
106
|
+
rebuild(): void {
|
|
107
|
+
if (!this.target) return
|
|
108
|
+
// the target is an empty — its own anchored marker shows the endpoint
|
|
109
|
+
Gizmos.line(this.node.worldPosition, this.target.worldPosition, { color: MOVE_LINE })
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Travel through the children of a path node (add an Empty per waypoint) in `duration` seconds.
|
|
114
|
+
* `loop` runs the circuit closed (last → first); `pingpong` goes back and forth along the open
|
|
115
|
+
* path; `once` stops at the last waypoint. */
|
|
116
|
+
export class FollowPath extends Aspect<"followPath"> {
|
|
117
|
+
/** A node whose CHILDREN are the waypoints, in file order. */
|
|
118
|
+
path: Node | null = null
|
|
119
|
+
|
|
120
|
+
/** Seconds for one full pass of the path. */
|
|
121
|
+
duration = 6
|
|
122
|
+
|
|
123
|
+
/** Seconds to wait before starting. */
|
|
124
|
+
delay = 0
|
|
125
|
+
|
|
126
|
+
mode: LoopMode = "loop"
|
|
127
|
+
|
|
128
|
+
/** Turn to face the direction of travel. */
|
|
129
|
+
orient = true
|
|
130
|
+
|
|
131
|
+
/** Which local axis leads when orienting. */
|
|
132
|
+
forward: "-z" | "z" | "x" | "-x" = "-z"
|
|
133
|
+
|
|
134
|
+
static readonly aspect = "followPath"
|
|
135
|
+
static editor = { rebuild: true }
|
|
136
|
+
static fields: FieldMeta<FollowPath> = {
|
|
137
|
+
path: { editor: "node" },
|
|
138
|
+
duration: { min: 0.05, max: 300, step: 0.05 },
|
|
139
|
+
delay: { min: 0, max: 120, step: 0.05 },
|
|
140
|
+
mode: { options: [ "loop", "once", "pingpong" ] },
|
|
141
|
+
forward: { options: [ "-z", "z", "x", "-x" ] },
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private _t = 0
|
|
145
|
+
|
|
146
|
+
private _points(): Vec3[] {
|
|
147
|
+
if (!this.path) return []
|
|
148
|
+
const points = pathWaypoints(this.path).map((c) => c.worldPosition)
|
|
149
|
+
if (this.mode === "loop" && points.length > 1) points.push(points[0]!.clone())
|
|
150
|
+
return points
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
update(dt: number): void {
|
|
154
|
+
const points = this._points()
|
|
155
|
+
if (points.length < 2) return
|
|
156
|
+
this._t += dt
|
|
157
|
+
const k = phase(Math.max(0, this._t - this.delay), this.duration, this.mode === "loop" ? "loop" : this.mode)
|
|
158
|
+
|
|
159
|
+
const lengths: number[] = []
|
|
160
|
+
let total = 0
|
|
161
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
162
|
+
const l = points[i + 1]!.sub(points[i]!).length()
|
|
163
|
+
lengths.push(l)
|
|
164
|
+
total += l
|
|
165
|
+
}
|
|
166
|
+
if (total <= 0) return
|
|
167
|
+
|
|
168
|
+
let dist = k * total
|
|
169
|
+
let i = 0
|
|
170
|
+
while (i < lengths.length - 1 && dist > lengths[i]!) { dist -= lengths[i]!; i++ }
|
|
171
|
+
const a = points[i]!
|
|
172
|
+
const b = points[i + 1]!
|
|
173
|
+
const seg = b.sub(a)
|
|
174
|
+
const u = lengths[i]! > 0 ? Math.min(dist / lengths[i]!, 1) : 0
|
|
175
|
+
const world = a.add(seg.scale(u))
|
|
176
|
+
this.node.position = toParentLocal(this.node, world)
|
|
177
|
+
if (this.orient && seg.lengthSq() > 1e-12) this.node.lookAt(world.add(seg), this.forward)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Editor-only waypoint polyline (edit mode; play mode never calls this). */
|
|
181
|
+
rebuild(): void {
|
|
182
|
+
const points = this._points() // loop mode already appends the first point again
|
|
183
|
+
Gizmos.polyline(points, { color: MOVE_LINE }) // waypoints are empties: they carry their own markers
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Continuous rotation about one of the node's local axes, degrees per second. */
|
|
188
|
+
export class Spin extends Aspect<"spin"> {
|
|
189
|
+
/** Degrees per second. */
|
|
190
|
+
speed = 45
|
|
191
|
+
|
|
192
|
+
/** Local axis to spin around. */
|
|
193
|
+
axis: "x" | "y" | "z" = "y"
|
|
194
|
+
|
|
195
|
+
static readonly aspect = "spin"
|
|
196
|
+
static fields: FieldMeta<Spin> = {
|
|
197
|
+
speed: { min: -720, max: 720, step: 5 },
|
|
198
|
+
axis: { options: [ "x", "y", "z" ] },
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
update(dt: number): void {
|
|
202
|
+
const axis: [number, number, number] = this.axis === "x" ? [ 1, 0, 0 ] : this.axis === "y" ? [ 0, 1, 0 ] : [ 0, 0, 1 ]
|
|
203
|
+
// compose in quaternion space about the LOCAL axis (q ⊗ dq) — never euler-increment
|
|
204
|
+
this.node.quaternion = this.node.quaternion.mul(Quat.fromAxisAngle(axis, this.speed * dt * DEG2RAD))
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Keep the node facing a target node (a turret tracking, a signpost, a camera aimed at a hero).
|
|
209
|
+
* Accessor `node.facing` — NOT `lookAt`, which would shadow the `Node.lookAt()` method. */
|
|
210
|
+
export class LookAt extends Aspect<"facing"> {
|
|
211
|
+
/** The node to face. */
|
|
212
|
+
target: Node | null = null
|
|
213
|
+
|
|
214
|
+
/** Which local axis points at the target. */
|
|
215
|
+
forward: "-z" | "z" | "x" | "-x" = "-z"
|
|
216
|
+
|
|
217
|
+
/** 0 = snap instantly; otherwise seconds of turn lag (bigger = slower, smoother). */
|
|
218
|
+
smoothing = 0
|
|
219
|
+
|
|
220
|
+
static readonly aspect = "facing"
|
|
221
|
+
static editor = { rebuild: true }
|
|
222
|
+
static fields: FieldMeta<LookAt> = {
|
|
223
|
+
target: { editor: "node" },
|
|
224
|
+
forward: { options: [ "-z", "z", "x", "-x" ] },
|
|
225
|
+
smoothing: { min: 0, max: 5, step: 0.05 },
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
update(dt: number): void {
|
|
229
|
+
if (!this.target) return
|
|
230
|
+
const point = this.target.worldPosition
|
|
231
|
+
if (this.smoothing <= 0) { this.node.lookAt(point, this.forward); return }
|
|
232
|
+
const q0 = this.node.quaternion
|
|
233
|
+
this.node.lookAt(point, this.forward)
|
|
234
|
+
const q1 = this.node.quaternion
|
|
235
|
+
this.node.quaternion = q0.slerp(q1, 1 - Math.exp(-dt / this.smoothing))
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Editor-only sight line (edit mode; play mode never calls this). */
|
|
239
|
+
rebuild(): void {
|
|
240
|
+
if (!this.target) return
|
|
241
|
+
Gizmos.line(this.node.worldPosition, this.target.worldPosition, { color: LOOK_LINE })
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Start a GLB animation clip when the scene runs. Attach to a `model:` node; the inspector card
|
|
246
|
+
* lists the model's clips live and can preview them while editing. */
|
|
247
|
+
export class PlayAnimation extends Aspect<"playAnimation", Model> {
|
|
248
|
+
/** Clip name; empty = the model's first clip. */
|
|
249
|
+
clip = ""
|
|
250
|
+
|
|
251
|
+
loop = true
|
|
252
|
+
|
|
253
|
+
/** Playback rate (1 = authored speed). */
|
|
254
|
+
speed = 1
|
|
255
|
+
|
|
256
|
+
static readonly aspect = "playAnimation"
|
|
257
|
+
static fields: FieldMeta<PlayAnimation> = {
|
|
258
|
+
speed: { min: 0.05, max: 5, step: 0.05 },
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private _preview?: Playback
|
|
262
|
+
|
|
263
|
+
onAttach(): void {
|
|
264
|
+
const anim = (this.node as Model | undefined)?.anim
|
|
265
|
+
if (!anim) return
|
|
266
|
+
if (this.loop) anim.playLoop(this.clip === "" ? 0 : this.clip, { speed: this.speed })
|
|
267
|
+
else anim.play(this.clip === "" ? 0 : this.clip, { speed: this.speed })
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
static inspector(ui: InspectorUI, a: PlayAnimation): void {
|
|
271
|
+
const anim = (a.node as Model | undefined)?.anim
|
|
272
|
+
const clips = anim ? anim.clips : []
|
|
273
|
+
if (clips.length === 0) {
|
|
274
|
+
ui.warn("No animation clips — attach this to a GLB model node")
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
ui.select("clip", clips.map((c) => c.name))
|
|
278
|
+
ui.auto("loop", "speed")
|
|
279
|
+
if (anim) {
|
|
280
|
+
const clip = a.clip === "" ? clips[0]?.name : a.clip
|
|
281
|
+
const playing = a._preview?.playing === true
|
|
282
|
+
if (ui.button(playing ? "Restart preview" : "Preview")) {
|
|
283
|
+
if (a.loop) { anim.playLoop(clip, { speed: a.speed }); a._preview = undefined }
|
|
284
|
+
else a._preview = anim.play(clip, { speed: a.speed, restart: true })
|
|
285
|
+
}
|
|
286
|
+
if (playing && ui.button("Stop")) { anim.stop(); a._preview = undefined }
|
|
287
|
+
const duration = clips.find((c) => c.name === clip)?.duration
|
|
288
|
+
ui.info(playing ? "previewing…" : duration !== undefined ? `${duration.toFixed(2)}s` : "")
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
package/src/gl/state.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Tiny shared module to break the Node ↔ Scene ↔ touch dependency cycle. The active 3D scene is set
|
|
2
|
+
// here by Scene.open()/close() and read by the touch dispatch system.
|
|
3
|
+
|
|
4
|
+
import type { Scene } from "./Scene"
|
|
5
|
+
|
|
6
|
+
export const glState: { activeScene: Scene | null, frame: number } = { activeScene: null, frame: 0 }
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// The terrain grid as geometry, in TypeScript — used two ways (docs/terrain-plan.md §1.3, §3.5):
|
|
2
|
+
// • `heightAt` / `normalAt` — the SDK's own queries, identical on every host (bilinear on the SAME
|
|
3
|
+
// triangle split the engines draw and collide);
|
|
4
|
+
// • `buildTerrainChunk` — chunk meshes for hosts WITHOUT `_creator.terrainCreate` (web-lite, headless,
|
|
5
|
+
// a wasm/AAR/Apple build predating the feature): the same grid → chunk → triangles rule
|
|
6
|
+
// creator-gl/src/terrain.cpp applies (the i00–i11 diagonal, CCW seen from +Y, a triangle only when
|
|
7
|
+
// its three samples are valid), as plain Geometry for setMesh. Normals use central differences over
|
|
8
|
+
// the FULL grid (seamless chunks); the tangent frame is then the host's arbitrary one, so normal maps
|
|
9
|
+
// read wrong on those hosts — the accepted fallback cost.
|
|
10
|
+
// Keep the hole / split rule in step with terrain.cpp buildChunk and Jolt's HeightFieldShape.
|
|
11
|
+
|
|
12
|
+
import { Geometry } from "./Geometry"
|
|
13
|
+
|
|
14
|
+
export type TerrainGrid = {
|
|
15
|
+
sizeX: number
|
|
16
|
+
sizeZ: number
|
|
17
|
+
cellSize: number
|
|
18
|
+
/** sizeX × sizeZ heights, row-major (z rows). */
|
|
19
|
+
heights: Float32Array
|
|
20
|
+
/** One byte per sample, 1 = hole; null = no holes. */
|
|
21
|
+
holes: Uint8Array | null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const clampI = (v: number, hi: number): number => (v < 0 ? 0 : v > hi ? hi : v)
|
|
25
|
+
|
|
26
|
+
/** The sample at integer (x, z), clamped to the grid. */
|
|
27
|
+
export const sampleAt = (g: TerrainGrid, x: number, z: number): number =>
|
|
28
|
+
g.heights[clampI(z, g.sizeZ - 1) * g.sizeX + clampI(x, g.sizeX - 1)]
|
|
29
|
+
|
|
30
|
+
export const isHole = (g: TerrainGrid, x: number, z: number): boolean =>
|
|
31
|
+
g.holes !== null && g.holes[z * g.sizeX + x] !== 0
|
|
32
|
+
|
|
33
|
+
/** Surface normal at a sample from central differences (one-sided at the borders). Writes `out` (xyz). */
|
|
34
|
+
export const normalAt = (g: TerrainGrid, x: number, z: number, out: Float32Array | number[] = [ 0, 0, 0 ]): typeof out => {
|
|
35
|
+
const x0 = x > 0 ? x - 1 : x, x1 = x + 1 < g.sizeX ? x + 1 : x
|
|
36
|
+
const z0 = z > 0 ? z - 1 : z, z1 = z + 1 < g.sizeZ ? z + 1 : z
|
|
37
|
+
const dx = (sampleAt(g, x1, z) - sampleAt(g, x0, z)) / ((x1 - x0) * g.cellSize)
|
|
38
|
+
const dz = (sampleAt(g, x, z1) - sampleAt(g, x, z0)) / ((z1 - z0) * g.cellSize)
|
|
39
|
+
const len = Math.hypot(dx, 1, dz)
|
|
40
|
+
out[0] = -dx / len; out[1] = 1 / len; out[2] = -dz / len
|
|
41
|
+
return out
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Height at a LOCAL (x, z) in metres, interpolated on the drawn triangles (the i00–i11 diagonal), so a
|
|
46
|
+
* character's feet and a raycast agree with what is rendered to the millimetre. NaN inside a hole
|
|
47
|
+
* triangle or outside the grid.
|
|
48
|
+
*/
|
|
49
|
+
export const heightAt = (g: TerrainGrid, x: number, z: number): number => {
|
|
50
|
+
const fx = x / g.cellSize, fz = z / g.cellSize
|
|
51
|
+
if (!(fx >= 0 && fz >= 0 && fx <= g.sizeX - 1 && fz <= g.sizeZ - 1)) return NaN
|
|
52
|
+
let ix = Math.floor(fx), iz = Math.floor(fz)
|
|
53
|
+
if (ix >= g.sizeX - 1) ix = g.sizeX - 2
|
|
54
|
+
if (iz >= g.sizeZ - 1) iz = g.sizeZ - 2
|
|
55
|
+
const u = fx - ix, v = fz - iz
|
|
56
|
+
const i00 = iz * g.sizeX + ix
|
|
57
|
+
const h00 = g.heights[i00], h10 = g.heights[i00 + 1], h01 = g.heights[i00 + g.sizeX], h11 = g.heights[i00 + g.sizeX + 1]
|
|
58
|
+
if (g.holes !== null) {
|
|
59
|
+
const holes = g.holes
|
|
60
|
+
if (holes[i00] || holes[i00 + g.sizeX + 1]) return NaN
|
|
61
|
+
// lower-left triangle (i00, i01, i11) when v >= u, else upper-right (i00, i11, i10)
|
|
62
|
+
if (v >= u ? holes[i00 + g.sizeX] !== 0 : holes[i00 + 1] !== 0) return NaN
|
|
63
|
+
}
|
|
64
|
+
// plane of the triangle the point falls in
|
|
65
|
+
return v >= u
|
|
66
|
+
? h00 + (h11 - h01) * u + (h01 - h00) * v
|
|
67
|
+
: h00 + (h10 - h00) * u + (h11 - h10) * v
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The geometric normal at a LOCAL (x, z): the drawn triangle's normal (not the smoothed vertex one). */
|
|
71
|
+
export const faceNormalAt = (g: TerrainGrid, x: number, z: number, out: Float32Array | number[] = [ 0, 0, 0 ]): typeof out => {
|
|
72
|
+
const fx = x / g.cellSize, fz = z / g.cellSize
|
|
73
|
+
let ix = Math.floor(fx), iz = Math.floor(fz)
|
|
74
|
+
ix = clampI(ix, g.sizeX - 2); iz = clampI(iz, g.sizeZ - 2)
|
|
75
|
+
const u = fx - ix, v = fz - iz
|
|
76
|
+
const i00 = iz * g.sizeX + ix
|
|
77
|
+
const h00 = g.heights[i00], h10 = g.heights[i00 + 1], h01 = g.heights[i00 + g.sizeX], h11 = g.heights[i00 + g.sizeX + 1]
|
|
78
|
+
const c = g.cellSize
|
|
79
|
+
// gradients of the two planes (see heightAt)
|
|
80
|
+
const dx = (v >= u ? h11 - h01 : h10 - h00) / c
|
|
81
|
+
const dz = (v >= u ? h01 - h00 : h11 - h10) / c
|
|
82
|
+
const len = Math.hypot(dx, 1, dz)
|
|
83
|
+
out[0] = -dx / len; out[1] = 1 / len; out[2] = -dz / len
|
|
84
|
+
return out
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Chunks along X and Z for a grid: a chunk covers `chunk` cells (chunk + 1 samples). */
|
|
88
|
+
export const chunkCounts = (g: TerrainGrid, chunk: number): [number, number] =>
|
|
89
|
+
[ Math.floor((g.sizeX - 2) / chunk) + 1, Math.floor((g.sizeZ - 2) / chunk) + 1 ]
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* One chunk as a Geometry (positions, smooth normals, UV0 = local XZ metres, UV1 = the terrain's unit
|
|
93
|
+
* square, u16 indices), or null when every cell of it is a hole.
|
|
94
|
+
*/
|
|
95
|
+
export const buildTerrainChunk = (g: TerrainGrid, cx: number, cz: number, chunk: number): Geometry | null => {
|
|
96
|
+
const x0 = cx * chunk, z0 = cz * chunk
|
|
97
|
+
const x1 = Math.min(x0 + chunk, g.sizeX - 1), z1 = Math.min(z0 + chunk, g.sizeZ - 1)
|
|
98
|
+
const nx = x1 - x0 + 1, nz = z1 - z0 + 1
|
|
99
|
+
const idx: number[] = []
|
|
100
|
+
for (let z = z0; z < z1; z++) {
|
|
101
|
+
for (let x = x0; x < x1; x++) {
|
|
102
|
+
const i00 = (z - z0) * nx + (x - x0), i10 = i00 + 1, i01 = i00 + nx, i11 = i01 + 1
|
|
103
|
+
const v00 = !isHole(g, x, z), v10 = !isHole(g, x + 1, z), v01 = !isHole(g, x, z + 1), v11 = !isHole(g, x + 1, z + 1)
|
|
104
|
+
if (!(v00 && v11)) continue
|
|
105
|
+
if (v01) idx.push(i00, i01, i11)
|
|
106
|
+
if (v10) idx.push(i00, i11, i10)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (idx.length === 0) return null
|
|
110
|
+
const count = nx * nz
|
|
111
|
+
const vertices = new Float32Array(count * 3)
|
|
112
|
+
const normals = new Float32Array(count * 3)
|
|
113
|
+
const uv = new Float32Array(count * 2)
|
|
114
|
+
const uv1 = new Float32Array(count * 2)
|
|
115
|
+
const n = [ 0, 0, 0 ]
|
|
116
|
+
const invX = 1 / (g.sizeX - 1), invZ = 1 / (g.sizeZ - 1)
|
|
117
|
+
for (let z = z0; z <= z1; z++) {
|
|
118
|
+
for (let x = x0; x <= x1; x++) {
|
|
119
|
+
const i = (z - z0) * nx + (x - x0)
|
|
120
|
+
vertices[i * 3] = x * g.cellSize
|
|
121
|
+
vertices[i * 3 + 1] = g.heights[z * g.sizeX + x]
|
|
122
|
+
vertices[i * 3 + 2] = z * g.cellSize
|
|
123
|
+
normalAt(g, x, z, n)
|
|
124
|
+
normals[i * 3] = n[0]; normals[i * 3 + 1] = n[1]; normals[i * 3 + 2] = n[2]
|
|
125
|
+
uv[i * 2] = x * g.cellSize; uv[i * 2 + 1] = z * g.cellSize
|
|
126
|
+
uv1[i * 2] = x * invX; uv1[i * 2 + 1] = z * invZ
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const geom = new Geometry(vertices, normals, Uint16Array.from(idx), uv)
|
|
130
|
+
geom.uv1 = uv1
|
|
131
|
+
return geom
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---- ray queries ---------------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
export type GridHit = { x: number, y: number, z: number, distance: number, nx: number, ny: number, nz: number }
|
|
137
|
+
|
|
138
|
+
/** Möller–Trumbore against one triangle; the distance along the ray, or -1. */
|
|
139
|
+
const rayTriangle = (
|
|
140
|
+
ox: number, oy: number, oz: number, dx: number, dy: number, dz: number,
|
|
141
|
+
ax: number, ay: number, az: number, bx: number, by: number, bz: number, qx: number, qy: number, qz: number,
|
|
142
|
+
): number => {
|
|
143
|
+
const e1x = bx - ax, e1y = by - ay, e1z = bz - az
|
|
144
|
+
const e2x = qx - ax, e2y = qy - ay, e2z = qz - az
|
|
145
|
+
const px = dy * e2z - dz * e2y, py = dz * e2x - dx * e2z, pz = dx * e2y - dy * e2x
|
|
146
|
+
const det = e1x * px + e1y * py + e1z * pz
|
|
147
|
+
if (Math.abs(det) < 1e-12) return -1
|
|
148
|
+
const inv = 1 / det
|
|
149
|
+
const tx = ox - ax, ty = oy - ay, tz = oz - az
|
|
150
|
+
const u = (tx * px + ty * py + tz * pz) * inv
|
|
151
|
+
if (u < -1e-6 || u > 1 + 1e-6) return -1
|
|
152
|
+
const qxx = ty * e1z - tz * e1y, qyy = tz * e1x - tx * e1z, qzz = tx * e1y - ty * e1x
|
|
153
|
+
const v = (dx * qxx + dy * qyy + dz * qzz) * inv
|
|
154
|
+
if (v < -1e-6 || u + v > 1 + 1e-6) return -1
|
|
155
|
+
const t = (e2x * qxx + e2y * qyy + e2z * qzz) * inv
|
|
156
|
+
return t >= 0 ? t : -1
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Nearest hit of a LOCAL ray (origin + direction, metres) on the drawn triangles — the same
|
|
161
|
+
* i00–i11 split and hole rule as `heightAt`, so a pick lands on what is rendered. Walks the cells
|
|
162
|
+
* the ray crosses (2D DDA on XZ), testing each cell's two triangles; holes let the ray through.
|
|
163
|
+
* Null when it misses (or never enters the grid within `maxDistance`).
|
|
164
|
+
*/
|
|
165
|
+
export const gridRaycast = (g: TerrainGrid, ox: number, oy: number, oz: number, dx: number, dy: number, dz: number, maxDistance = 1e5): GridHit | null => {
|
|
166
|
+
const c = g.cellSize, W = (g.sizeX - 1) * c, D = (g.sizeZ - 1) * c
|
|
167
|
+
let t0 = 0, t1 = maxDistance
|
|
168
|
+
const clip = (o: number, d: number, hi: number): boolean => {
|
|
169
|
+
if (Math.abs(d) < 1e-12) return o >= 0 && o <= hi
|
|
170
|
+
let a = -o / d, b = (hi - o) / d
|
|
171
|
+
if (a > b) { const s = a; a = b; b = s }
|
|
172
|
+
if (a > t0) t0 = a
|
|
173
|
+
if (b < t1) t1 = b
|
|
174
|
+
return t0 <= t1
|
|
175
|
+
}
|
|
176
|
+
if (!clip(ox, dx, W) || !clip(oz, dz, D)) return null
|
|
177
|
+
const sx = ox + dx * t0, sz = oz + dz * t0
|
|
178
|
+
let ix = Math.min(g.sizeX - 2, Math.max(0, Math.floor(sx / c)))
|
|
179
|
+
let iz = Math.min(g.sizeZ - 2, Math.max(0, Math.floor(sz / c)))
|
|
180
|
+
const stepX = dx > 1e-12 ? 1 : dx < -1e-12 ? -1 : 0
|
|
181
|
+
const stepZ = dz > 1e-12 ? 1 : dz < -1e-12 ? -1 : 0
|
|
182
|
+
const tDeltaX = stepX ? Math.abs(c / dx) : Infinity, tDeltaZ = stepZ ? Math.abs(c / dz) : Infinity
|
|
183
|
+
let tMaxX = stepX > 0 ? t0 + ((ix + 1) * c - sx) / dx : stepX < 0 ? t0 + (ix * c - sx) / dx : Infinity
|
|
184
|
+
let tMaxZ = stepZ > 0 ? t0 + ((iz + 1) * c - sz) / dz : stepZ < 0 ? t0 + (iz * c - sz) / dz : Infinity
|
|
185
|
+
const holes = g.holes
|
|
186
|
+
let t = t0
|
|
187
|
+
for (let guard = 0; guard < g.sizeX + g.sizeZ + 2 && t <= t1 + 1e-9; guard++) {
|
|
188
|
+
const i00 = iz * g.sizeX + ix, i10 = i00 + 1, i01 = i00 + g.sizeX, i11 = i01 + 1
|
|
189
|
+
const x0 = ix * c, x1 = x0 + c, z0 = iz * c, z1 = z0 + c
|
|
190
|
+
const h00 = g.heights[i00], h10 = g.heights[i10], h01 = g.heights[i01], h11 = g.heights[i11]
|
|
191
|
+
const ok00 = !holes || holes[i00] === 0, ok11 = !holes || holes[i11] === 0
|
|
192
|
+
let best = -1, tri = 0
|
|
193
|
+
if (ok00 && ok11) {
|
|
194
|
+
// lower-left (i00, i01, i11) and upper-right (i00, i11, i10) — both CCW seen from +Y
|
|
195
|
+
if (!holes || holes[i01] === 0) {
|
|
196
|
+
const d = rayTriangle(ox, oy, oz, dx, dy, dz, x0, h00, z0, x0, h01, z1, x1, h11, z1)
|
|
197
|
+
if (d >= 0 && (best < 0 || d < best)) { best = d; tri = 0 }
|
|
198
|
+
}
|
|
199
|
+
if (!holes || holes[i10] === 0) {
|
|
200
|
+
const d = rayTriangle(ox, oy, oz, dx, dy, dz, x0, h00, z0, x1, h11, z1, x1, h10, z0)
|
|
201
|
+
if (d >= 0 && (best < 0 || d < best)) { best = d; tri = 1 }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (best >= 0 && best <= t1 + 1e-6) {
|
|
205
|
+
// the hit triangle's normal
|
|
206
|
+
let ax: number, ay: number, az: number, bx: number, by: number, bz: number
|
|
207
|
+
if (tri === 0) { ax = 0; ay = h01 - h00; az = c; bx = c; by = h11 - h00; bz = c }
|
|
208
|
+
else { ax = c; ay = h11 - h00; az = c; bx = c; by = h10 - h00; bz = 0 }
|
|
209
|
+
let nx = ay * bz - az * by, ny = az * bx - ax * bz, nz = ax * by - ay * bx
|
|
210
|
+
const l = Math.hypot(nx, ny, nz) || 1
|
|
211
|
+
nx /= l; ny /= l; nz /= l
|
|
212
|
+
return { x: ox + dx * best, y: oy + dy * best, z: oz + dz * best, distance: best, nx, ny, nz }
|
|
213
|
+
}
|
|
214
|
+
if (tMaxX < tMaxZ) { t = tMaxX; ix += stepX; tMaxX += tDeltaX }
|
|
215
|
+
else { t = tMaxZ; iz += stepZ; tMaxZ += tDeltaZ }
|
|
216
|
+
if (ix < 0 || iz < 0 || ix > g.sizeX - 2 || iz > g.sizeZ - 2) break
|
|
217
|
+
}
|
|
218
|
+
return null
|
|
219
|
+
}
|