lecodes-sdk 2.8.0 → 2.8.2
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 +4 -0
- package/dist/types/core/Aspect.d.ts +7 -0
- package/dist/types/gl/Statics.d.ts +12 -1
- package/dist/types/gl/physics/CharacterController.d.ts +9 -4
- package/dist/types/gl/physics/Physics.d.ts +7 -1
- package/dist/types/gl/physics/Shape.d.ts +4 -0
- package/dist/types/inject.d.ts +1 -0
- package/dist/types/plugin.d.ts +1 -0
- package/dist/types/plugins/gen/ar/sdk/ar.d.ts +25 -5
- package/dist/types/runtime/gestures.d.ts +52 -0
- package/dist/types/ui/UIModal.d.ts +5 -3
- package/dist/types/version.d.ts +1 -1
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/src/bridges/gl.d.ts +7 -0
- package/src/bridges/tree.d.ts +14 -4
- package/src/core/Aspect.ts +7 -0
- package/src/gl/Node.ts +21 -2
- package/src/gl/Statics.ts +48 -2
- package/src/gl/physics/CharacterController.ts +12 -5
- package/src/gl/physics/Physics.ts +17 -3
- package/src/gl/physics/Shape.ts +29 -9
- package/src/gl/physics/physicsEvents.ts +9 -4
- package/src/gl/touch.ts +4 -1
- package/src/inject.ts +2 -0
- package/src/plugin.ts +3 -1
- package/src/plugins/gen/ar/sdk/ar.ts +59 -14
- package/src/runtime/gestures.ts +190 -0
- package/src/scene/defineScene.ts +5 -2
- package/src/scene/grammar.ts +10 -2
- package/src/ui/UIModal.ts +44 -9
- package/src/ui/UINode.ts +4 -0
- package/src/ui/tree.ts +8 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// trackGesture: one recognizer for pan, pinch and twist, built on the per-pointer tracks. Every
|
|
2
|
+
// host already sends a touchstart per finger and the SDK keeps a TrackHandler per pointer id
|
|
3
|
+
// (ui/tree.ts, gestures; g2/touch.ts for 2D scenes): this module only listens for those
|
|
4
|
+
// touchstarts, `ev.track(...)`s each finger with its own handler, and turns the live set of
|
|
5
|
+
// fingers into one reading. No runtime, contract or host part; the AR plugin's `attachControls`
|
|
6
|
+
// (plugins/ar/sdk/controls.ts) is the same thing done by hand.
|
|
7
|
+
//
|
|
8
|
+
// Two target shapes, one adapter (`listen`):
|
|
9
|
+
// - `onTouchStart(cb)` — UIButton, UIScreen, UIWidget. Such a listener cannot be removed:
|
|
10
|
+
// `remove()` leaves the callback in place and it ignores every event from then on.
|
|
11
|
+
// - `addEventListener("touchstart", cb)` / `removeEventListener` — Scene, Scene2D, Node,
|
|
12
|
+
// Node2D. `remove()` takes the listener off.
|
|
13
|
+
//
|
|
14
|
+
// The reading (`Gesture`, a fresh object per callback — safe to keep) is an INCREMENT, like a
|
|
15
|
+
// track's own deltaX / deltaY: every onMove reports the change since the previous onMove (or since
|
|
16
|
+
// the gesture started), so applying it to the current camera / transform is one line and there is
|
|
17
|
+
// no start state to keep. Moves arrive per finger, so increments compose: over any run of moves
|
|
18
|
+
// the deltas sum and the scales multiply to the change of the whole set (rotations sum exactly for
|
|
19
|
+
// two fingers, closely for more). Only positions are read, never a track's deltas (the web's are
|
|
20
|
+
// measured from the touch-down).
|
|
21
|
+
// centerX / centerY the centroid of the live fingers (the event's client coordinates, logical px)
|
|
22
|
+
// deltaX / deltaY the centroid's move since the previous onMove
|
|
23
|
+
// scale the mean finger distance to the centroid over the previous one
|
|
24
|
+
// rotation the mean change of each finger's angle about the centroid, radians;
|
|
25
|
+
// positive = clockwise on screen (y grows downward, so atan2(dy, dx) increasing)
|
|
26
|
+
// pointers the number of live fingers
|
|
27
|
+
// One finger is a gesture too: its moves are pans with scale 1 and rotation 0 (a finger AT the
|
|
28
|
+
// centroid has no distance and no angle — that, not a finger count, is what makes them neutral;
|
|
29
|
+
// three and more fingers run the same formulas). A finger joining or leaving re-bases silently: the
|
|
30
|
+
// set's previous positions are its current ones, so the next onMove shows no jump. onStart (the
|
|
31
|
+
// first finger) and onEnd (the last one lifted: the centroid is its end position, pointers 0) carry
|
|
32
|
+
// deltas 0, scale 1, rotation 0. A cancel of any finger cancels the whole gesture: onCancel once,
|
|
33
|
+
// then the remaining fingers are ignored (and a finger landing meanwhile is not taken) until all
|
|
34
|
+
// have lifted; the next touch starts a new gesture. `claim` (default true) goes to every finger's
|
|
35
|
+
// TrackHandler, so a scrollable above never takes the pan.
|
|
36
|
+
|
|
37
|
+
import type { TouchStartEvent, TrackHandler } from "./touch"
|
|
38
|
+
|
|
39
|
+
/** One reading of a gesture, handed to `onStart` / `onMove` / `onEnd` (a fresh object each time). */
|
|
40
|
+
export interface Gesture {
|
|
41
|
+
/** The centroid of the live fingers, in the event's client coordinates (logical px). */
|
|
42
|
+
centerX: number
|
|
43
|
+
centerY: number
|
|
44
|
+
/** The centroid's move since the previous `onMove` (0 on start / end). */
|
|
45
|
+
deltaX: number
|
|
46
|
+
deltaY: number
|
|
47
|
+
/** Mean finger distance to the centroid over the previous one (1 with one finger, 1 on start / end). */
|
|
48
|
+
scale: number
|
|
49
|
+
/** The mean change of each finger's angle about the centroid since the previous `onMove`, radians,
|
|
50
|
+
* positive = clockwise on screen (0 with one finger, 0 on start / end). */
|
|
51
|
+
rotation: number
|
|
52
|
+
/** The number of live fingers (0 in `onEnd`: the last one lifted). */
|
|
53
|
+
pointers: number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** What `trackGesture` takes: the gesture's callbacks and the claim every finger's track makes. */
|
|
57
|
+
export interface GestureHandler {
|
|
58
|
+
/** Passed to every finger's `ev.track(...)` (default `true`: a scrollable above never takes the pan). */
|
|
59
|
+
claim?: TrackHandler["claim"]
|
|
60
|
+
/** The first finger landed. */
|
|
61
|
+
onStart?(g: Gesture): void
|
|
62
|
+
/** Any finger moved: the change since the previous `onMove`. */
|
|
63
|
+
onMove?(g: Gesture): void
|
|
64
|
+
/** The last finger lifted. */
|
|
65
|
+
onEnd?(g: Gesture): void
|
|
66
|
+
/** The host cancelled one of the fingers: the whole gesture is over. */
|
|
67
|
+
onCancel?(): void
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Anything that hands out touchstarts: a UI element (`onTouchStart`) or a scene / node
|
|
71
|
+
* (`addEventListener("touchstart", …)`). */
|
|
72
|
+
export type GestureTarget =
|
|
73
|
+
| { onTouchStart(callback: (ev: TouchStartEvent<any>) => void): unknown }
|
|
74
|
+
| {
|
|
75
|
+
addEventListener(channel: "touchstart", callback: (ev: TouchStartEvent<any>) => void): void
|
|
76
|
+
removeEventListener(channel: "touchstart", callback: (ev: TouchStartEvent<any>) => void): void
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type Finger = { id: number, x: number, y: number }
|
|
80
|
+
|
|
81
|
+
// A finger this close to the centroid (logical px) has no angle, and a set this tight has no scale.
|
|
82
|
+
const EPS = 1e-3
|
|
83
|
+
|
|
84
|
+
/** Start listening on `target`; returns the off switch. */
|
|
85
|
+
const listen = (target: GestureTarget, cb: (ev: TouchStartEvent<any>) => void): (() => void) => {
|
|
86
|
+
if ("addEventListener" in target) {
|
|
87
|
+
target.addEventListener("touchstart", cb)
|
|
88
|
+
return () => target.removeEventListener("touchstart", cb)
|
|
89
|
+
}
|
|
90
|
+
target.onTouchStart(cb)
|
|
91
|
+
return () => {}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Follow pan, pinch and twist on `target` as one gesture: every finger that lands on it is tracked,
|
|
96
|
+
* and `onMove` reports the change of the whole set since the previous move — the centroid's move,
|
|
97
|
+
* the spread's ratio, the twist's angle. One finger is a gesture too (a pan, scale 1, rotation 0).
|
|
98
|
+
*
|
|
99
|
+
* ```ts
|
|
100
|
+
* trackGesture(scene, {
|
|
101
|
+
* onMove(g) { zoom *= g.scale; angle += g.rotation; panX += g.deltaX; panY += g.deltaY },
|
|
102
|
+
* })
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export const trackGesture = (target: GestureTarget, handler: GestureHandler): { remove(): void } => {
|
|
106
|
+
const fingers: Finger[] = []
|
|
107
|
+
let cancelled = false // a finger was cancelled: ignore the rest until all have lifted
|
|
108
|
+
let removed = false
|
|
109
|
+
|
|
110
|
+
const drop = (f: Finger): boolean => {
|
|
111
|
+
const i = fingers.indexOf(f)
|
|
112
|
+
if (i < 0) return false
|
|
113
|
+
fingers.splice(i, 1)
|
|
114
|
+
if (fingers.length === 0) cancelled = false
|
|
115
|
+
return true
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// One finger moved from where it was to (x, y): the set's change, before → after.
|
|
119
|
+
const step = (f: Finger, x: number, y: number): Gesture => {
|
|
120
|
+
const n = fingers.length
|
|
121
|
+
let sx = 0, sy = 0
|
|
122
|
+
for (const p of fingers) { sx += p.x; sy += p.y }
|
|
123
|
+
const c0x = sx / n, c0y = sy / n
|
|
124
|
+
const c1x = (sx - f.x + x) / n, c1y = (sy - f.y + y) / n
|
|
125
|
+
let d0 = 0, d1 = 0, turn = 0, turned = 0
|
|
126
|
+
for (const p of fingers) {
|
|
127
|
+
const ax = p.x - c0x, ay = p.y - c0y
|
|
128
|
+
const bx = (p === f ? x : p.x) - c1x, by = (p === f ? y : p.y) - c1y
|
|
129
|
+
const r0 = Math.hypot(ax, ay), r1 = Math.hypot(bx, by)
|
|
130
|
+
d0 += r0
|
|
131
|
+
d1 += r1
|
|
132
|
+
if (r0 > EPS && r1 > EPS) {
|
|
133
|
+
let a = Math.atan2(by, bx) - Math.atan2(ay, ax)
|
|
134
|
+
if (a > Math.PI) a -= 2 * Math.PI
|
|
135
|
+
else if (a <= -Math.PI) a += 2 * Math.PI
|
|
136
|
+
turn += a
|
|
137
|
+
turned++
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
f.x = x
|
|
141
|
+
f.y = y
|
|
142
|
+
return {
|
|
143
|
+
centerX: c1x, centerY: c1y, deltaX: c1x - c0x, deltaY: c1y - c0y,
|
|
144
|
+
scale: d0 > EPS * n ? d1 / d0 : 1, rotation: turned ? turn / turned : 0, pointers: n,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const onTouch = (ev: TouchStartEvent<any>): void => {
|
|
149
|
+
if (removed || cancelled) return
|
|
150
|
+
// A pointer id already in the set never got its end (its tracker was replaced): the new touch
|
|
151
|
+
// takes its place in the same gesture.
|
|
152
|
+
const stale = fingers.findIndex((p) => p.id === ev.pointerId)
|
|
153
|
+
if (stale >= 0) fingers.splice(stale, 1)
|
|
154
|
+
const first = fingers.length === 0 && stale < 0
|
|
155
|
+
const f: Finger = { id: ev.pointerId, x: ev.clientX, y: ev.clientY }
|
|
156
|
+
fingers.push(f)
|
|
157
|
+
ev.track({
|
|
158
|
+
claim: handler.claim ?? true,
|
|
159
|
+
// only when the gesture has an onMove: otherwise the host never crosses into JS for the moves
|
|
160
|
+
onMove: handler.onMove ? (pos) => {
|
|
161
|
+
if (removed || cancelled || fingers.indexOf(f) < 0) return
|
|
162
|
+
handler.onMove?.(step(f, pos.clientX, pos.clientY))
|
|
163
|
+
} : undefined,
|
|
164
|
+
onEnd: (pos) => {
|
|
165
|
+
if (removed) return
|
|
166
|
+
const live = !cancelled
|
|
167
|
+
if (!drop(f) || !live || fingers.length > 0) return
|
|
168
|
+
handler.onEnd?.({ centerX: pos.clientX, centerY: pos.clientY, deltaX: 0, deltaY: 0, scale: 1, rotation: 0, pointers: 0 })
|
|
169
|
+
},
|
|
170
|
+
onCancel: () => {
|
|
171
|
+
if (removed || fingers.indexOf(f) < 0) return
|
|
172
|
+
const was = cancelled
|
|
173
|
+
cancelled = true
|
|
174
|
+
drop(f)
|
|
175
|
+
if (!was) handler.onCancel?.()
|
|
176
|
+
},
|
|
177
|
+
})
|
|
178
|
+
if (first) handler.onStart?.({ centerX: f.x, centerY: f.y, deltaX: 0, deltaY: 0, scale: 1, rotation: 0, pointers: 1 })
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const off = listen(target, onTouch)
|
|
182
|
+
return {
|
|
183
|
+
remove() {
|
|
184
|
+
if (removed) return
|
|
185
|
+
removed = true
|
|
186
|
+
fingers.length = 0
|
|
187
|
+
off()
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
}
|
package/src/scene/defineScene.ts
CHANGED
|
@@ -53,7 +53,7 @@ import { Model, foliageKindOf, type FoliageKind } from "../gl/Model"
|
|
|
53
53
|
import { Physics } from "../gl/physics/Physics"
|
|
54
54
|
import { CharacterController } from "../gl/physics/CharacterController"
|
|
55
55
|
import { Lightmap, type LightmapDef, type LightmapPrepared } from "../gl/Lightmap"
|
|
56
|
-
import { coveredRoots, defsDigest, bakeDigest, findNode, open as staticsOpen, build as staticsBuild, snapshot as staticsSnapshot, staticsConfig, toolRun, type CoveredRoot, type OpenedCache } from "../gl/Statics"
|
|
56
|
+
import { coveredRoots, defsDigest, bakeDigest, findNode, open as staticsOpen, build as staticsBuild, snapshot as staticsSnapshot, staticsConfig, toolRun, attachPassive, type CoveredRoot, type OpenedCache } from "../gl/Statics"
|
|
57
57
|
import { Foliage, type FoliageOptions } from "../gl/Foliage"
|
|
58
58
|
import { NavMesh } from "../gl/nav/NavMesh"
|
|
59
59
|
import { Terrain, type TerrainLayer, type TerrainRegion, type TerrainOptions } from "../gl/terrain/Terrain"
|
|
@@ -918,6 +918,9 @@ const buildNodes = async (
|
|
|
918
918
|
if (parentNode) attachHost(parentNode, nd, path).add(node)
|
|
919
919
|
scene.add(node)
|
|
920
920
|
applyNode(node, name, nd)
|
|
921
|
+
// a root with a model is itself a placement: its clone takes the tags on the first hit (Statics.ts). A group
|
|
922
|
+
// root has no clone — its tags go here, one `parent` up from a hit on a wall under it
|
|
923
|
+
if (nd.model === undefined) attachPassive(node, nd)
|
|
921
924
|
cov.node = node
|
|
922
925
|
nodes[path] = node
|
|
923
926
|
return
|
|
@@ -1028,7 +1031,7 @@ const instantiate = async (def: SceneDef, editorRuns: EditorRun[]): Promise<{ sc
|
|
|
1028
1031
|
let cachedMade = 0
|
|
1029
1032
|
if (opened) {
|
|
1030
1033
|
const pages = prepared ? Lightmap.pageIds(prepared) : new Uint32Array(0)
|
|
1031
|
-
cachedMade = await staticsBuild(opened, scene, roots.map((r) => covered.get(r.path)!.node!), pages, templateModel)
|
|
1034
|
+
cachedMade = await staticsBuild(opened, scene, roots.map((r) => covered.get(r.path)!.node!), pages, templateModel, roots, def)
|
|
1032
1035
|
}
|
|
1033
1036
|
const nodesMs = Date.now() - tLoad
|
|
1034
1037
|
if (navmesh && !(EDITOR && isEditMode())) registerNavOverrides(def.nodes, nodes)
|
package/src/scene/grammar.ts
CHANGED
|
@@ -17,8 +17,16 @@ export type AspectEntry<A extends Aspect<any, any> = Aspect<any, any>> = {
|
|
|
17
17
|
|
|
18
18
|
/** Reference an aspect in a scene file: `aspects: [use(Physics, { motion: 'static' })]`. Props are
|
|
19
19
|
* typechecked against the aspect's fields, exactly like `node.aspect(Ctor, props)`. */
|
|
20
|
-
export const use = <A extends Aspect<any, any>>(ctor: AspectCtor<A>, props?: Partial<A>): AspectEntry<A> =>
|
|
21
|
-
(
|
|
20
|
+
export const use = <A extends Aspect<any, any>>(ctor: AspectCtor<A>, props?: Partial<A>): AspectEntry<A> => {
|
|
21
|
+
// a passive aspect (Aspect.passive) is data: the level cache attaches it on the first hit, so an update() on it
|
|
22
|
+
// would never have run from the level's first frame — refused here, at the scene's own line, not in a raycast
|
|
23
|
+
if ((ctor as { passive?: boolean }).passive === true) {
|
|
24
|
+
const proto = (ctor as unknown as { prototype: Record<string, unknown> }).prototype
|
|
25
|
+
if (typeof proto.update === "function" || typeof proto.updateBefore === "function" || typeof proto.updateFixed === "function")
|
|
26
|
+
throw new Error(`use(${(ctor as { name?: string }).name ?? "?"}): a passive aspect carries data only — it cannot have update(); drop \`static passive\` or the update`)
|
|
27
|
+
}
|
|
28
|
+
return { __use: true, ctor, props }
|
|
29
|
+
}
|
|
22
30
|
|
|
23
31
|
/** A `make(fn, args)` source entry — see {@link make}. `N` is the node family the factory returns
|
|
24
32
|
* (3D `Node` by default; a `.scene2d.ts` file's factories return `Node2D`). */
|
package/src/ui/UIModal.ts
CHANGED
|
@@ -23,6 +23,14 @@ export type UIModalShow = (() => boolean) | Signal<boolean>
|
|
|
23
23
|
const MODAL_ANIM_MS = 200
|
|
24
24
|
const SHOW_BINDING = "#show"
|
|
25
25
|
|
|
26
|
+
// The transition period's `display` on a modal: said once per run, not per write.
|
|
27
|
+
let displayWarned = false
|
|
28
|
+
function warnDisplayDeprecated(): void {
|
|
29
|
+
if (displayWarned) return
|
|
30
|
+
displayWarned = true
|
|
31
|
+
console.warn("deprecated: a modal's `display` — show it with show() / hide(), or bind it with bindShow(() => …) (+ onDismiss for the scrim tap / back); `display` on a modal will throw again")
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
/**
|
|
27
35
|
* A dialog: a `UIWidget` with the modal boilerplate built in. Comes with a scrim
|
|
28
36
|
* (`overlayColor: "rgba(0, 0, 0, 0.5)"` unless overridden), is HIDDEN until shown, animates on
|
|
@@ -38,9 +46,10 @@ const SHOW_BINDING = "#show"
|
|
|
38
46
|
* request, `onDismiss`. By default it hides an unbound modal and writes `false` into a bound
|
|
39
47
|
* signal; a bound function has nowhere to write, so a modal bound to one without `onDismiss`
|
|
40
48
|
* cannot be dismissed (a forced choice — the same as `dismissible(false)`). `display` is not a
|
|
41
|
-
* modal's switch
|
|
42
|
-
*
|
|
43
|
-
*
|
|
49
|
+
* modal's switch (a bound display died on the first dismissal, which wrote a static
|
|
50
|
+
* `display: "none"` over the binding — the dialog never opened again). Deprecated, for the
|
|
51
|
+
* transition period a `.style({ display })` is still accepted, with a warning: a function becomes
|
|
52
|
+
* `bindShow(() => fn() !== "none")`, a value a show() / hide(); it will throw again.
|
|
44
53
|
*/
|
|
45
54
|
export interface UIModal extends UIWidget {
|
|
46
55
|
/**
|
|
@@ -107,14 +116,40 @@ export class ModalElement extends WidgetElement {
|
|
|
107
116
|
this._syncPose()
|
|
108
117
|
}
|
|
109
118
|
|
|
110
|
-
/** @internal `display` is not a modal's switch
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
119
|
+
/** @internal `display` is not a modal's switch — DEPRECATED, accepted for the transition period
|
|
120
|
+
* and translated: a function → `bindShow(() => fn() !== "none")`, a value → the start state of
|
|
121
|
+
* an unplaced modal or a show() / hide() of a placed one (replacing a binding, as a static write
|
|
122
|
+
* did). The widget's own writes (`_ownWrite`) pass untouched. During the constructor the field is
|
|
123
|
+
* still undefined (it is initialised after `super()`), which lets the default `display: "none"`
|
|
124
|
+
* through. */
|
|
114
125
|
_mapStyle(style: any): void {
|
|
115
|
-
if ("display" in style
|
|
116
|
-
|
|
126
|
+
if (!("display" in style) || this._ownWrite !== false) return
|
|
127
|
+
const value = style.display
|
|
128
|
+
delete style.display
|
|
129
|
+
warnDisplayDeprecated()
|
|
130
|
+
if (typeof value === "function") { this._bindDisplay(value); return }
|
|
131
|
+
if (this._bound) {
|
|
132
|
+
disposeBinding(this, SHOW_BINDING)
|
|
133
|
+
this._bound = false
|
|
134
|
+
this._showSignal = undefined
|
|
135
|
+
this._syncDismissible()
|
|
117
136
|
}
|
|
137
|
+
const shown = value !== "none"
|
|
138
|
+
if (this._h.id === 0) return
|
|
139
|
+
if (tree().parent(this._h.id) === 0 && !this._shown) this._writeDisplay(shown ? "flex" : "none") // the start state
|
|
140
|
+
else this._setOpen(shown)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** @internal `el.style.display = () => …` (the style proxy's binding form) — DEPRECATED, as `_mapStyle`. */
|
|
144
|
+
_bindStyle(key: string, fn: () => any): boolean {
|
|
145
|
+
if (key !== "display") return false
|
|
146
|
+
warnDisplayDeprecated()
|
|
147
|
+
this._bindDisplay(fn)
|
|
148
|
+
return true
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private _bindDisplay(fn: () => any): void {
|
|
152
|
+
this.bindShow(() => fn() !== "none")
|
|
118
153
|
}
|
|
119
154
|
|
|
120
155
|
// The pose the runtime plays on a display flip: the hidden pose + its duration, plus the scrim
|
package/src/ui/UINode.ts
CHANGED
|
@@ -399,6 +399,7 @@ const handler: ProxyHandler<any> = {
|
|
|
399
399
|
// dispose the binding (this is the hot path) — override statically via `.style({ prop })`.
|
|
400
400
|
const el = target._el
|
|
401
401
|
const fn = value as unknown as () => any
|
|
402
|
+
if (el._bindStyle?.(key, fn) === true) return true // the element took the binding itself
|
|
402
403
|
createBinding(el, key, () => { el.style[key] = fn() })
|
|
403
404
|
return true
|
|
404
405
|
}
|
|
@@ -518,6 +519,9 @@ export class Element<T extends string> implements TreeElement {
|
|
|
518
519
|
* `type: "phone"` → `"tel"`), mutating the style in place before it is stored or sent. Runs on
|
|
519
520
|
* every entry point: the constructor's style, `.style()`, and single-key `el.style.k =` writes. */
|
|
520
521
|
_mapStyle?(style: any): void
|
|
522
|
+
/** @internal A function written as `el.style.key = () => …`: true when the element bound it
|
|
523
|
+
* itself (a modal's deprecated `display`), false for the ordinary per-key binding. */
|
|
524
|
+
_bindStyle?(key: string, fn: () => any): boolean
|
|
521
525
|
|
|
522
526
|
// Per-type default styles are inlined at each component's construction site (spread UNDER the
|
|
523
527
|
// user's style — e.g. UIRow's `flexDirection: "row"`), so every node reaches the host with
|
package/src/ui/tree.ts
CHANGED
|
@@ -233,6 +233,14 @@ export const unregisterNativeView = (viewId: number): void => { nativeViews.dele
|
|
|
233
233
|
// ---- gestures -----------------------------------------------------------------------------------------------
|
|
234
234
|
// The track handler of every pointer a listener claimed (`ev.track(...)`): the runtime feeds the
|
|
235
235
|
// pointer's moves / end / cancel here by pointer id — nothing of it is retained natively.
|
|
236
|
+
//
|
|
237
|
+
// Several fingers on one node — the rule every host keeps (renderers/uikit + renderers/android
|
|
238
|
+
// TouchPipeline, renderers/tgfx ui_internal.h Gesture, renderers/web-lite events.ts, web-canvas
|
|
239
|
+
// TreeUI): every finger is its own pointer — its own touchStart and so its own `ev.track`, its own
|
|
240
|
+
// moves and end / cancel at its own point, keyed here by its id. The click and the long press are the
|
|
241
|
+
// node's PRIMARY finger's, the one that pressed it while no other held it: a two-finger tap is ONE
|
|
242
|
+
// click, a second finger landing cancels a hold still pending (a pinch is not a hold). `$pressed`
|
|
243
|
+
// holds until the last finger lifts.
|
|
236
244
|
const trackers = new Map<number, TrackLike>()
|
|
237
245
|
|
|
238
246
|
// The hover's own slot — the cursor is one, and it is NOT a gesture: a mouse drag keeps its touch
|
package/src/version.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// same manifest). Its MAJOR is the bundle ↔ runtime contract: a bundle's `// sdk:` header line
|
|
5
5
|
// and a host's embedded version must agree on the major for the host to run the bundle; minor
|
|
6
6
|
// and patch never gate anything (features are detected, never versioned).
|
|
7
|
-
export const SDK_VERSION = "2.8.
|
|
7
|
+
export const SDK_VERSION = "2.8.2"
|
|
8
8
|
|
|
9
9
|
/** The major of a semver string, or null when it is not one. */
|
|
10
10
|
export const sdkMajor = (version: string | null | undefined): number | null => {
|