lecodes-cli 0.18.2 → 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/dist/index.js +1240 -170
- package/package.json +3 -2
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk/compile/aspectMacro.ts +86 -0
- package/runtime/sdk/compile/assetIconMacro.ts +384 -0
- package/runtime/sdk/compile/assetMacro.ts +146 -0
- package/runtime/sdk/compile/assetName.ts +50 -0
- package/runtime/sdk/compile/bundler.ts +287 -0
- package/runtime/sdk/compile/compileProject.ts +129 -0
- package/runtime/sdk/compile/detectEntry.ts +128 -0
- package/runtime/sdk/compile/fontMacro.ts +459 -0
- package/runtime/sdk/compile/fontRegistry.ts +78 -0
- package/runtime/sdk/compile/header.ts +67 -0
- package/runtime/sdk/compile/index.ts +100 -0
- package/runtime/sdk/compile/libraryImports.ts +52 -0
- package/runtime/sdk/compile/liteMaterial.ts +247 -0
- package/runtime/sdk/compile/sceneEditor.ts +88 -0
- package/runtime/sdk/compile/serverSplit.ts +233 -0
- package/runtime/sdk/compile/serverTypes.ts +227 -0
- package/runtime/sdk/compile/sfnt.ts +98 -0
- package/runtime/sdk/compile/shaderSchema.ts +202 -0
- package/runtime/sdk/compile/shaderTargets.ts +81 -0
- package/runtime/sdk/compile/sourcemap.ts +25 -0
- package/runtime/sdk/core/Aspect.ts +65 -9
- package/runtime/sdk/core/StateMachine.ts +308 -0
- package/runtime/sdk/core/time.ts +3 -0
- package/runtime/sdk/gl/CameraPlace.ts +52 -52
- package/runtime/sdk/gl/Light.ts +12 -4
- package/runtime/sdk/gl/Lightmap.ts +87 -10
- package/runtime/sdk/gl/Material.ts +15 -0
- package/runtime/sdk/gl/Model.ts +68 -8
- package/runtime/sdk/gl/NavAgent.ts +337 -0
- package/runtime/sdk/gl/NavMesh.ts +397 -0
- package/runtime/sdk/gl/Ragdoll.ts +270 -0
- package/runtime/sdk/gl/Scene.ts +7 -0
- package/runtime/sdk/gl/Shape.ts +37 -5
- package/runtime/sdk/gl/Terrain.ts +1092 -0
- package/runtime/sdk/gl/Texture.ts +17 -0
- package/runtime/sdk/gl/Trigger.ts +45 -45
- package/runtime/sdk/gl/animation/Animator.ts +35 -2
- package/runtime/sdk/gl/animation/Layer.ts +3 -1
- package/runtime/sdk/gl/animation/Loop.ts +5 -0
- package/runtime/sdk/gl/animation/Playback.ts +4 -3
- package/runtime/sdk/gl/animation/core.ts +52 -10
- package/runtime/sdk/gl/scenarios.ts +291 -291
- package/runtime/sdk/gl/state.ts +6 -6
- package/runtime/sdk/gl/terrainMesh.ts +219 -0
- package/runtime/sdk/inject.ts +12 -1
- package/runtime/sdk/net/codec.ts +119 -0
- package/runtime/sdk/net/core.ts +384 -0
- package/runtime/sdk/net/index.ts +181 -0
- package/runtime/sdk/net/replication.ts +622 -0
- package/runtime/sdk/scene/defineScene.ts +140 -14
- package/runtime/sdk/scene/editorPlugins.ts +18 -0
- package/runtime/sdk/scene/gizmos.ts +148 -148
- package/runtime/sdk/ui/UINode.ts +4 -0
- package/runtime/sdk-types.json +1 -1
|
@@ -48,6 +48,8 @@ import { Mesh } from "../gl/Mesh"
|
|
|
48
48
|
import { Model } from "../gl/Model"
|
|
49
49
|
import { Physics } from "../gl/Physics"
|
|
50
50
|
import { Lightmap } from "../gl/Lightmap"
|
|
51
|
+
import { NavMesh } from "../gl/NavMesh"
|
|
52
|
+
import { Terrain, type TerrainLayer, type TerrainRegion, type TerrainOptions } from "../gl/Terrain"
|
|
51
53
|
import { Light, type SunOptions } from "../gl/Light"
|
|
52
54
|
import { assignMaterialDef, MaterialHandle, resolveMaterialDef, type MaterialDef } from "./material"
|
|
53
55
|
import type { CylinderOptions, PlaneOptions, SphereOptions } from "../gl/Geometry"
|
|
@@ -63,6 +65,34 @@ export type MeshDef =
|
|
|
63
65
|
|
|
64
66
|
export type LightDef = { kind: "sun" } & SunOptions
|
|
65
67
|
|
|
68
|
+
/** A stamp: the terrain conforms to another node of this file (a road) at load — see Terrain.conform. */
|
|
69
|
+
export type TerrainStampDef = {
|
|
70
|
+
/** Path of the node (a Mesh, a Model, or a node holding them) the ground hugs. */
|
|
71
|
+
node: string
|
|
72
|
+
offset?: number
|
|
73
|
+
falloff?: number
|
|
74
|
+
mode?: "both" | "lower" | "raise"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The `terrain: {}` source block (docs/terrain-plan.md §3.3): a `.terrain` file OR a flat grid to
|
|
78
|
+
* generate from, its layers, and the stamps / holes applied at load (in order, after every node of the
|
|
79
|
+
* file exists — a road built by `make()` is a valid target). Add `Shape { heightfield: true }` +
|
|
80
|
+
* `Physics { motion: 'static' }` in `aspects` for collision. */
|
|
81
|
+
export type TerrainNodeDef = {
|
|
82
|
+
/** `asset('./assets/terrain/valley.terrain')` — heights / holes / control from a file. */
|
|
83
|
+
data?: string
|
|
84
|
+
size?: number
|
|
85
|
+
sizeX?: number
|
|
86
|
+
sizeZ?: number
|
|
87
|
+
cellSize?: number
|
|
88
|
+
chunk?: number
|
|
89
|
+
layers?: TerrainLayer[]
|
|
90
|
+
normals?: TerrainOptions["normals"]
|
|
91
|
+
tint?: TerrainOptions["tint"]
|
|
92
|
+
stamps?: TerrainStampDef[]
|
|
93
|
+
holes?: TerrainRegion[]
|
|
94
|
+
}
|
|
95
|
+
|
|
66
96
|
// The material grammar (`{ lit }` / `{ unlit }` / `{ shadow }` / `{ shader, params }` / a
|
|
67
97
|
// material asset handle / a code instance) lives in ./material.ts — re-exported for callers.
|
|
68
98
|
export type { MaterialDef }
|
|
@@ -108,6 +138,8 @@ export type SceneNodeDef = {
|
|
|
108
138
|
/** GLB url — `asset('./hero.glb')`. */
|
|
109
139
|
model?: string
|
|
110
140
|
light?: LightDef
|
|
141
|
+
/** A heightmap ground — see {@link TerrainNodeDef}. */
|
|
142
|
+
terrain?: TerrainNodeDef
|
|
111
143
|
/** The scene camera as a NODE: in play mode `scene.camera` follows this node's world transform
|
|
112
144
|
* every frame (so movement aspects on it are camera flythroughs); the editor shows a frustum
|
|
113
145
|
* marker and refuses to delete the last camera node. The first camera node in file order wins;
|
|
@@ -141,6 +173,8 @@ export type SceneNodeDef = {
|
|
|
141
173
|
* (`time` freezes it at that second instead), so attachments / sight lines / a first-person eye
|
|
142
174
|
* are placed against the animated pose, not the rest pose. Never applied when the scene runs. */
|
|
143
175
|
editor?: { clip?: string, time?: number }
|
|
176
|
+
/** `mesh` / `model` nodes: real-time shadow flags. On a `model` they reach every renderable of
|
|
177
|
+
* the GLB instance, so `castShadows: false` on a first-person viewmodel covers the whole gun. */
|
|
144
178
|
castShadows?: boolean
|
|
145
179
|
receiveShadows?: boolean
|
|
146
180
|
/** Baked lighting (a scene with `env.lightmap`): is this model/mesh node a lightmap STATIC — a
|
|
@@ -148,6 +182,12 @@ export type SceneNodeDef = {
|
|
|
148
182
|
* Default: static unless a `Physics` aspect moves the node (`dynamic` — Physics' default — or
|
|
149
183
|
* `kinematic`). Set it only to override that rule; prefab subtrees inherit the verdict. */
|
|
150
184
|
lightmap?: boolean
|
|
185
|
+
/** Navigation (a scene with `env.navmesh`, see navmesh.md): every STATIC body is walkable by
|
|
186
|
+
* default. `false` leaves this one out of the bake; `'unwalkable'` cuts its footprint out
|
|
187
|
+
* (nobody stands on or crosses it). */
|
|
188
|
+
nav?: false | "unwalkable"
|
|
189
|
+
/** The named navmesh area this static body's surface belongs to (a key of `env.navmesh.areas`). */
|
|
190
|
+
navArea?: string
|
|
151
191
|
// -- capabilities / hierarchy --
|
|
152
192
|
aspects?: readonly AspectEntry<any>[]
|
|
153
193
|
children?: Record<string, SceneNodeDef>
|
|
@@ -166,14 +206,29 @@ export type SceneLightmapDef = {
|
|
|
166
206
|
data: string
|
|
167
207
|
/** `asset('./assets/lightmap/lightmap.ktx2')` */
|
|
168
208
|
texture: string
|
|
209
|
+
/** `asset('./assets/lightmap/lightmap.volume')` — the light VOLUME for everything that moves (the
|
|
210
|
+
* same bake writes it): dynamic nodes, and every model loaded in code while the level runs, read
|
|
211
|
+
* the baked sun/ambient at their own position. Absent = movers stay real-time only. */
|
|
212
|
+
volume?: string
|
|
169
213
|
/** 1 = baked sun shadows at full strength, 0 = ambient occlusion only. Default 1. */
|
|
170
214
|
sunStrength?: number
|
|
171
215
|
/** Multiplier on the ambient share in the shadow math (1 = filament's own darkness). Default 1. */
|
|
172
216
|
ambientScale?: number
|
|
173
217
|
}
|
|
174
218
|
|
|
219
|
+
/** `env.navmesh` — the level's navigation mesh (see navmesh.md): the file `lecodes navmesh bake`
|
|
220
|
+
* writes, the agent size it is built for, and the named areas. Absent = no navigation. */
|
|
221
|
+
export type SceneNavmeshDef = {
|
|
222
|
+
/** `asset('./assets/nav/<scene>.navmesh')` */
|
|
223
|
+
data: string
|
|
224
|
+
/** The agent the mesh is built for (radius 0.4 · height 1.8 · climb 0.4 · slope 50 by default). */
|
|
225
|
+
agent?: { radius?: number, height?: number, climb?: number, slope?: number }
|
|
226
|
+
/** Named areas → traversal cost (1 = plain ground); nodes join one with `navArea`. */
|
|
227
|
+
areas?: Record<string, number>
|
|
228
|
+
}
|
|
229
|
+
|
|
175
230
|
export type SceneDef = {
|
|
176
|
-
env?: SceneOptions & { lightmap?: SceneLightmapDef }
|
|
231
|
+
env?: SceneOptions & { lightmap?: SceneLightmapDef, navmesh?: SceneNavmeshDef }
|
|
177
232
|
camera?: SceneCameraDef
|
|
178
233
|
nodes?: Record<string, SceneNodeDef>
|
|
179
234
|
}
|
|
@@ -344,7 +399,7 @@ const createMesh = async (def: MeshDef, material?: MaterialDef): Promise<Mesh> =
|
|
|
344
399
|
// rename honestly invalidates the rect. make() subtrees are code — they register themselves with
|
|
345
400
|
// Lightmap.add. Edit mode never bakes or applies (the editor shows real-time shadows).
|
|
346
401
|
|
|
347
|
-
type LightmapCtx = { prefix: string }
|
|
402
|
+
type LightmapCtx = { prefix: string, volume: boolean, dynamicOnly?: boolean }
|
|
348
403
|
|
|
349
404
|
const movesByPhysics = (def: SceneNodeDef): boolean =>
|
|
350
405
|
(def.aspects ?? []).some((e) =>
|
|
@@ -353,11 +408,23 @@ const movesByPhysics = (def: SceneNodeDef): boolean =>
|
|
|
353
408
|
/** The static verdict for one node def (the editor's "Baked lighting" switch shows the same rule). */
|
|
354
409
|
export const isLightmapStatic = (def: SceneNodeDef): boolean => def.lightmap ?? !movesByPhysics(def)
|
|
355
410
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
411
|
+
// `lightmap` undefined = Model.load's own default ('dynamic' while a level with a light volume runs —
|
|
412
|
+
// this is how a weapon or arms scene instantiated from code reads the volume; an explicit false would win)
|
|
413
|
+
/** A terrain node: from its file or a fresh flat grid; textures load before the node appears (like a
|
|
414
|
+
* mesh's material). Stamps and holes apply later in buildNodes, once every target node exists. */
|
|
415
|
+
const createTerrain = async (def: TerrainNodeDef): Promise<Node> => {
|
|
416
|
+
const { data, stamps: _s, holes: _h, ...opts } = def
|
|
417
|
+
const t = data !== undefined ? await Terrain.load(data, opts) : Terrain.create(opts)
|
|
418
|
+
await t.ready
|
|
419
|
+
return t.node
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const createSource = (path: string, def: SceneNodeDef, lightmap: boolean | "dynamic" | undefined): Node | Promise<Node> => {
|
|
423
|
+
const sources = [ def.mesh, def.model, def.light, def.make, def.prefab, def.camera, def.terrain ].filter((s) => s !== undefined).length
|
|
424
|
+
if (sources > 1) throw new Error(`Scene node "${path}" declares more than one source (mesh/model/light/make/prefab/camera/terrain)`)
|
|
359
425
|
if (def.model !== undefined) return Model.load(def.model, { lightmap })
|
|
360
426
|
if (def.mesh !== undefined) return createMesh(def.mesh, def.material)
|
|
427
|
+
if (def.terrain !== undefined) return createTerrain(def.terrain)
|
|
361
428
|
if (def.light !== undefined) { const { kind: _k, ...opts } = def.light; return Light.sun(opts) }
|
|
362
429
|
// make/prefab/camera/group nodes are plain wrappers — make/prefab subtrees mount under them
|
|
363
430
|
// during the build (attachPrefab) or in phase 2 (attachMake); a camera node drives scene.camera
|
|
@@ -400,9 +467,15 @@ const applyNode = (node: Node, name: string, def: SceneNodeDef): void => {
|
|
|
400
467
|
// waypoint/def order for aspects that read children (FollowPath) — the engine's live child
|
|
401
468
|
// order is insertion-based and may not match the file
|
|
402
469
|
if (def.children) (node as { _sceneChildOrder?: string[] })._sceneChildOrder = Object.keys(def.children)
|
|
403
|
-
|
|
470
|
+
// shadow flags: a Mesh has one renderable, a Model a whole GLB instance (Model routes the pair
|
|
471
|
+
// through setGlbShadows) — every other node kind is transform-only and carries none
|
|
472
|
+
if (node instanceof Mesh || node instanceof Model) {
|
|
404
473
|
if (def.castShadows !== undefined) node.castShadows = def.castShadows
|
|
405
474
|
if (def.receiveShadows !== undefined) node.receiveShadows = def.receiveShadows
|
|
475
|
+
} else if (def.terrain !== undefined) {
|
|
476
|
+
const t = Terrain.of(node)
|
|
477
|
+
if (t && def.castShadows !== undefined) t.castShadows = def.castShadows
|
|
478
|
+
if (t && def.receiveShadows !== undefined) t.receiveShadows = def.receiveShadows
|
|
406
479
|
}
|
|
407
480
|
if (def.overrides && def.model !== undefined) applyModelOverrides(node, def.overrides)
|
|
408
481
|
}
|
|
@@ -412,7 +485,7 @@ const applyNode = (node: Node, name: string, def: SceneNodeDef): void => {
|
|
|
412
485
|
/** True for a def with no source block at all — a plain group ("Empty" in the editor). */
|
|
413
486
|
const isGroupDef = (def: SceneNodeDef): boolean =>
|
|
414
487
|
def.mesh === undefined && def.model === undefined && def.light === undefined
|
|
415
|
-
&& def.make === undefined && def.prefab === undefined && def.camera === undefined
|
|
488
|
+
&& def.make === undefined && def.prefab === undefined && def.camera === undefined && def.terrain === undefined
|
|
416
489
|
|
|
417
490
|
/** Edit mode: otherwise-invisible nodes (empties, `camera:` nodes) get an ANCHORED gizmo marker —
|
|
418
491
|
* an axis cross / a frustum in the node's local frame (scene/gizmos.ts) — so they show and pick
|
|
@@ -753,8 +826,10 @@ const buildNodes = async (
|
|
|
753
826
|
// the path is derived from def keys BEFORE any await, so it is deterministic even though
|
|
754
827
|
// Promise.all makes build completion (and record insertion) order nondeterministic
|
|
755
828
|
const path = parentPath === "" ? name : `${parentPath}/${name}`
|
|
756
|
-
const lmStatic = lm !== null && isLightmapStatic(nd)
|
|
757
|
-
|
|
829
|
+
const lmStatic = lm !== null && !lm.dynamicOnly && isLightmapStatic(nd)
|
|
830
|
+
// a mover in a level with a light volume reads the volume (models: the engine binds it; meshes: registered below)
|
|
831
|
+
const lmDynamic = lm !== null && !lmStatic && lm.volume
|
|
832
|
+
const node = await createSource(path, nd, lmStatic ? true : lmDynamic ? "dynamic" : undefined)
|
|
758
833
|
// a `mount` def parents to an internal part of the parent asset — safe here: the parent's
|
|
759
834
|
// model was awaited and attachPrefab completed before its children build
|
|
760
835
|
if (parentNode) attachHost(parentNode, nd, path).add(node)
|
|
@@ -762,8 +837,11 @@ const buildNodes = async (
|
|
|
762
837
|
scene.add(node)
|
|
763
838
|
applyNode(node, name, nd) // engine-side name stays the bare sibling segment
|
|
764
839
|
if (lmStatic && (node instanceof Model || node instanceof Mesh)) Lightmap._register(node, lm!.prefix + path)
|
|
840
|
+
else if (lmStatic && Terrain.of(node)) Lightmap._registerTerrain(Terrain.of(node)!, lm!.prefix + path)
|
|
841
|
+
else if (lmDynamic && node instanceof Mesh) Lightmap._registerDynamic(node)
|
|
765
842
|
if (isEditMode()) addEditorMarker(node, nd)
|
|
766
|
-
|
|
843
|
+
// a prefab under a static wrapper bakes; under a mover its internals stay movers (volume-lit)
|
|
844
|
+
if (nd.prefab !== undefined) await attachPrefab(node, path, nd, scene, stack, lmStatic ? { prefix: `${lm!.prefix}${path}/`, volume: lm!.volume } : lm && lm.volume ? { prefix: `${lm.prefix}${path}/`, volume: true, dynamicOnly: true } : null)
|
|
767
845
|
pending.push({ path, node, def: nd })
|
|
768
846
|
nodes[path] = node
|
|
769
847
|
await Promise.all(Object.entries(nd.children ?? {}).map(([ childName, child ]) => build(childName, child, node, path)))
|
|
@@ -778,16 +856,59 @@ const buildNodes = async (
|
|
|
778
856
|
if (wait) makeWaits.push(wait)
|
|
779
857
|
}
|
|
780
858
|
if (makeWaits.length > 0) await Promise.all(makeWaits)
|
|
859
|
+
|
|
860
|
+
// terrain stamps + holes: every node of the file exists now (make() subtrees included), and a
|
|
861
|
+
// Shape { heightfield } attached above takes the edits through commit() (an in-place SetHeights)
|
|
862
|
+
for (const p of pending) {
|
|
863
|
+
const td = p.def.terrain
|
|
864
|
+
const t = td ? Terrain.of(p.node) : null
|
|
865
|
+
if (!td || !t || (!td.stamps?.length && !td.holes?.length)) continue
|
|
866
|
+
for (const s of td.stamps ?? []) {
|
|
867
|
+
const target = nodes[s.node]
|
|
868
|
+
if (!target) { console.warn(`[scene] "${p.path}".terrain stamp "${s.node}" matches no node — skipped`); continue }
|
|
869
|
+
t.conform(target, { offset: s.offset, falloff: s.falloff, mode: s.mode })
|
|
870
|
+
}
|
|
871
|
+
for (const h of td.holes ?? []) t.hole(h)
|
|
872
|
+
t.commit()
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// ---- navigation (env.navmesh) ------------------------------------------------------------------
|
|
877
|
+
// Walkable = every STATIC Physics body (what the CharacterController collides with), derived like
|
|
878
|
+
// the lightmap statics — no per-node declaration. `nav: false | 'unwalkable'` and `navArea` are the
|
|
879
|
+
// per-node overrides, keyed by the node's entity for the bake (NavMesh.exclude / unwalkable / area).
|
|
880
|
+
// Prefab internals are not walked (v1): override them in the prefab's own file.
|
|
881
|
+
|
|
882
|
+
const forEachNodeDef = (defs: Record<string, SceneNodeDef> | undefined, prefix: string, fn: (path: string, def: SceneNodeDef) => void): void => {
|
|
883
|
+
for (const [ key, d ] of Object.entries(defs ?? {})) {
|
|
884
|
+
const path = prefix ? `${prefix}/${key}` : key
|
|
885
|
+
fn(path, d)
|
|
886
|
+
forEachNodeDef(d.children, path, fn)
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const registerNavOverrides = (defs: Record<string, SceneNodeDef> | undefined, nodes: Record<string, Node>): void => {
|
|
891
|
+
NavMesh.clear()
|
|
892
|
+
forEachNodeDef(defs, "", (path, d) => {
|
|
893
|
+
const node = nodes[path]
|
|
894
|
+
if (!node) return
|
|
895
|
+
if (d.nav === false) NavMesh.exclude(node)
|
|
896
|
+
else if (d.nav === "unwalkable") NavMesh.unwalkable(node)
|
|
897
|
+
else if (d.navArea) NavMesh.area(node, d.navArea)
|
|
898
|
+
})
|
|
781
899
|
}
|
|
782
900
|
|
|
783
901
|
const instantiate = async (def: SceneDef, editorRuns: EditorRun[]): Promise<{ scene: Scene, nodes: Record<string, Node> }> => {
|
|
784
|
-
const { lightmap, ...env } = def.env ?? {}
|
|
902
|
+
const { lightmap, navmesh, ...env } = def.env ?? {}
|
|
785
903
|
const scene = new Scene(env)
|
|
786
904
|
const nodes: Record<string, Node> = {}
|
|
787
905
|
// baked lighting is a play-mode concern; the level owns the registry (a rebuild starts clean)
|
|
788
|
-
const lm: LightmapCtx | null = lightmap && !isEditMode() ? { prefix: "" } : null
|
|
906
|
+
const lm: LightmapCtx | null = lightmap && !isEditMode() ? { prefix: "", volume: !!lightmap.volume } : null
|
|
789
907
|
if (lm) Lightmap.clear()
|
|
908
|
+
// with a light volume every model loaded from now on (weapons, spawns, make() subtrees) is a mover by default
|
|
909
|
+
if (lm) Model._lightmapDefault = lm.volume ? "dynamic" : false
|
|
790
910
|
await buildNodes(def.nodes ?? {}, null, scene, nodes, editorRuns, new Set([ def ]), lm)
|
|
911
|
+
if (navmesh && !isEditMode()) registerNavOverrides(def.nodes, nodes)
|
|
791
912
|
|
|
792
913
|
// a camera NODE wins over the top-level `camera:` block; in play mode it keeps driving the view
|
|
793
914
|
// (CameraRig), in edit mode it only seeds the editor's starting viewpoint. The PROJECTION applies
|
|
@@ -818,9 +939,14 @@ const instantiate = async (def: SceneDef, editorRuns: EditorRun[]): Promise<{ sc
|
|
|
818
939
|
|
|
819
940
|
// the level is complete: apply the bake — or, under `lecodes lightmap bake`, run it
|
|
820
941
|
if (lm && lightmap) {
|
|
821
|
-
await Lightmap.load(scene, { data: lightmap.data, texture: lightmap.texture },
|
|
942
|
+
await Lightmap.load(scene, { data: lightmap.data, texture: lightmap.texture, volume: lightmap.volume },
|
|
822
943
|
{ sunStrength: lightmap.sunStrength, ambientScale: lightmap.ambientScale })
|
|
823
944
|
}
|
|
945
|
+
// navigation: load the baked mesh — or, under `lecodes navmesh bake`, dump the level's collision
|
|
946
|
+
// geometry for it (the static bodies exist by now; edit mode never loads it — v1)
|
|
947
|
+
if (navmesh && !isEditMode()) {
|
|
948
|
+
await NavMesh.load(scene, { data: navmesh.data, agent: navmesh.agent, areas: navmesh.areas })
|
|
949
|
+
}
|
|
824
950
|
|
|
825
951
|
return { scene, nodes }
|
|
826
952
|
}
|
|
@@ -943,7 +1069,7 @@ export class SceneHandle<D extends SceneDef = SceneDef> {
|
|
|
943
1069
|
if (parent) attachHost(parent, d, p).add(existing)
|
|
944
1070
|
return existing
|
|
945
1071
|
}
|
|
946
|
-
const node = await createSource(p, d)
|
|
1072
|
+
const node = await createSource(p, d, undefined)
|
|
947
1073
|
if (parent) attachHost(parent, d, p).add(node)
|
|
948
1074
|
scene.add(node)
|
|
949
1075
|
applyNode(node, p.slice(p.lastIndexOf("/") + 1), d)
|
|
@@ -52,6 +52,15 @@ export type EditorApi = {
|
|
|
52
52
|
raycast(screenX: number, screenY: number): EditorRayHit | null
|
|
53
53
|
/** Group every doc edit inside `fn` into ONE undo step. */
|
|
54
54
|
transact(fn: () => void): void
|
|
55
|
+
/** Read one def prop of a node as document data (`asset()` refs come as `{ $asset: "./x" }`);
|
|
56
|
+
* undefined when the node or key doesn't exist. */
|
|
57
|
+
getProp(path: string, key: string): unknown
|
|
58
|
+
/** Write a BINARY asset file for a node (a terrain's `.terrain`, a baked mask…). `ref` = the
|
|
59
|
+
* node's existing def-relative `asset()` ref to overwrite, or null to create `./<name>` at the
|
|
60
|
+
* project root (a free name is picked on collisions). Resolves to the ref to store in the node's
|
|
61
|
+
* def (`{ $asset: ref }` via `setProp`), null when the host can't write files. Not a doc edit —
|
|
62
|
+
* it is not undoable; keep the doc pointing at the file. */
|
|
63
|
+
writeAsset(path: string, ref: string | null, name: string, bytes: Uint8Array): Promise<string | null>
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
export type EditorWindowFn = (ui: InspectorUI, editor: EditorApi) => void
|
|
@@ -65,6 +74,15 @@ export type EditorToolHooks = {
|
|
|
65
74
|
/** A viewport click while the tool is active — `hit` is the raycast result under the pointer.
|
|
66
75
|
* Viewport clicks route to the tool while it is active. */
|
|
67
76
|
onViewportClick?(hit: EditorRayHit, editor: EditorApi): void
|
|
77
|
+
/** Drag tools (brushes): a primary-button drag in the viewport routes here instead of orbiting
|
|
78
|
+
* the camera — start / every pointer move (with the hit under it, null while off-world) /
|
|
79
|
+
* release. A tool with `onDragStart` gets NO `onViewportClick` for the same gesture. */
|
|
80
|
+
onDragStart?(hit: EditorRayHit, editor: EditorApi): void
|
|
81
|
+
onDrag?(hit: EditorRayHit | null, editor: EditorApi): void
|
|
82
|
+
onDragEnd?(editor: EditorApi): void
|
|
83
|
+
/** The pointer moved over the viewport with no button down (throttled to the frame) — draw a
|
|
84
|
+
* brush cursor through `Gizmos`; null when nothing is under it. */
|
|
85
|
+
onHover?(hit: EditorRayHit | null, editor: EditorApi): void
|
|
68
86
|
}
|
|
69
87
|
|
|
70
88
|
/** @internal The registry the scene-editor harness reads (windows/tools in registration order). */
|
|
@@ -1,148 +1,148 @@
|
|
|
1
|
-
// Editor gizmos: immediate-mode line drawing for code that runs while a scene is EDITED — a
|
|
2
|
-
// generator aspect's `rebuild()`, a `make()` factory, an editor tool's hooks. Nothing here touches
|
|
3
|
-
// the scene: calls accumulate WORLD-space line segments into the buffer of the run that is
|
|
4
|
-
// currently executing (an ambient scope the scene loader opens around each synchronous call), and
|
|
5
|
-
// the scene editor pushes those buffers to the lite engine's overlay layer, which draws them over
|
|
6
|
-
// the picture — never outlined, never picked, never rendered by Filament, absent in play mode.
|
|
7
|
-
//
|
|
8
|
-
// rebuild() {
|
|
9
|
-
// Gizmos.polyline(points, { color: "#5b8ef0", closed: true })
|
|
10
|
-
// for (const p of points) Gizmos.cross(p, 0.07)
|
|
11
|
-
// }
|
|
12
|
-
//
|
|
13
|
-
// Every run starts with an EMPTY buffer — what a call draws is the whole picture (like
|
|
14
|
-
// `generated.clear()`, only implicit). Outside any scope (play mode, hand-attached aspects, a call
|
|
15
|
-
// after an `await` inside rebuild) the calls are no-ops: rebuild is synchronous by contract.
|
|
16
|
-
//
|
|
17
|
-
// ANCHORED gizmos (`{ node }` in the style): points are in that node's LOCAL frame and the engine
|
|
18
|
-
// follows the node live — through a gizmo drag, not only after it — with the node's scale
|
|
19
|
-
// stripped (an empty scaled ×10 for its children keeps a normal-sized cross). An anchored picture
|
|
20
|
-
// is the node's own: clicking it in the viewport selects the node, and it draws in the selection
|
|
21
|
-
// accent while selected. World-space calls (path lines) are never pickable.
|
|
22
|
-
|
|
23
|
-
import { Mat4, type Mat4Like } from "../math/mat4"
|
|
24
|
-
import type { Vec3Like } from "../math/vec"
|
|
25
|
-
|
|
26
|
-
/** The anchor of a gizmo call: any object with an engine entity id (a `Node`). */
|
|
27
|
-
export type GizmoAnchor = { readonly id: number }
|
|
28
|
-
|
|
29
|
-
export type GizmoStyle = {
|
|
30
|
-
/** CSS hex color (`#rgb` / `#rrggbb`); default a neutral light gray. */
|
|
31
|
-
color?: string
|
|
32
|
-
/** 0..1, default 1. */
|
|
33
|
-
alpha?: number
|
|
34
|
-
/** Anchor: points are in this node's LOCAL frame (scale ignored); the lines follow the node
|
|
35
|
-
* live, are pickable (a click selects the node) and turn the selection accent when it is
|
|
36
|
-
* selected. Omit for world-space lines (not pickable). */
|
|
37
|
-
node?: GizmoAnchor | null
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** One color batch of LINES (flat `[x,y,z, x,y,z]` per segment) — what a host hands the engine.
|
|
41
|
-
* With `entityId` the segments are in that entity's local frame (see GizmoStyle.node). */
|
|
42
|
-
export type GizmoBatch = { color: string, alpha: number, segments: number[], entityId?: number }
|
|
43
|
-
|
|
44
|
-
const DEFAULT_COLOR = "#cfd4dd"
|
|
45
|
-
|
|
46
|
-
/** The per-run collector (owned by the scene loader's EditorRun). Batches are keyed by style so a
|
|
47
|
-
* hundred same-colored segments cost one draw. */
|
|
48
|
-
export class GizmoBuffer {
|
|
49
|
-
batches: GizmoBatch[] = []
|
|
50
|
-
private _byStyle = new Map<string, GizmoBatch>()
|
|
51
|
-
|
|
52
|
-
clear(): void {
|
|
53
|
-
this.batches = []
|
|
54
|
-
this._byStyle.clear()
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
segments(style: GizmoStyle | undefined): number[] {
|
|
58
|
-
const color = style?.color ?? DEFAULT_COLOR
|
|
59
|
-
const alpha = Math.max(0, Math.min(1, style?.alpha ?? 1))
|
|
60
|
-
const entityId = style?.node?.id
|
|
61
|
-
const key = `${color}@${alpha}@${entityId ?? ""}`
|
|
62
|
-
let b = this._byStyle.get(key)
|
|
63
|
-
if (!b) {
|
|
64
|
-
b = entityId ? { color, alpha, segments: [], entityId } : { color, alpha, segments: [] }
|
|
65
|
-
this._byStyle.set(key, b)
|
|
66
|
-
this.batches.push(b)
|
|
67
|
-
}
|
|
68
|
-
return b.segments
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
let current: GizmoBuffer | null = null
|
|
73
|
-
let warnedOutOfScope = false
|
|
74
|
-
|
|
75
|
-
const xyz = (p: Vec3Like): [number, number, number] =>
|
|
76
|
-
Array.isArray(p) ? [ p[0] ?? 0, p[1] ?? 0, p[2] ?? 0 ] : [ (p as { x: number }).x, (p as { y: number }).y, (p as { z: number }).z ]
|
|
77
|
-
|
|
78
|
-
const target = (style: GizmoStyle | undefined): number[] | null => {
|
|
79
|
-
if (current) return current.segments(style)
|
|
80
|
-
if (!warnedOutOfScope && (globalThis as { __lecodesSceneEdit?: boolean }).__lecodesSceneEdit) {
|
|
81
|
-
warnedOutOfScope = true
|
|
82
|
-
console.warn("[Gizmos] draw call outside an editor run — gizmos must be drawn synchronously inside rebuild() / a make() factory / a tool hook")
|
|
83
|
-
}
|
|
84
|
-
return null
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** @internal Run `fn` with `buffer` as the ambient gizmo target (cleared first). Nested scopes
|
|
88
|
-
* restore the outer one. */
|
|
89
|
-
export const withGizmoScope = <T>(buffer: GizmoBuffer, fn: () => T): T => {
|
|
90
|
-
const prev = current
|
|
91
|
-
buffer.clear()
|
|
92
|
-
current = buffer
|
|
93
|
-
try { return fn() } finally { current = prev }
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Editor-only line drawing, available inside generator `rebuild()`, `make()` factories and editor
|
|
98
|
-
* tool hooks. Points are WORLD space. Drawn by the scene editor's viewport overlay (depth-test
|
|
99
|
-
* off — a path through a wall is still a path); invisible everywhere else.
|
|
100
|
-
*/
|
|
101
|
-
export const Gizmos = {
|
|
102
|
-
/** One segment from `a` to `b`. */
|
|
103
|
-
line(a: Vec3Like, b: Vec3Like, style?: GizmoStyle): void {
|
|
104
|
-
const out = target(style)
|
|
105
|
-
if (!out) return
|
|
106
|
-
out.push(...xyz(a), ...xyz(b))
|
|
107
|
-
},
|
|
108
|
-
|
|
109
|
-
/** Consecutive segments through `points`; `closed` joins the last point back to the first. */
|
|
110
|
-
polyline(points: readonly Vec3Like[], style?: GizmoStyle & { closed?: boolean }): void {
|
|
111
|
-
if (points.length < 2) return
|
|
112
|
-
const out = target(style)
|
|
113
|
-
if (!out) return
|
|
114
|
-
const n = points.length
|
|
115
|
-
const last = style?.closed ? n : n - 1
|
|
116
|
-
for (let i = 0; i < last; i++) out.push(...xyz(points[i]!), ...xyz(points[(i + 1) % n]!))
|
|
117
|
-
},
|
|
118
|
-
|
|
119
|
-
/** A wireframe camera frustum looking down −Z: the four edges from the origin to a rect
|
|
120
|
-
* `length` metres ahead sized by `fov` (vertical, degrees) × `aspect` (default 16:9), plus an
|
|
121
|
-
* "up" fin above the rect. Anchor it (`{ node }`) for a camera node's marker — the frustum
|
|
122
|
-
* then follows the node's pose; `matrix` places an unanchored one in world space. */
|
|
123
|
-
frustum(fov: number, style?: GizmoStyle & { aspect?: number, length?: number, matrix?: Mat4Like }): void {
|
|
124
|
-
const out = target(style)
|
|
125
|
-
if (!out) return
|
|
126
|
-
const aspect = style?.aspect ?? 16 / 9, length = style?.length ?? 0.6
|
|
127
|
-
const m = style?.matrix ? new Mat4(style.matrix) : null
|
|
128
|
-
const h = Math.tan((fov * Math.PI) / 360) * length, w = h * aspect, z = -length
|
|
129
|
-
const P = (x: number, y: number, zz: number): [number, number, number] => {
|
|
130
|
-
if (!m) return [ x, y, zz ]
|
|
131
|
-
const v = m.transformPoint([ x, y, zz ])
|
|
132
|
-
return [ v.x, v.y, v.z ]
|
|
133
|
-
}
|
|
134
|
-
const o = P(0, 0, 0)
|
|
135
|
-
const c = [ P(-w, -h, z), P(w, -h, z), P(w, h, z), P(-w, h, z) ]
|
|
136
|
-
for (const q of c) out.push(...o, ...q)
|
|
137
|
-
for (let i = 0; i < 4; i++) out.push(...c[i]!, ...c[(i + 1) % 4]!)
|
|
138
|
-
out.push(...P(-w * 0.4, h, z), ...P(0, h * 1.6, z), ...P(0, h * 1.6, z), ...P(w * 0.4, h, z))
|
|
139
|
-
},
|
|
140
|
-
|
|
141
|
-
/** A three-axis cross centred on `p` (a point marker); `size` = half extent, default 0.1. */
|
|
142
|
-
cross(p: Vec3Like, size = 0.1, style?: GizmoStyle): void {
|
|
143
|
-
const out = target(style)
|
|
144
|
-
if (!out) return
|
|
145
|
-
const [ x, y, z ] = xyz(p)
|
|
146
|
-
out.push(x - size, y, z, x + size, y, z, x, y - size, z, x, y + size, z, x, y, z - size, x, y, z + size)
|
|
147
|
-
},
|
|
148
|
-
}
|
|
1
|
+
// Editor gizmos: immediate-mode line drawing for code that runs while a scene is EDITED — a
|
|
2
|
+
// generator aspect's `rebuild()`, a `make()` factory, an editor tool's hooks. Nothing here touches
|
|
3
|
+
// the scene: calls accumulate WORLD-space line segments into the buffer of the run that is
|
|
4
|
+
// currently executing (an ambient scope the scene loader opens around each synchronous call), and
|
|
5
|
+
// the scene editor pushes those buffers to the lite engine's overlay layer, which draws them over
|
|
6
|
+
// the picture — never outlined, never picked, never rendered by Filament, absent in play mode.
|
|
7
|
+
//
|
|
8
|
+
// rebuild() {
|
|
9
|
+
// Gizmos.polyline(points, { color: "#5b8ef0", closed: true })
|
|
10
|
+
// for (const p of points) Gizmos.cross(p, 0.07)
|
|
11
|
+
// }
|
|
12
|
+
//
|
|
13
|
+
// Every run starts with an EMPTY buffer — what a call draws is the whole picture (like
|
|
14
|
+
// `generated.clear()`, only implicit). Outside any scope (play mode, hand-attached aspects, a call
|
|
15
|
+
// after an `await` inside rebuild) the calls are no-ops: rebuild is synchronous by contract.
|
|
16
|
+
//
|
|
17
|
+
// ANCHORED gizmos (`{ node }` in the style): points are in that node's LOCAL frame and the engine
|
|
18
|
+
// follows the node live — through a gizmo drag, not only after it — with the node's scale
|
|
19
|
+
// stripped (an empty scaled ×10 for its children keeps a normal-sized cross). An anchored picture
|
|
20
|
+
// is the node's own: clicking it in the viewport selects the node, and it draws in the selection
|
|
21
|
+
// accent while selected. World-space calls (path lines) are never pickable.
|
|
22
|
+
|
|
23
|
+
import { Mat4, type Mat4Like } from "../math/mat4"
|
|
24
|
+
import type { Vec3Like } from "../math/vec"
|
|
25
|
+
|
|
26
|
+
/** The anchor of a gizmo call: any object with an engine entity id (a `Node`). */
|
|
27
|
+
export type GizmoAnchor = { readonly id: number }
|
|
28
|
+
|
|
29
|
+
export type GizmoStyle = {
|
|
30
|
+
/** CSS hex color (`#rgb` / `#rrggbb`); default a neutral light gray. */
|
|
31
|
+
color?: string
|
|
32
|
+
/** 0..1, default 1. */
|
|
33
|
+
alpha?: number
|
|
34
|
+
/** Anchor: points are in this node's LOCAL frame (scale ignored); the lines follow the node
|
|
35
|
+
* live, are pickable (a click selects the node) and turn the selection accent when it is
|
|
36
|
+
* selected. Omit for world-space lines (not pickable). */
|
|
37
|
+
node?: GizmoAnchor | null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** One color batch of LINES (flat `[x,y,z, x,y,z]` per segment) — what a host hands the engine.
|
|
41
|
+
* With `entityId` the segments are in that entity's local frame (see GizmoStyle.node). */
|
|
42
|
+
export type GizmoBatch = { color: string, alpha: number, segments: number[], entityId?: number }
|
|
43
|
+
|
|
44
|
+
const DEFAULT_COLOR = "#cfd4dd"
|
|
45
|
+
|
|
46
|
+
/** The per-run collector (owned by the scene loader's EditorRun). Batches are keyed by style so a
|
|
47
|
+
* hundred same-colored segments cost one draw. */
|
|
48
|
+
export class GizmoBuffer {
|
|
49
|
+
batches: GizmoBatch[] = []
|
|
50
|
+
private _byStyle = new Map<string, GizmoBatch>()
|
|
51
|
+
|
|
52
|
+
clear(): void {
|
|
53
|
+
this.batches = []
|
|
54
|
+
this._byStyle.clear()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
segments(style: GizmoStyle | undefined): number[] {
|
|
58
|
+
const color = style?.color ?? DEFAULT_COLOR
|
|
59
|
+
const alpha = Math.max(0, Math.min(1, style?.alpha ?? 1))
|
|
60
|
+
const entityId = style?.node?.id
|
|
61
|
+
const key = `${color}@${alpha}@${entityId ?? ""}`
|
|
62
|
+
let b = this._byStyle.get(key)
|
|
63
|
+
if (!b) {
|
|
64
|
+
b = entityId ? { color, alpha, segments: [], entityId } : { color, alpha, segments: [] }
|
|
65
|
+
this._byStyle.set(key, b)
|
|
66
|
+
this.batches.push(b)
|
|
67
|
+
}
|
|
68
|
+
return b.segments
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let current: GizmoBuffer | null = null
|
|
73
|
+
let warnedOutOfScope = false
|
|
74
|
+
|
|
75
|
+
const xyz = (p: Vec3Like): [number, number, number] =>
|
|
76
|
+
Array.isArray(p) ? [ p[0] ?? 0, p[1] ?? 0, p[2] ?? 0 ] : [ (p as { x: number }).x, (p as { y: number }).y, (p as { z: number }).z ]
|
|
77
|
+
|
|
78
|
+
const target = (style: GizmoStyle | undefined): number[] | null => {
|
|
79
|
+
if (current) return current.segments(style)
|
|
80
|
+
if (!warnedOutOfScope && (globalThis as { __lecodesSceneEdit?: boolean }).__lecodesSceneEdit) {
|
|
81
|
+
warnedOutOfScope = true
|
|
82
|
+
console.warn("[Gizmos] draw call outside an editor run — gizmos must be drawn synchronously inside rebuild() / a make() factory / a tool hook")
|
|
83
|
+
}
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** @internal Run `fn` with `buffer` as the ambient gizmo target (cleared first). Nested scopes
|
|
88
|
+
* restore the outer one. */
|
|
89
|
+
export const withGizmoScope = <T>(buffer: GizmoBuffer, fn: () => T): T => {
|
|
90
|
+
const prev = current
|
|
91
|
+
buffer.clear()
|
|
92
|
+
current = buffer
|
|
93
|
+
try { return fn() } finally { current = prev }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Editor-only line drawing, available inside generator `rebuild()`, `make()` factories and editor
|
|
98
|
+
* tool hooks. Points are WORLD space. Drawn by the scene editor's viewport overlay (depth-test
|
|
99
|
+
* off — a path through a wall is still a path); invisible everywhere else.
|
|
100
|
+
*/
|
|
101
|
+
export const Gizmos = {
|
|
102
|
+
/** One segment from `a` to `b`. */
|
|
103
|
+
line(a: Vec3Like, b: Vec3Like, style?: GizmoStyle): void {
|
|
104
|
+
const out = target(style)
|
|
105
|
+
if (!out) return
|
|
106
|
+
out.push(...xyz(a), ...xyz(b))
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
/** Consecutive segments through `points`; `closed` joins the last point back to the first. */
|
|
110
|
+
polyline(points: readonly Vec3Like[], style?: GizmoStyle & { closed?: boolean }): void {
|
|
111
|
+
if (points.length < 2) return
|
|
112
|
+
const out = target(style)
|
|
113
|
+
if (!out) return
|
|
114
|
+
const n = points.length
|
|
115
|
+
const last = style?.closed ? n : n - 1
|
|
116
|
+
for (let i = 0; i < last; i++) out.push(...xyz(points[i]!), ...xyz(points[(i + 1) % n]!))
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
/** A wireframe camera frustum looking down −Z: the four edges from the origin to a rect
|
|
120
|
+
* `length` metres ahead sized by `fov` (vertical, degrees) × `aspect` (default 16:9), plus an
|
|
121
|
+
* "up" fin above the rect. Anchor it (`{ node }`) for a camera node's marker — the frustum
|
|
122
|
+
* then follows the node's pose; `matrix` places an unanchored one in world space. */
|
|
123
|
+
frustum(fov: number, style?: GizmoStyle & { aspect?: number, length?: number, matrix?: Mat4Like }): void {
|
|
124
|
+
const out = target(style)
|
|
125
|
+
if (!out) return
|
|
126
|
+
const aspect = style?.aspect ?? 16 / 9, length = style?.length ?? 0.6
|
|
127
|
+
const m = style?.matrix ? new Mat4(style.matrix) : null
|
|
128
|
+
const h = Math.tan((fov * Math.PI) / 360) * length, w = h * aspect, z = -length
|
|
129
|
+
const P = (x: number, y: number, zz: number): [number, number, number] => {
|
|
130
|
+
if (!m) return [ x, y, zz ]
|
|
131
|
+
const v = m.transformPoint([ x, y, zz ])
|
|
132
|
+
return [ v.x, v.y, v.z ]
|
|
133
|
+
}
|
|
134
|
+
const o = P(0, 0, 0)
|
|
135
|
+
const c = [ P(-w, -h, z), P(w, -h, z), P(w, h, z), P(-w, h, z) ]
|
|
136
|
+
for (const q of c) out.push(...o, ...q)
|
|
137
|
+
for (let i = 0; i < 4; i++) out.push(...c[i]!, ...c[(i + 1) % 4]!)
|
|
138
|
+
out.push(...P(-w * 0.4, h, z), ...P(0, h * 1.6, z), ...P(0, h * 1.6, z), ...P(w * 0.4, h, z))
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
/** A three-axis cross centred on `p` (a point marker); `size` = half extent, default 0.1. */
|
|
142
|
+
cross(p: Vec3Like, size = 0.1, style?: GizmoStyle): void {
|
|
143
|
+
const out = target(style)
|
|
144
|
+
if (!out) return
|
|
145
|
+
const [ x, y, z ] = xyz(p)
|
|
146
|
+
out.push(x - size, y, z, x + size, y, z, x, y - size, z, x, y + size, z, x, y, z - size, x, y, z + size)
|
|
147
|
+
},
|
|
148
|
+
}
|
package/runtime/sdk/ui/UINode.ts
CHANGED
|
@@ -26,6 +26,10 @@ export type Color = number | string | null
|
|
|
26
26
|
export type BaseStyle = {
|
|
27
27
|
opacity?: number | `${number}`,
|
|
28
28
|
transform?: string | null,
|
|
29
|
+
/** Pivot of `transform` (CSS `transform-origin` subset): one or two values, each a keyword
|
|
30
|
+
* (`left`/`center`/`right`, `top`/`center`/`bottom`), a percentage of the box, or a px length
|
|
31
|
+
* (bare numbers are px). One value sets x, y stays `50%`. Default `"50% 50%"` = the centre. */
|
|
32
|
+
transformOrigin?: string | null,
|
|
29
33
|
backgroundColor?: Color | null,
|
|
30
34
|
bgColor?: Color | null,
|
|
31
35
|
overflow?: "visible" | "hidden",
|