lecodes-sdk 0.20.2 → 1.0.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/global.d.ts +17 -5
- package/dist/types/canvas/Canvas.d.ts +2 -0
- package/dist/types/gl/DecalSet.d.ts +48 -3
- package/dist/types/gl/Geometry.d.ts +12 -0
- package/dist/types/gl/Lightmap.d.ts +9 -0
- package/dist/types/gl/Material.d.ts +4 -0
- package/dist/types/gl/Mesh.d.ts +7 -1
- package/dist/types/gl/Model.d.ts +34 -0
- package/dist/types/gl/Particles.d.ts +13 -0
- package/dist/types/gl/Scene.d.ts +1 -1
- package/dist/types/gl/animation/DynamicBone.d.ts +173 -0
- package/dist/types/gl/{IK.d.ts → animation/IK.d.ts} +4 -4
- package/dist/types/gl/{Locomotion.d.ts → animation/Locomotion.d.ts} +6 -6
- package/dist/types/gl/{AudioSource.d.ts → audio/AudioSource.d.ts} +6 -6
- package/dist/types/gl/{AudioZone.d.ts → audio/AudioZone.d.ts} +4 -4
- package/dist/types/gl/{SceneAudio.d.ts → audio/SceneAudio.d.ts} +1 -1
- package/dist/types/gl/{NavAgent.d.ts → nav/NavAgent.d.ts} +4 -4
- package/dist/types/gl/{NavMesh.d.ts → nav/NavMesh.d.ts} +4 -4
- package/dist/types/gl/{CharacterController.d.ts → physics/CharacterController.d.ts} +5 -5
- package/dist/types/gl/{Physics.d.ts → physics/Physics.d.ts} +6 -5
- package/dist/types/gl/{Ragdoll.d.ts → physics/Ragdoll.d.ts} +4 -4
- package/dist/types/gl/{Shape.d.ts → physics/Shape.d.ts} +3 -3
- package/dist/types/gl/{Trigger.d.ts → physics/Trigger.d.ts} +2 -2
- package/dist/types/gl/{Terrain.d.ts → terrain/Terrain.d.ts} +6 -6
- package/dist/types/gl/{terrainMesh.d.ts → terrain/terrainMesh.d.ts} +1 -1
- package/dist/types/gl/vehicle/Vehicle.d.ts +300 -0
- package/dist/types/gl/vehicle/Wheel.d.ts +147 -0
- package/dist/types/inject.d.ts +28 -26
- package/dist/types/runtime/files.d.ts +24 -1
- package/dist/types/scene/defineScene.d.ts +10 -3
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/README.md +142 -142
- package/src/bridges.d.ts +1599 -1481
- package/src/canvas/Canvas.ts +21 -0
- package/src/compile/__tests__/compile.test.ts +11 -0
- package/src/compile/header.ts +6 -3
- package/src/compile/index.ts +1 -0
- package/src/compile/sceneEditor.ts +42 -1
- package/src/gl/DecalSet.ts +132 -5
- package/src/gl/Geometry.ts +348 -284
- package/src/gl/Lightmap.ts +32 -5
- package/src/gl/Material.ts +21 -0
- package/src/gl/Mesh.ts +120 -102
- package/src/gl/Model.ts +33 -1
- package/src/gl/Node.ts +1 -1
- package/src/gl/Particles.ts +21 -3
- package/src/gl/Scene.ts +1 -1
- package/src/gl/animation/DynamicBone.ts +459 -0
- package/src/gl/{IK.ts → animation/IK.ts} +4 -4
- package/src/gl/{Locomotion.ts → animation/Locomotion.ts} +7 -7
- package/src/gl/{AudioSource.ts → audio/AudioSource.ts} +7 -7
- package/src/gl/{AudioZone.ts → audio/AudioZone.ts} +75 -75
- package/src/gl/{SceneAudio.ts → audio/SceneAudio.ts} +2 -2
- package/src/gl/{NavAgent.ts → nav/NavAgent.ts} +5 -5
- package/src/gl/{NavMesh.ts → nav/NavMesh.ts} +8 -8
- package/src/gl/{CharacterController.ts → physics/CharacterController.ts} +5 -5
- package/src/gl/{Physics.ts → physics/Physics.ts} +12 -5
- package/src/gl/{Ragdoll.ts → physics/Ragdoll.ts} +8 -6
- package/src/gl/{Shape.ts → physics/Shape.ts} +3 -3
- package/src/gl/{Trigger.ts → physics/Trigger.ts} +45 -45
- package/src/gl/{physicsEvents.ts → physics/physicsEvents.ts} +1 -1
- package/src/gl/{Terrain.ts → terrain/Terrain.ts} +10 -10
- package/src/gl/{terrainMesh.ts → terrain/terrainMesh.ts} +1 -1
- package/src/gl/vehicle/Vehicle.ts +666 -0
- package/src/gl/vehicle/Wheel.ts +290 -0
- package/src/inject.ts +226 -224
- package/src/runtime/files.ts +32 -2
- package/src/scene/defineScene.ts +26 -10
- package/src/scene/level.ts +2 -2
- package/dist/types/gl/Gearbox.d.ts +0 -86
- package/dist/types/gl/Vehicle.d.ts +0 -191
- package/dist/types/gl/Wheel.d.ts +0 -95
- package/src/gl/Gearbox.ts +0 -212
- package/src/gl/Vehicle.ts +0 -473
- package/src/gl/Wheel.ts +0 -240
- /package/dist/types/gl/{physicsEvents.d.ts → physics/physicsEvents.d.ts} +0 -0
package/src/canvas/Canvas.ts
CHANGED
|
@@ -35,6 +35,11 @@ const OP = {
|
|
|
35
35
|
DRAW_IMAGE: 30,
|
|
36
36
|
} as const
|
|
37
37
|
|
|
38
|
+
// Above this many buffered values, a buffer that is STILL growing between bakes is a leak, not a
|
|
39
|
+
// drawing (a dense static picture is a few thousand; a 15 Hz graph without reset() passes this in
|
|
40
|
+
// seconds and doubles its frame cost every minute).
|
|
41
|
+
const CMD_GROWTH_WARN = 20000
|
|
42
|
+
|
|
38
43
|
const ALIGN = { left: 0, center: 1, right: 2, start: 3, end: 4 } as const
|
|
39
44
|
const BASELINE = { alphabetic: 0, top: 1, middle: 2, bottom: 3, hanging: 4, ideographic: 5 } as const
|
|
40
45
|
const JOIN = { miter: 0, round: 1, bevel: 2 } as const
|
|
@@ -76,6 +81,11 @@ export class Canvas {
|
|
|
76
81
|
// also refreshes the on-screen image node(s) via _creatorUI — the UI consumer the 2D/3D texture
|
|
77
82
|
// hooks below don't cover. Stays a bare flag so a non-UI canvas never references the UI bridge.
|
|
78
83
|
private _uiBound = false
|
|
84
|
+
// Growth watch: the command buffer is a RECORDING that every bake replays in full, so a canvas
|
|
85
|
+
// redrawn every frame without `reset()` gets slower with every update. `_bakeSurface` compares the
|
|
86
|
+
// buffer against the previous bake and warns once when it keeps growing past the threshold.
|
|
87
|
+
private _lastBakeLen = 0
|
|
88
|
+
private _warnedGrowth = false
|
|
79
89
|
|
|
80
90
|
constructor(width: number, height: number, opts: { pixelRatio?: number } = {}) {
|
|
81
91
|
this.width = width
|
|
@@ -198,6 +208,17 @@ export class Canvas {
|
|
|
198
208
|
|
|
199
209
|
/** @internal — rasterize the recorded commands into the surface; returns the surface id. */
|
|
200
210
|
_bakeSurface(): number {
|
|
211
|
+
const len = this._cmd.length
|
|
212
|
+
if (!this._warnedGrowth && len > CMD_GROWTH_WARN && this._lastBakeLen > 0 && len > this._lastBakeLen) {
|
|
213
|
+
this._warnedGrowth = true
|
|
214
|
+
console.warn(
|
|
215
|
+
`[canvas] ${this.width}×${this.height}: the command buffer keeps growing (${len} values, ` +
|
|
216
|
+
`${this._lastBakeLen} at the previous bake) — a Canvas records every call and each update() ` +
|
|
217
|
+
`replays all of it, so a redraw that never calls reset() gets slower every frame. ` +
|
|
218
|
+
`Call canvas.reset() before redrawing (or toBitmap() + reset() to flatten a drawing).`,
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
this._lastBakeLen = len
|
|
201
222
|
const buf = new Float32Array(this._cmd)
|
|
202
223
|
this._surfaceId = _creatorCanvas.rasterize(this._surfaceId, this.width, this.height, this.pixelRatio, buf, buf.length, this._refs)
|
|
203
224
|
return this._surfaceId
|
|
@@ -39,6 +39,17 @@ test("a 2D project (new Scene2D) is marked 2d: enable and gl: disable", async ()
|
|
|
39
39
|
expect(js).toContain("_creator2d.createScene")
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
+
test("a 3D project with a Canvas is NOT marked 2d: enable (Canvas keeps a `_creator2d.` texture path)", async () => {
|
|
43
|
+
const js = await compileProject({
|
|
44
|
+
name: "Car",
|
|
45
|
+
publicUrl: "https://le.codes",
|
|
46
|
+
entries: [ { path: "/main.ts", type: "text", text: "const c = new Canvas(64, 64)\nc.update()\nnew Scene().open()\n" } ],
|
|
47
|
+
})
|
|
48
|
+
expect(js).toContain("_creator2d.") // the canvas 2D-texture path survives tree-shaking
|
|
49
|
+
expect(js).not.toContain("// 2d: enable") // …but no Scene2D → the 2D engine is not in use
|
|
50
|
+
expect(js).not.toContain("// gl: disable")
|
|
51
|
+
})
|
|
52
|
+
|
|
42
53
|
test("2D and 3D bundles are DISJOINT (tree-shake drops the other engine)", async () => {
|
|
43
54
|
const twoD = await compileProject({
|
|
44
55
|
name: "2D", publicUrl: "https://le.codes", header: false,
|
package/src/compile/header.ts
CHANGED
|
@@ -33,8 +33,11 @@ export const buildHeader = (name: string, code: string, opts: HeaderOptions = {}
|
|
|
33
33
|
else if (code.includes("_creator.createEngine")) header += `// gl: defer\n`
|
|
34
34
|
|
|
35
35
|
// 2D (creator-2d) — non-exclusive: emitted alongside `gl: disable` so hosts skip Filament and a
|
|
36
|
-
// 2D-aware host brings up the lightweight sokol engine instead.
|
|
37
|
-
|
|
36
|
+
// 2D-aware host brings up the lightweight sokol engine instead. Keyed on the scene constructor
|
|
37
|
+
// (like GL above), NOT on any `_creator2d.` reference: Canvas carries a 2D-texture path
|
|
38
|
+
// (`canvas.texture()` → `_creator2d.createTextureFromCanvas`) that chisel keeps in a pure-3D
|
|
39
|
+
// bundle, and nothing 2D works without a Scene2D anyway.
|
|
40
|
+
if (code.includes("_creator2d.createScene")) header += `// 2d: enable\n`
|
|
38
41
|
|
|
39
42
|
if (code.includes("_creator.createARController")) header += `// ar: enable\n`
|
|
40
43
|
|
|
@@ -61,7 +64,7 @@ export const buildHeader = (name: string, code: string, opts: HeaderOptions = {}
|
|
|
61
64
|
/** Same engine signals the header uses, for callers that need the project kind (e.g. icon type). */
|
|
62
65
|
export const detectProjectKind = (code: string): "ar" | "ui" | "2d" | "3d" => {
|
|
63
66
|
if (code.includes("_creator.createARController")) return "ar"
|
|
64
|
-
if (code.includes("_creator2d.")) return "2d"
|
|
67
|
+
if (code.includes("_creator2d.createScene")) return "2d"
|
|
65
68
|
if (!code.includes("_creator.")) return "ui"
|
|
66
69
|
return "3d"
|
|
67
70
|
}
|
package/src/compile/index.ts
CHANGED
|
@@ -47,10 +47,44 @@ export const loadSceneHarness = (fallbackDir?: string): Record<string, string> =
|
|
|
47
47
|
* the project's file list — `.editor.ts` entries are picked out of it. Compile with
|
|
48
48
|
* `entryOverride: SCENE_ENTRY_PATH`, `header: false`, `format: "iife"`, `editor: true`.
|
|
49
49
|
*/
|
|
50
|
+
/** Resolve a relative import specifier against a project path ("./src/map.scene.ts" + "./map/x.scene"
|
|
51
|
+
* → "./src/map/x.scene"). */
|
|
52
|
+
const resolveRelative = (fromPath: string, spec: string): string => {
|
|
53
|
+
const segs = (p: string) => p.split("/").filter((s) => s !== "" && s !== ".")
|
|
54
|
+
const out = segs(fromPath).slice(0, -1)
|
|
55
|
+
for (const seg of segs(spec)) {
|
|
56
|
+
if (seg === "..") out.pop()
|
|
57
|
+
else out.push(seg)
|
|
58
|
+
}
|
|
59
|
+
return "./" + out.join("/")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The other scene files a scene file imports as prefabs (`import x from './x.scene'`), as project
|
|
64
|
+
* paths present in `projectPaths`. The edit bundle registers their handles BY PATH
|
|
65
|
+
* (`__lecodesSceneFiles`), so the editor can live-patch a `prefab:` node — add, duplicate, undo —
|
|
66
|
+
* without a compile: the doc sees `prefab: X` as an identifier, the host maps it through the
|
|
67
|
+
* file's import table to this path, and the harness resolves the path to the running handle.
|
|
68
|
+
*/
|
|
69
|
+
export const sceneImportsOf = (sceneText: string, scenePath: string, projectPaths: string[]): string[] => {
|
|
70
|
+
const known = new Set(projectPaths)
|
|
71
|
+
const out: string[] = []
|
|
72
|
+
const re = /^\s*import\s+[A-Za-z_$][\w$]*\s+from\s+["']([^"']+\.scene(?:\.ts)?)["']/gm
|
|
73
|
+
for (const m of sceneText.matchAll(re)) {
|
|
74
|
+
const spec = m[1]
|
|
75
|
+
if (!spec.startsWith(".")) continue
|
|
76
|
+
let p = resolveRelative(scenePath, spec)
|
|
77
|
+
if (!p.endsWith(".ts")) p += ".ts"
|
|
78
|
+
if (known.has(p) && !out.includes(p)) out.push(p)
|
|
79
|
+
}
|
|
80
|
+
return out
|
|
81
|
+
}
|
|
82
|
+
|
|
50
83
|
export const sceneEditorEntries = (
|
|
51
84
|
scenePath: string,
|
|
52
85
|
harness: Record<string, string>,
|
|
53
86
|
projectPaths: string[] = [],
|
|
87
|
+
sceneImports: string[] = [],
|
|
54
88
|
): CompileEntry[] => {
|
|
55
89
|
const entries: CompileEntry[] = Object.entries(harness).map(([ name, text ]) => ({
|
|
56
90
|
path: `${SCENE_HARNESS_DIR}/${name}`,
|
|
@@ -74,15 +108,22 @@ export const sceneEditorEntries = (
|
|
|
74
108
|
const materialImports = materialPaths.map((p, i) => `import __mat${i} from "${p}"\n`).join("")
|
|
75
109
|
const materialRegister = materialPaths.length === 0 ? ""
|
|
76
110
|
: `;(globalThis as any).__lecodesMaterials = { ${materialPaths.map((p, i) => `${JSON.stringify(p)}: __mat${i}`).join(", ")} }\n`
|
|
111
|
+
// The scene's prefab imports, registered by project path (see sceneImportsOf) — the same module
|
|
112
|
+
// instances the scene itself imported, so a live-patched prefab node is the same handle.
|
|
113
|
+
const sceneImportLines = sceneImports.map((p, i) => `import __scn${i} from "${p}"\n`).join("")
|
|
114
|
+
const sceneRegister = sceneImports.length === 0 ? ""
|
|
115
|
+
: `;(globalThis as any).__lecodesSceneFiles = { ${sceneImports.map((p, i) => `${JSON.stringify(p)}: __scn${i}`).join(", ")} }\n`
|
|
77
116
|
entries.push({
|
|
78
117
|
path: SCENE_ENTRY_PATH,
|
|
79
118
|
type: "text",
|
|
80
119
|
text: `import "${SCENE_HARNESS_DIR}/.edit-mode.ts"\n`
|
|
81
120
|
+ materialImports
|
|
121
|
+
+ sceneImportLines
|
|
82
122
|
+ `import "${scenePath}"\n`
|
|
83
123
|
+ editorImports
|
|
84
124
|
+ `import "${SCENE_HARNESS_DIR}/main.ts"\n`
|
|
85
|
-
+ materialRegister
|
|
125
|
+
+ materialRegister
|
|
126
|
+
+ sceneRegister,
|
|
86
127
|
})
|
|
87
128
|
return entries
|
|
88
129
|
}
|
package/src/gl/DecalSet.ts
CHANGED
|
@@ -35,6 +35,14 @@ export type DecalOptions = {
|
|
|
35
35
|
fadeIn?: number
|
|
36
36
|
/** Seconds of fade at the end of `life` (ignored with life 0). Default 0. */
|
|
37
37
|
fadeOut?: number
|
|
38
|
+
/** Opacity multipliers at the image's BOTTOM and TOP edge (−Y / +Y), interpolated along it — a
|
|
39
|
+
* tire mark that darkens as the slide deepens. Default `[1, 1]`. */
|
|
40
|
+
gradient?: [number, number]
|
|
41
|
+
/** Tilt of the image's bottom and top edge, as a unit-space slope (Δy per Δx across the box):
|
|
42
|
+
* the edge pivots on the corner of the side it keeps and cuts INTO the box on the other, so two
|
|
43
|
+
* boxes can meet on one shared line — what `DecalTrail` uses to mitre its joints. `0` = square.
|
|
44
|
+
* Default `[0, 0]`. */
|
|
45
|
+
caps?: [number, number]
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
export type DecalSetOptions = DecalOptions & {
|
|
@@ -63,8 +71,10 @@ export type DecalSetOptions = DecalOptions & {
|
|
|
63
71
|
/** HDR boost of the image (0 = none). Default material only. */
|
|
64
72
|
emissive?: number
|
|
65
73
|
name?: string
|
|
66
|
-
/** Coarse draw order, 0 (first) … 7 (last)
|
|
67
|
-
*
|
|
74
|
+
/** Coarse draw order, 0 (first) … 7 (last) — see `Mesh.renderPriority`. Default 3: a decal is
|
|
75
|
+
* part of the surface it sits on, so it draws before every other blended thing (meshes 4,
|
|
76
|
+
* particles 5) — the engine's depth sort compares object centres, and a set spread over the
|
|
77
|
+
* level has no useful centre. */
|
|
68
78
|
renderPriority?: number
|
|
69
79
|
}
|
|
70
80
|
|
|
@@ -86,7 +96,7 @@ export type DecalPlacement = DecalOptions & {
|
|
|
86
96
|
rotation?: QuatLike | Vec3Like
|
|
87
97
|
}
|
|
88
98
|
|
|
89
|
-
const RECORD =
|
|
99
|
+
const RECORD = 27
|
|
90
100
|
const NO_SLOT = 0xFFFFFFFF
|
|
91
101
|
|
|
92
102
|
export class DecalSet extends Node {
|
|
@@ -116,7 +126,7 @@ export class DecalSet extends Node {
|
|
|
116
126
|
}
|
|
117
127
|
if (_creator.createDecalSet) {
|
|
118
128
|
_creator.createDecalSet(this.id, Material.idOf(this._material), this._max)
|
|
119
|
-
|
|
129
|
+
this.renderPriority = options.renderPriority ?? 3
|
|
120
130
|
} else if (!DecalSet._warned) {
|
|
121
131
|
DecalSet._warned = true
|
|
122
132
|
console.warn("[decals] this host has no createDecalSet — decals are not drawn")
|
|
@@ -200,7 +210,8 @@ export class DecalSet extends Node {
|
|
|
200
210
|
return this._write(slot, right, up, normal, new Vec3(p.position), p)
|
|
201
211
|
}
|
|
202
212
|
|
|
203
|
-
|
|
213
|
+
/** @internal One decal's frame → record → host (`DecalTrail` writes its segments through here). */
|
|
214
|
+
_write(slot: number, right: Vec3, up: Vec3, normal: Vec3, centre: Vec3, o: DecalOptions): number {
|
|
204
215
|
const d = this._defaults
|
|
205
216
|
const size = o.size ?? d.size!
|
|
206
217
|
const w = typeof size === "number" ? size : size[0]
|
|
@@ -222,6 +233,10 @@ export class DecalSet extends Node {
|
|
|
222
233
|
const opacity = o.opacity ?? d.opacity!
|
|
223
234
|
r[16] = tr; r[17] = tg; r[18] = tb; r[19] = ta * opacity
|
|
224
235
|
r[20] = o.life ?? d.life!; r[21] = o.fadeIn ?? d.fadeIn!; r[22] = o.fadeOut ?? d.fadeOut!
|
|
236
|
+
const caps = o.caps ?? d.caps
|
|
237
|
+
r[23] = caps ? caps[0] : 0; r[24] = caps ? caps[1] : 0
|
|
238
|
+
const gradient = o.gradient ?? d.gradient
|
|
239
|
+
r[25] = gradient ? gradient[0] : 1; r[26] = gradient ? gradient[1] : 1
|
|
225
240
|
if (slot === NO_SLOT) {
|
|
226
241
|
if (!_creator.addDecal) return -1
|
|
227
242
|
const s = _creator.addDecal(this.id, r)
|
|
@@ -231,3 +246,115 @@ export class DecalSet extends Node {
|
|
|
231
246
|
return slot
|
|
232
247
|
}
|
|
233
248
|
}
|
|
249
|
+
|
|
250
|
+
/** A `DecalTrail`'s look: the strip's width, and what every segment carries. */
|
|
251
|
+
export type DecalTrailOptions = Omit<DecalOptions, "size" | "caps" | "gradient"> & {
|
|
252
|
+
/** Width of the strip on the surface, world units. */
|
|
253
|
+
width: number
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// The mitre stops making sense past this turn between two segments (a sharp reversal would need a
|
|
257
|
+
// box longer than the strip is wide); the trail breaks there and starts afresh.
|
|
258
|
+
const TRAIL_MAX_SLOPE = 1.75 // tan 60°
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* A continuous strip of decals along a path — a tire's skid mark, a dragged body, a tread track.
|
|
262
|
+
* Feed it points (`add`) as the thing moves; every new point becomes one box-decal SEGMENT from the
|
|
263
|
+
* previous point, its image's up along the travel, and the joints between segments are MITRED: each
|
|
264
|
+
* box is cut along the bisector it shares with its neighbour (`caps`), so the strip has no overlaps
|
|
265
|
+
* darkening the outside of a bend and no wedges of gap on the inside. The opacity given with each
|
|
266
|
+
* point is interpolated along the segment (`gradient`), so a mark can darken as a slide deepens and
|
|
267
|
+
* fade as it ends. Segments are the set's decals — same ring, same `max` budget: a set of 900 holds
|
|
268
|
+
* 900 segments across every trail drawn from it.
|
|
269
|
+
*
|
|
270
|
+
* The previous segment is REWRITTEN when the next point arrives (its far cap becomes the shared
|
|
271
|
+
* mitre), through `DecalSet.update`'s path — a recycled slot is simply left alone by the engine.
|
|
272
|
+
*/
|
|
273
|
+
export class DecalTrail {
|
|
274
|
+
private _prev: { point: Vec3, opacity: number } | null = null
|
|
275
|
+
private _seg: {
|
|
276
|
+
slot: number, start: Vec3, end: Vec3, dir: Vec3, right: Vec3, normal: Vec3,
|
|
277
|
+
slopeStart: number, a0: number, a1: number,
|
|
278
|
+
} | null = null
|
|
279
|
+
|
|
280
|
+
readonly set: DecalSet
|
|
281
|
+
readonly options: DecalTrailOptions
|
|
282
|
+
|
|
283
|
+
constructor(set: DecalSet, options: DecalTrailOptions) {
|
|
284
|
+
this.set = set
|
|
285
|
+
this.options = options
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Extend the strip to `point` on a surface with `normal`, `opacity` (0..1) at that point. The
|
|
289
|
+
* first call after a start / `end()` only anchors the strip. Returns the new segment's slot, or
|
|
290
|
+
* −1 when nothing was drawn (the anchor, a point that did not move, no host support). */
|
|
291
|
+
add(point: Vec3Like, normal: Vec3Like, opacity = 1): number {
|
|
292
|
+
const p = new Vec3(point)
|
|
293
|
+
const n = new Vec3(normal).normalize()
|
|
294
|
+
const prev = this._prev
|
|
295
|
+
this._prev = { point: p, opacity }
|
|
296
|
+
if (!prev) return -1
|
|
297
|
+
// The segment, flattened onto the surface plane.
|
|
298
|
+
let d = p.sub(prev.point)
|
|
299
|
+
d = d.sub(n.scale(d.dot(n)))
|
|
300
|
+
const len = d.length()
|
|
301
|
+
if (len < 1e-3) { this._prev = prev; return -1 } // not moved: wait for a real step
|
|
302
|
+
const dir = d.scale(1 / len)
|
|
303
|
+
const right = dir.cross(n) // DecalSet.spawn's frame: right × up = normal, image unmirrored from the normal's side
|
|
304
|
+
const w = this.options.width
|
|
305
|
+
|
|
306
|
+
// Mitre with the previous segment: the joint's bisector is the sum of the two right normals,
|
|
307
|
+
// and each box takes it as a cap — the previous one at its END (rewritten now), this one at its
|
|
308
|
+
// START — with the slopes measured in each box's own frame. A trail's first segment, or one
|
|
309
|
+
// after a break, starts square.
|
|
310
|
+
let slopeStart = 0
|
|
311
|
+
const last = this._seg
|
|
312
|
+
if (last) {
|
|
313
|
+
const lastDir = last.dir.sub(n.scale(last.dir.dot(n))).normalize()
|
|
314
|
+
const lastRight = lastDir.cross(n)
|
|
315
|
+
const m = lastRight.add(right)
|
|
316
|
+
const denom = m.dot(right)
|
|
317
|
+
const slopeEnd = Math.abs(denom) > 1e-4 ? m.dot(lastDir) / denom : Infinity
|
|
318
|
+
if (Math.abs(slopeEnd) <= TRAIL_MAX_SLOPE) {
|
|
319
|
+
this._rewrite(last, slopeEnd)
|
|
320
|
+
slopeStart = -slopeEnd
|
|
321
|
+
} else {
|
|
322
|
+
this._rewrite(last, 0) // too sharp to mitre: leave the last one square, start afresh
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const seg = { slot: -1, start: prev.point, end: p, dir, right, normal: n, slopeStart, a0: prev.opacity, a1: opacity }
|
|
326
|
+
seg.slot = this._box(seg, 0, -1)
|
|
327
|
+
this._seg = seg
|
|
328
|
+
return seg.slot
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Finish the strip. With a `point`, one last segment runs out to it at opacity 0 — a mark that
|
|
332
|
+
* tapers away instead of stopping dead. The next `add` anchors a new strip. */
|
|
333
|
+
end(point?: Vec3Like, normal: Vec3Like = Vec3.up): this {
|
|
334
|
+
if (point && this._prev) this.add(point, normal, 0)
|
|
335
|
+
this._prev = null
|
|
336
|
+
this._seg = null
|
|
337
|
+
return this
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private _rewrite(seg: NonNullable<DecalTrail["_seg"]>, slopeEnd: number): void {
|
|
341
|
+
if (seg.slot >= 0) this._box(seg, slopeEnd, seg.slot)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Write one segment's box: the strip between its joints, extended past each mitred joint by the
|
|
345
|
+
* mitre's reach (w/2 · |slope|) so the tilted cap still passes through the joint's centre. */
|
|
346
|
+
private _box(seg: NonNullable<DecalTrail["_seg"]>, slopeEnd: number, slot: number): number {
|
|
347
|
+
const w = this.options.width
|
|
348
|
+
const L = seg.end.sub(seg.start).length()
|
|
349
|
+
const extStart = 0.5 * w * Math.abs(seg.slopeStart)
|
|
350
|
+
const extEnd = 0.5 * w * Math.abs(slopeEnd)
|
|
351
|
+
const boxLen = L + extStart + extEnd
|
|
352
|
+
const centre = seg.start.add(seg.dir.scale(0.5 * boxLen - extStart))
|
|
353
|
+
// World slopes → unit-space slopes: x is measured in widths, y in box lengths.
|
|
354
|
+
const caps: [number, number] = [ seg.slopeStart * (w / boxLen), slopeEnd * (w / boxLen) ]
|
|
355
|
+
const { width: _w, ...rest } = this.options
|
|
356
|
+
const s = this.set._write(slot < 0 ? NO_SLOT : slot, seg.right, seg.dir, seg.normal, centre,
|
|
357
|
+
{ ...rest, size: [ w, boxLen ], caps, gradient: [ seg.a0, seg.a1 ] })
|
|
358
|
+
return slot < 0 ? s : slot
|
|
359
|
+
}
|
|
360
|
+
}
|