kireji 0.6.7 → 0.7.3
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/package.json +1 -1
- package/src/app/kireji/editor/point.js +0 -1
- package/src/app/kireji/editor/tab-group/type.d.ts +6 -18
- package/src/app/kireji/sidebar/type.d.ts +1 -3
- package/src/build.js +56 -0
- package/src/parts/abstract/mesh/build.js +84 -0
- package/src/parts/abstract/mesh/constants.js +1 -0
- package/src/parts/abstract/mesh/model_.js +1 -0
- package/src/parts/abstract/mesh/part.json +18 -0
- package/src/parts/abstract/mesh/point-tri-contains.js +14 -0
- package/src/parts/abstract/mesh/point-tri-that-contains.js +10 -0
- package/src/parts/abstract/mesh/ray-cast.js +262 -0
- package/src/parts/abstract/mesh/routeID-distribute.js +42 -0
- package/src/parts/abstract/mesh/routeID-model-to.js +5 -0
- package/src/parts/abstract/mesh/type.d.ts +85 -0
- package/src/parts/abstract/part/part.json +1 -3
- package/src/parts/abstract/part/routeID-update.js +5 -5
- package/src/parts/abstract/part/type.d.ts +5 -11
- package/src/parts/abstract/permutation/type.d.ts +2 -10
- package/src/parts/core/client/async-install.js +63 -72
- package/src/parts/core/client/build.js +6 -2
- package/src/parts/core/client/loop-request.js +10 -0
- package/src/parts/core/client/part.json +6 -1
- package/src/parts/core/client/type.d.ts +17 -3
- package/src/parts/core/hot-keys/async-install.js +18 -8
- package/src/parts/core/hot-keys/constants.js +4 -2
- package/src/parts/core/pointer/type.d.ts +4 -12
- package/src/parts/desktop/task-bar/tray/stats/button.html_.js +4 -1
- package/src/parts/desktop/task-bar/tray/stats/loop.js +1 -3
- package/src/parts/desktop/task-bar/tray/stats/type.d.ts +0 -7
- package/src/parts/desktop/windows/type.d.ts +1 -3
- package/src/type.d.ts +16 -12
- package/src/loop-distribute.js +0 -2
- package/src/parts/desktop/task-bar/tray/stats/build.js +0 -5
package/package.json
CHANGED
|
@@ -163,7 +163,6 @@ const pointerConfig = {
|
|
|
163
163
|
tabGroup.recomputeRouteID(true)
|
|
164
164
|
} else {
|
|
165
165
|
|
|
166
|
-
debug("B", tabGroup.previewTabIndex === null, BigInt(tabGroup.openTabs.length), tabGroup.maxTabCount)
|
|
167
166
|
if (BigInt(tabGroup.openTabs.length) === tabGroup.maxTabCount) {
|
|
168
167
|
alert("You have too many tabs open!")
|
|
169
168
|
return
|
|
@@ -28,17 +28,11 @@ declare interface IKirejiAppTabGroup
|
|
|
28
28
|
readonly permutationSizes: bigint[]
|
|
29
29
|
readonly payloadCardinality: bigint
|
|
30
30
|
readonly payloadSizes: bigint[]
|
|
31
|
-
/**
|
|
32
|
-
*
|
|
33
|
-
* The tab group's main HTML element. */
|
|
31
|
+
/** The tab group's main HTML element. @remarks Client-only */
|
|
34
32
|
readonly container: HTMLElement
|
|
35
|
-
/**
|
|
36
|
-
*
|
|
37
|
-
* The most recent tab model, as determined while populating the view (not when propagating the route ID). */
|
|
33
|
+
/** The most recent tab model, as determined while populating the view (not when propagating the route ID). @remarks Client-only */
|
|
38
34
|
readonly viewedActiveTab?: IKirejiAppTabGroupTab
|
|
39
|
-
/**
|
|
40
|
-
*
|
|
41
|
-
* The most recent preview tab model, as determined while populating the view (not when propagating the route ID). */
|
|
35
|
+
/** The most recent preview tab model, as determined while populating the view (not when propagating the route ID). @remarks Client-only */
|
|
42
36
|
readonly viewedPreviewTab?: IKirejiAppTabGroupTab
|
|
43
37
|
/** The most recent permutation route ID, used to quickly determine if the tab arrangement has changed since the last view population. */
|
|
44
38
|
readonly viewedPermutationRouteID?: bigint
|
|
@@ -70,17 +64,11 @@ declare interface IKirejiAppTabGroupTab {
|
|
|
70
64
|
}
|
|
71
65
|
|
|
72
66
|
declare const tabGroup: IKirejiAppTabGroup
|
|
73
|
-
/** The index of the tab to render.
|
|
74
|
-
*
|
|
75
|
-
* *Only available in `close`, `point` and `renderTabHTML` methods.* */
|
|
67
|
+
/** The index of the tab to render. @remarks Only in `close`, `point` and `renderTabHTML` methods. */
|
|
76
68
|
declare const TAB_INDEX: number
|
|
77
|
-
/** The host part of the given tab.
|
|
78
|
-
*
|
|
79
|
-
* *Only available in `renderTabHTML` method.* */
|
|
69
|
+
/** The host part of the given tab. @remarks Only in `renderTabHTML` method. */
|
|
80
70
|
declare const TAB_PART: IPartAny
|
|
81
|
-
/** The filename of the given tab.
|
|
82
|
-
*
|
|
83
|
-
* *Only available in `renderTabHTML` method.* */
|
|
71
|
+
/** The filename of the given tab. @remarks Only in `renderTabHTML` method. */
|
|
84
72
|
declare const TAB_FILENAME: string
|
|
85
73
|
declare const activeTab: IKirejiAppTabGroupTab
|
|
86
74
|
declare const activePart: IPartAny
|
|
@@ -14,9 +14,7 @@ declare interface IKirejiAppSidebar
|
|
|
14
14
|
readonly "view.html": string
|
|
15
15
|
/** The part outliner currently assigned to the sidebar (resolves to a view even when the sidebar is hidden). */
|
|
16
16
|
readonly "view": IPartOutliner<IKirejiAppSidebar>
|
|
17
|
-
/**
|
|
18
|
-
*
|
|
19
|
-
* Opens any closed parent folders of and scrolls (if necessary) to the element corresponding to the currently active tab. Does nothing if the sidebar is closed or there are no tabs open. */
|
|
17
|
+
/** Opens any closed parent folders of and scrolls (if necessary) to the element corresponding to the currently active tab. Does nothing if the sidebar is closed or there are no tabs open. @remarks Client-only */
|
|
20
18
|
readonly frameActiveTab(): void
|
|
21
19
|
|
|
22
20
|
// Runtime Properties.
|
package/src/build.js
CHANGED
|
@@ -90,6 +90,62 @@ function ƒ(_, compressedSubjectOrigins) {
|
|
|
90
90
|
result[dimension] = vector[dimension] / magnitude
|
|
91
91
|
return result
|
|
92
92
|
}
|
|
93
|
+
static sign(vector) {
|
|
94
|
+
const result = {}
|
|
95
|
+
for (const dimension in vector)
|
|
96
|
+
result[dimension] = Math.sign(vector[dimension])
|
|
97
|
+
return result
|
|
98
|
+
}
|
|
99
|
+
static floor(vector) {
|
|
100
|
+
const result = {}
|
|
101
|
+
for (const dimension in vector)
|
|
102
|
+
result[dimension] = Math.floor(vector[dimension])
|
|
103
|
+
return result
|
|
104
|
+
}
|
|
105
|
+
static round(vector) {
|
|
106
|
+
const result = {}
|
|
107
|
+
for (const dimension in vector)
|
|
108
|
+
result[dimension] = Math.round(vector[dimension])
|
|
109
|
+
return result
|
|
110
|
+
}
|
|
111
|
+
static operate(value1, value2, operation) {
|
|
112
|
+
if (typeof value1 === "number" || typeof value2 === "number") {
|
|
113
|
+
if (typeof value1 === "number" && typeof value2 === "number")
|
|
114
|
+
return operation(value1, value2)
|
|
115
|
+
|
|
116
|
+
const result = {}
|
|
117
|
+
const number = typeof value1 === "number" ? value1 : value2
|
|
118
|
+
const vector = typeof value1 === "object" ? value1 : value2
|
|
119
|
+
|
|
120
|
+
for (const dimension in vector)
|
|
121
|
+
result[dimension] = operation(vector[dimension], number)
|
|
122
|
+
|
|
123
|
+
return result
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const result = {}
|
|
127
|
+
const dimensions = Object.keys(value1)
|
|
128
|
+
|
|
129
|
+
if (dimensions.some(key => !(key in value2)) || Object.keys(value2).some(key => !(key in value1)))
|
|
130
|
+
throw new Error(`Vector Operation Error: the two vectors do not have the same keys.`)
|
|
131
|
+
|
|
132
|
+
for (const dimension of dimensions)
|
|
133
|
+
result[dimension] = operation(value1[dimension], value2[dimension])
|
|
134
|
+
|
|
135
|
+
return result
|
|
136
|
+
}
|
|
137
|
+
static add(value1, value2) {
|
|
138
|
+
return this.operate(value1, value2, (a, b) => a + b)
|
|
139
|
+
}
|
|
140
|
+
static subtract(value1, value2) {
|
|
141
|
+
return this.operate(value1, value2, (a, b) => a - b)
|
|
142
|
+
}
|
|
143
|
+
static multiply(value1, value2) {
|
|
144
|
+
return this.operate(value1, value2, (a, b) => a * b)
|
|
145
|
+
}
|
|
146
|
+
static dot(vector1, vector2) {
|
|
147
|
+
return vector1.x * vector2.x + vector1.y * vector2.y
|
|
148
|
+
}
|
|
93
149
|
}
|
|
94
150
|
class FenwickTree {
|
|
95
151
|
static LSB = []
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
mesh.define({
|
|
2
|
+
triTable: { value: [] },
|
|
3
|
+
triIndex: { value: -1, writable: true },
|
|
4
|
+
position: { value: { x: null, y: null } },
|
|
5
|
+
cardinality: {
|
|
6
|
+
resolve() {
|
|
7
|
+
let meshCardinality = 0n
|
|
8
|
+
|
|
9
|
+
// Obtain the raw data for this mesh.
|
|
10
|
+
const [pointList, tris] = mesh.getData()
|
|
11
|
+
|
|
12
|
+
// Iterate over each tri (array of three point indices) in the data.
|
|
13
|
+
for (const tri of tris) {
|
|
14
|
+
|
|
15
|
+
// Prepare the per-tri cardinality sum.
|
|
16
|
+
let triCardinality = 0n
|
|
17
|
+
|
|
18
|
+
// Recover the true points of the tri.
|
|
19
|
+
const points = tri.map(pointID => pointList[pointID])
|
|
20
|
+
|
|
21
|
+
// Get just the y-coordinate of all the points.
|
|
22
|
+
const yPoints = points.map(point => point[1])
|
|
23
|
+
|
|
24
|
+
// Estimate the y-range of this tri.
|
|
25
|
+
const range = {
|
|
26
|
+
min: Math.min(...yPoints),
|
|
27
|
+
max: Math.max(...yPoints)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Iterate over the range inclusively to populate each row subspace.
|
|
31
|
+
const rows = []
|
|
32
|
+
for (let rowID = range.min; rowID <= range.max; rowID++) {
|
|
33
|
+
|
|
34
|
+
// This irrational offset ensures that all grid points lie vertically in at most one triangular region.
|
|
35
|
+
const offsetRowID = rowID + (Math.PI / 3.141 - 0.5)
|
|
36
|
+
|
|
37
|
+
// Iterate over the 3 lines in order to determine where the edges intersect the given row.
|
|
38
|
+
const intersections = []
|
|
39
|
+
for (let edgeID = 0; edgeID < 3; edgeID++) {
|
|
40
|
+
|
|
41
|
+
const pointA = points[edgeID]
|
|
42
|
+
const pointB = points[(edgeID + 1) % 3]
|
|
43
|
+
|
|
44
|
+
if ((pointA[1] <= offsetRowID && pointB[1] > offsetRowID) || (pointB[1] <= offsetRowID && pointA[1] > offsetRowID))
|
|
45
|
+
intersections.push(pointA[0] + ((offsetRowID - pointA[1]) / (pointB[1] - pointA[1])) * (pointB[0] - pointA[0]))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (intersections.length < 2) {
|
|
49
|
+
// This row doesn't have a full pixel. Adjust the range.
|
|
50
|
+
if (rowID <= range.min + 1) range.min = rowID + 1
|
|
51
|
+
else if (rowID >= range.max - 1) range.max = rowID - 1
|
|
52
|
+
else throw new Error("Unexpected tri geometry found in mesh.")
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** @type {IMeshTriDataRow} */
|
|
57
|
+
const row = {
|
|
58
|
+
y: rowID,
|
|
59
|
+
range: {
|
|
60
|
+
min: Math.ceil(Math.min(...intersections)),
|
|
61
|
+
max: Math.ceil(Math.max(...intersections)) - 1,
|
|
62
|
+
},
|
|
63
|
+
offset: triCardinality
|
|
64
|
+
}
|
|
65
|
+
rows.push(row)
|
|
66
|
+
triCardinality += BigInt(row.range.max - row.range.min + 1)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Store the tri.
|
|
70
|
+
mesh.triTable.push({
|
|
71
|
+
points,
|
|
72
|
+
range,
|
|
73
|
+
rows,
|
|
74
|
+
offset: meshCardinality,
|
|
75
|
+
cardinality: triCardinality
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
meshCardinality += triCardinality
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return meshCardinality
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const mesh = this
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
return { ...mesh.position }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"abstract": true,
|
|
3
|
+
"methods": {
|
|
4
|
+
"data-get": [],
|
|
5
|
+
"point-tri-contains": [
|
|
6
|
+
"TRI_INDEX",
|
|
7
|
+
"POINT"
|
|
8
|
+
],
|
|
9
|
+
"point-tri-that-contains": [
|
|
10
|
+
"POINT"
|
|
11
|
+
],
|
|
12
|
+
"ray-cast": [
|
|
13
|
+
"FORCE_VECTOR",
|
|
14
|
+
"DELTA_TIME",
|
|
15
|
+
"ENABLE_SLIDING"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const roundedY = Math.floor(POINT.y)
|
|
2
|
+
const triData = mesh.triTable[TRI_INDEX]
|
|
3
|
+
|
|
4
|
+
if (roundedY < triData.range.min || roundedY > triData.range.max)
|
|
5
|
+
return false
|
|
6
|
+
|
|
7
|
+
const row = triData.rows[roundedY - triData.range.min]
|
|
8
|
+
|
|
9
|
+
if (!row)
|
|
10
|
+
return false
|
|
11
|
+
|
|
12
|
+
const roundedX = Math.floor(POINT.x)
|
|
13
|
+
|
|
14
|
+
return roundedX >= row.range.min && roundedX <= row.range.max
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Check current tri first.
|
|
2
|
+
if (mesh.triIndex !== -1 && mesh.triContainsPoint(mesh.triIndex, POINT))
|
|
3
|
+
return mesh.triIndex
|
|
4
|
+
|
|
5
|
+
// Check all tris. TODO: Memoize neighbors.
|
|
6
|
+
for (let triIndex = 0; triIndex < mesh.triTable.length; triIndex++)
|
|
7
|
+
if (triIndex !== mesh.triIndex && mesh.triContainsPoint(triIndex, POINT))
|
|
8
|
+
return triIndex
|
|
9
|
+
|
|
10
|
+
return -1
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// Define a safe result: the current position before casting.
|
|
2
|
+
const safeIterationResult = {
|
|
3
|
+
hit: false,
|
|
4
|
+
triIndex: mesh.triIndex,
|
|
5
|
+
point: { ...mesh.position },
|
|
6
|
+
forceVector: FORCE_VECTOR
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Obtain the speed of the force vector, which will be used later if we need to slide along the boundary.
|
|
10
|
+
const speed = Vector.magnitude(FORCE_VECTOR)
|
|
11
|
+
|
|
12
|
+
// If there's no motion, nothing will happen; return the safe result.
|
|
13
|
+
if (speed === 0)
|
|
14
|
+
return safeIterationResult
|
|
15
|
+
|
|
16
|
+
// Otherwise, initialize timing data as though the vector doesn't intersect any grid lines.
|
|
17
|
+
const timeOfNextIntersection = { x: Infinity, y: Infinity }
|
|
18
|
+
const timeBetweenIntersections = { ...timeOfNextIntersection }
|
|
19
|
+
|
|
20
|
+
// Set the clock to zero.
|
|
21
|
+
let time = 0
|
|
22
|
+
|
|
23
|
+
function initalizeRayIntersectionSchedule(alsoComputeIntersectionInterval) {
|
|
24
|
+
|
|
25
|
+
// If the force vector isn't parallel to the x axis...
|
|
26
|
+
if (FORCE_VECTOR.x !== 0) {
|
|
27
|
+
|
|
28
|
+
// Determine the x coordinate of the first grid line the ray may intersect.
|
|
29
|
+
const nextGridX = Math.floor(safeIterationResult.point.x) + (FORCE_VECTOR.x > 0)
|
|
30
|
+
|
|
31
|
+
// Compute the exact moment that intersection will happen.
|
|
32
|
+
timeOfNextIntersection.x = time + (nextGridX - safeIterationResult.point.x) / FORCE_VECTOR.x
|
|
33
|
+
|
|
34
|
+
// Compute the constant time between each grid line intersection.
|
|
35
|
+
if (alsoComputeIntersectionInterval)
|
|
36
|
+
timeBetweenIntersections.x = 1 / Math.abs(FORCE_VECTOR.x)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Perform the same steps for the y-axis.
|
|
40
|
+
if (FORCE_VECTOR.y !== 0) {
|
|
41
|
+
const nextGridY = Math.floor(safeIterationResult.point.y) + (FORCE_VECTOR.y > 0)
|
|
42
|
+
timeOfNextIntersection.y = time + (nextGridY - safeIterationResult.point.y) / FORCE_VECTOR.y
|
|
43
|
+
if (alsoComputeIntersectionInterval)
|
|
44
|
+
timeBetweenIntersections.y = 1 / Math.abs(FORCE_VECTOR.y)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
initalizeRayIntersectionSchedule(true)
|
|
49
|
+
|
|
50
|
+
const start = _.now
|
|
51
|
+
|
|
52
|
+
let iteration = 0
|
|
53
|
+
|
|
54
|
+
// Start iterating on the ray cast.
|
|
55
|
+
while (true) {
|
|
56
|
+
|
|
57
|
+
iteration++
|
|
58
|
+
|
|
59
|
+
// Emergency exit the loop.
|
|
60
|
+
if (_.now - start >= (DELTA_TIME * 1000))
|
|
61
|
+
throw { error: "LOCKED WHILE LOOP", processingTime: _.now - start, iteration, DELTA_TIME, FORCE_VECTOR, speed, safeIterationResult, timeOfNextIntersection, timeBetweenIntersections }
|
|
62
|
+
|
|
63
|
+
// Pick the dimension whose grid intersection will happen next.
|
|
64
|
+
const dimension = timeOfNextIntersection.x < timeOfNextIntersection.y ? "x" : "y"
|
|
65
|
+
|
|
66
|
+
// Note how much time must elapse to reach that intersection.
|
|
67
|
+
let timeElapsedDuringThisIteration = timeOfNextIntersection[dimension] - time
|
|
68
|
+
|
|
69
|
+
// Set the clock to the moment of that intersection.
|
|
70
|
+
time += timeElapsedDuringThisIteration
|
|
71
|
+
|
|
72
|
+
// Note if this is a case where the grid intersection is reached after the time limit.
|
|
73
|
+
let ranOutOfTime = time >= DELTA_TIME
|
|
74
|
+
|
|
75
|
+
// In that case...
|
|
76
|
+
if (ranOutOfTime) {
|
|
77
|
+
|
|
78
|
+
// Measure how far we are past the time limit.
|
|
79
|
+
const overshootTime = time - DELTA_TIME
|
|
80
|
+
|
|
81
|
+
// Adjust how much time we say elapsed.
|
|
82
|
+
timeElapsedDuringThisIteration -= overshootTime
|
|
83
|
+
|
|
84
|
+
// Set the block back to the last possible moment.
|
|
85
|
+
time = DELTA_TIME
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Construct the next point along the ray, given the elapsed time.
|
|
89
|
+
const point = Vector.add(safeIterationResult.point, Vector.multiply(FORCE_VECTOR, timeElapsedDuringThisIteration))
|
|
90
|
+
|
|
91
|
+
// Check if the point is outside the mesh boundary.
|
|
92
|
+
const triIndex = mesh.triThatContainsPoint(point)
|
|
93
|
+
|
|
94
|
+
// If it is...
|
|
95
|
+
if (triIndex === -1) {
|
|
96
|
+
|
|
97
|
+
// We are at a boundary pixel. Let's move our position to exactly the center of our current point.
|
|
98
|
+
// safeIterationResult.point = Vector.add(Vector.floor(safeIterationResult.point), 0.5)
|
|
99
|
+
|
|
100
|
+
// If we have permission to simulate sliding...
|
|
101
|
+
if (ENABLE_SLIDING) {
|
|
102
|
+
|
|
103
|
+
// Walk back the clock to before the ray hit.
|
|
104
|
+
time -= timeElapsedDuringThisIteration
|
|
105
|
+
|
|
106
|
+
const neighbor = {
|
|
107
|
+
dot: -Infinity,
|
|
108
|
+
point: null,
|
|
109
|
+
triIndex: null,
|
|
110
|
+
direction: {
|
|
111
|
+
x: null,
|
|
112
|
+
y: null
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let boundaryAppearsFlat = false
|
|
117
|
+
|
|
118
|
+
// Search the local neighborhood of points for one that we can safely move to.
|
|
119
|
+
for (let x = -1; x <= 1; x++) for (let y = -1; y <= 1; y++) {
|
|
120
|
+
|
|
121
|
+
// Exclude the point itself
|
|
122
|
+
if (!x && !y)
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
const direction = { x, y }
|
|
126
|
+
|
|
127
|
+
// Find out how much of the force vector is cancelled out by going to this neighbor.
|
|
128
|
+
const dot = Vector.dot(Vector.normalize(direction), Vector.normalize(FORCE_VECTOR))
|
|
129
|
+
|
|
130
|
+
// If it would require traveling "against" or perpendicular to the force vector, exclude it from consideration.
|
|
131
|
+
if (dot < 0)
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
// Get the position of the center of the neighbor.
|
|
135
|
+
const point = Vector.add(Vector.floor(Vector.add(safeIterationResult.point, direction)), 0.5)
|
|
136
|
+
const triIndex = mesh.triThatContainsPoint(point)
|
|
137
|
+
|
|
138
|
+
// If it isn't part of the mesh, exclude it from consideration.
|
|
139
|
+
if (triIndex === -1)
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
// Store the neighbor which most agrees with the force vector.
|
|
143
|
+
if (dot >= neighbor.dot) {
|
|
144
|
+
if (dot < 0.05 && neighbor.dot !== -Infinity && neighbor.dot < 0.05) {
|
|
145
|
+
boundaryAppearsFlat = true
|
|
146
|
+
// Boundary is "flat". Don't allow perpendicular motion.
|
|
147
|
+
neighbor.dot = -Infinity
|
|
148
|
+
neighbor.point = null
|
|
149
|
+
neighbor.triIndex = null
|
|
150
|
+
neighbor.direction.x = null
|
|
151
|
+
neighbor.direction.y = null
|
|
152
|
+
} else {
|
|
153
|
+
boundaryAppearsFlat = false
|
|
154
|
+
neighbor.dot = dot
|
|
155
|
+
neighbor.point = point
|
|
156
|
+
neighbor.triIndex = triIndex
|
|
157
|
+
neighbor.direction = direction
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (boundaryAppearsFlat) {
|
|
163
|
+
// TODO: Consider using the line through the opposing neighbors to search for assymetry in the mesh boundry.
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// If there is no neighboring point, we are "stuck". The ray cast ends here.
|
|
167
|
+
if (!neighbor.point) {
|
|
168
|
+
|
|
169
|
+
// Return the safe result data.
|
|
170
|
+
safeIterationResult.hit = true
|
|
171
|
+
return safeIterationResult
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Compute travel distance to the center of the neighbor.
|
|
175
|
+
const vectorToNeighbor = Vector.subtract(neighbor.point, safeIterationResult.point)
|
|
176
|
+
const distance = Vector.magnitude(vectorToNeighbor)
|
|
177
|
+
|
|
178
|
+
// Construct a force vector pointing to the chosen neighbor.
|
|
179
|
+
// TODO: speed should actually be reduced.
|
|
180
|
+
const forceVectorToNeighbor = Vector.multiply(Vector.normalize(vectorToNeighbor), speed)
|
|
181
|
+
|
|
182
|
+
// Compute travel time to reach the neighbor.
|
|
183
|
+
timeElapsedDuringThisIteration = distance / speed
|
|
184
|
+
|
|
185
|
+
// Move the clock ahead to our arrival at the neighbor.
|
|
186
|
+
time += timeElapsedDuringThisIteration
|
|
187
|
+
|
|
188
|
+
// Check again if we ran out of time.
|
|
189
|
+
ranOutOfTime = time >= DELTA_TIME
|
|
190
|
+
if (ranOutOfTime) {
|
|
191
|
+
const overshootTime = time - DELTA_TIME
|
|
192
|
+
timeElapsedDuringThisIteration -= overshootTime
|
|
193
|
+
time = DELTA_TIME
|
|
194
|
+
|
|
195
|
+
// Construct the point along the way to the neighbor.
|
|
196
|
+
const point = Vector.add(safeIterationResult.point, Vector.multiply(forceVectorToNeighbor, timeElapsedDuringThisIteration))
|
|
197
|
+
const triIndex = mesh.triThatContainsPoint(point)
|
|
198
|
+
safeIterationResult.forceVector = forceVectorToNeighbor
|
|
199
|
+
|
|
200
|
+
if (triIndex === -1) {
|
|
201
|
+
// We didn't have enough time to overcome the diagonal boundary.
|
|
202
|
+
|
|
203
|
+
if (speed * DELTA_TIME > Math.SQRT2) {
|
|
204
|
+
// The current speed is enough to overcome diagonal corners.
|
|
205
|
+
|
|
206
|
+
// Return the previous safe point, chalking the lost time up to friction.
|
|
207
|
+
return safeIterationResult
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// There is not enough velocity to overcome the diagonal. Use random chance.
|
|
211
|
+
if (Math.random() < Math.SQRT1_2) {
|
|
212
|
+
safeIterationResult.point = neighbor.point
|
|
213
|
+
safeIterationResult.triIndex = neighbor.triIndex
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return safeIterationResult
|
|
217
|
+
} else {
|
|
218
|
+
|
|
219
|
+
// Go to the partial point.
|
|
220
|
+
safeIterationResult.point = point
|
|
221
|
+
safeIterationResult.triIndex = triIndex
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return safeIterationResult
|
|
225
|
+
|
|
226
|
+
// TODO: Consider moving toward a 4-point neighbor instead.
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Set the neighbor as the new safe point.
|
|
230
|
+
safeIterationResult.point = neighbor.point
|
|
231
|
+
safeIterationResult.triIndex = neighbor.triIndex
|
|
232
|
+
safeIterationResult.forceVector = FORCE_VECTOR
|
|
233
|
+
|
|
234
|
+
// The normal ray intersection schedule has changed.
|
|
235
|
+
initalizeRayIntersectionSchedule()
|
|
236
|
+
|
|
237
|
+
// Proceed with normal ray casting behavior from this point.
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
} else {
|
|
241
|
+
|
|
242
|
+
// Discard the unsafe point and return the safe result data.
|
|
243
|
+
safeIterationResult.hit = true
|
|
244
|
+
return safeIterationResult
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
// Otherwise, update the safe result.
|
|
250
|
+
safeIterationResult.point = point
|
|
251
|
+
safeIterationResult.triIndex = triIndex
|
|
252
|
+
|
|
253
|
+
// If we ran out of time...
|
|
254
|
+
if (ranOutOfTime) {
|
|
255
|
+
|
|
256
|
+
// We're done. Return the safe point.
|
|
257
|
+
return safeIterationResult
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Otherwise, Prepare for the next grid line time of this dimension.
|
|
261
|
+
timeOfNextIntersection[dimension] += timeBetweenIntersections[dimension]
|
|
262
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
mesh.updateRouteID(ROUTE_ID)
|
|
2
|
+
|
|
3
|
+
// Binary search better than embedded match.
|
|
4
|
+
mesh.triIndex = (() => {
|
|
5
|
+
|
|
6
|
+
let low = 0
|
|
7
|
+
let high = mesh.triTable.length - 1
|
|
8
|
+
|
|
9
|
+
while (low <= high) {
|
|
10
|
+
|
|
11
|
+
const mid = (low + high) >>> 1
|
|
12
|
+
const triData = mesh.triTable[mid]
|
|
13
|
+
const nextTriData = mesh.triTable[mid + 1]
|
|
14
|
+
|
|
15
|
+
if (ROUTE_ID < triData.offset)
|
|
16
|
+
high = mid - 1
|
|
17
|
+
else if (nextTriData && ROUTE_ID >= nextTriData.offset)
|
|
18
|
+
low = mid + 1
|
|
19
|
+
else
|
|
20
|
+
return mid
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Fallback.
|
|
24
|
+
return 0
|
|
25
|
+
})()
|
|
26
|
+
|
|
27
|
+
ROUTE_ID -= mesh.triTable[mesh.triIndex].offset
|
|
28
|
+
|
|
29
|
+
// Embedded match can become binary search later.
|
|
30
|
+
for (const row of mesh.triTable[mesh.triIndex].rows) {
|
|
31
|
+
|
|
32
|
+
if (!row)
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
const rowWidth = BigInt(row.range.max - row.range.min + 1)
|
|
36
|
+
|
|
37
|
+
if (ROUTE_ID < row.offset + rowWidth) {
|
|
38
|
+
mesh.position.y = row.y
|
|
39
|
+
mesh.position.x = row.range.min + Number(ROUTE_ID - row.offset)
|
|
40
|
+
break
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// TODO: Validate MODEL and handle out-of-range points.
|
|
2
|
+
const triIndex = mesh.triThatContainsPoint(MODEL)
|
|
3
|
+
const triData = mesh.triTable[triIndex]
|
|
4
|
+
const row = triData.rows[Math.floor(MODEL.y) - triData.range.min]
|
|
5
|
+
return triData.offset + row.offset + BigInt(Math.floor(MODEL.x) - row.range.min)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
declare interface IMesh<TOwner>
|
|
2
|
+
extends IPart<TOwner, null> {
|
|
3
|
+
|
|
4
|
+
// Serialized Properties.
|
|
5
|
+
readonly getData(): IMeshData
|
|
6
|
+
/** Casts a ray from the current mesh position along the force vector direction for the given delta time and returns a summary of the results. @param FORCE_VECTOR the force vector represent the position the uninterrupted ray will arrive at in one second. @param DELTA_TIME the duration of the time cast in seconds. @param ENABLE_SLIDING whether or not to enable the ray to "wrap" along the mesh boundary instead of stopping cold. */
|
|
7
|
+
readonly castRay(FORCE_VECTOR: IVector2, DELTA_TIME: number, ENABLE_SLIDING: boolean): {
|
|
8
|
+
/** Whether or not the ray hit the mesh boundary. */
|
|
9
|
+
readonly hit: boolean
|
|
10
|
+
/** The tri the ray most recently occupied when it hit the boundary or the index of the boundary where the ray stopped if there is no hit. */
|
|
11
|
+
readonly triIndex: IMeshTriIndex
|
|
12
|
+
/** The rounded cell position where the cast ray landed. */
|
|
13
|
+
readonly position: IVector2
|
|
14
|
+
/** The force vector used to cast the ray, which might be different from the input if sliding is enabled as it may point along the direction of the most recent sliding iteration. */
|
|
15
|
+
readonly forceVector: IVector2
|
|
16
|
+
}
|
|
17
|
+
/** Checks if a point (x, y) rounds to a valid pixel within the specified tri's memoized row data. */
|
|
18
|
+
readonly triContainsPoint(TRI_INDEX: IMeshTriIndex, POINT: IVector2): boolean
|
|
19
|
+
/** Returns whether or not the given point is in a tri. @returns the index of the tri that contains the point. -1 otherwise. */
|
|
20
|
+
readonly triThatContainsPoint(POINT: IVector2): IMeshTriIndex
|
|
21
|
+
|
|
22
|
+
// Runtime Properties.
|
|
23
|
+
/** The memoization data of every tri in the mesh, stored by tri index at build-time. */
|
|
24
|
+
readonly triTable: IMeshTriData[]
|
|
25
|
+
/** The index of the current tri in the mesh. */
|
|
26
|
+
readonly triIndex?: IMeshTriIndex
|
|
27
|
+
/** The current position of the state in the mesh. */
|
|
28
|
+
readonly position: IVector2
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
declare interface IMeshTriData {
|
|
32
|
+
/** The original point array of the tri as taken from `mesh.getData()`. */
|
|
33
|
+
readonly points: IMeshTriPoints
|
|
34
|
+
/** The y-axis range of the tri. */
|
|
35
|
+
readonly range: {
|
|
36
|
+
/** The y position of the top-most row of the tri. */
|
|
37
|
+
readonly min: number
|
|
38
|
+
/** The y position of the bottom-most row of the tri. */
|
|
39
|
+
readonly max: number
|
|
40
|
+
}
|
|
41
|
+
/** The rows of pixels in the tri. */
|
|
42
|
+
readonly rows: IMeshTriDataRow[]
|
|
43
|
+
/** The offset of the tri in the overall mesh. */
|
|
44
|
+
readonly offset: bigint
|
|
45
|
+
/** The total number of pixels in the tri. */
|
|
46
|
+
readonly cardinality: bigint
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
declare interface IMeshTriDataRow {
|
|
50
|
+
/** The y position of this row of the tri's space. */
|
|
51
|
+
readonly y: number
|
|
52
|
+
/** The x-axis range of the row. */
|
|
53
|
+
readonly range: {
|
|
54
|
+
/** The smallest x position that is within the tri row. */
|
|
55
|
+
readonly min: number
|
|
56
|
+
/** The largest x position that is within the tri row. */
|
|
57
|
+
readonly max: number
|
|
58
|
+
}
|
|
59
|
+
/** The bigint offset of this row in the overall tri. */
|
|
60
|
+
readonly offset: bigint
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
declare type IMeshData = [IMeshPoint[], IMeshTri[]]
|
|
64
|
+
|
|
65
|
+
declare type IMeshTri =
|
|
66
|
+
[IMeshPointIndex, IMeshPointIndex, IMeshPointIndex]
|
|
67
|
+
|
|
68
|
+
declare type IMeshPointIndex =
|
|
69
|
+
number
|
|
70
|
+
|
|
71
|
+
declare type IMeshTriPoints =
|
|
72
|
+
[IMeshPoint, IMeshPoint, IMeshPoint]
|
|
73
|
+
|
|
74
|
+
declare type IMeshPoint =
|
|
75
|
+
[x: number, y: number]
|
|
76
|
+
|
|
77
|
+
declare type IMeshTriIndex =
|
|
78
|
+
number
|
|
79
|
+
|
|
80
|
+
declare type IMeshAny =
|
|
81
|
+
IMesh<IPartAny>
|
|
82
|
+
|
|
83
|
+
declare const mesh: IMeshAny
|
|
84
|
+
|
|
85
|
+
declare const TRI: IMeshTriData
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
if (part.isAbstract)
|
|
2
|
-
throw new Error(`You can't set the route ID of an abstract part. (${part.host})`)
|
|
2
|
+
throw new Error(`Route ID Update Error: You can't set the route ID of an abstract part. (${part.host})`)
|
|
3
3
|
|
|
4
4
|
if (ROUTE_ID === undefined)
|
|
5
|
-
throw new Error(`The ROUTE_ID argument is missing. ` + part.host)
|
|
5
|
+
throw new Error(`Route ID Update Error: The ROUTE_ID argument is missing. ` + part.host)
|
|
6
6
|
|
|
7
7
|
if (typeof ROUTE_ID !== "bigint" || ROUTE_ID < -1n)
|
|
8
|
-
throw new Error(`Route ID is invalid. ` + part.host)
|
|
8
|
+
throw new Error(`Route ID Update Error: Route ID is invalid. ` + part.host)
|
|
9
9
|
|
|
10
10
|
if (ROUTE_ID >= part.cardinality)
|
|
11
|
-
throw new Error(`Route ID (${ROUTE_ID}) out of range (max = ${part.cardinality - 1n}). ` + part.host)
|
|
11
|
+
throw new Error(`Route ID Update Error: Route ID (${ROUTE_ID}) out of range (max = ${part.cardinality - 1n}). ` + part.host)
|
|
12
12
|
|
|
13
13
|
part.previousRouteID = part.routeID
|
|
14
14
|
part.routeID = ROUTE_ID
|
|
@@ -20,6 +20,6 @@ part.justDisabled = !part.enabled && part.wasEnabled
|
|
|
20
20
|
part.deltaRouteID = part.routeID - part.previousRouteID
|
|
21
21
|
|
|
22
22
|
if (part.deltaRouteID === 0n)
|
|
23
|
-
warn(`Reassigned route ID (${part.routeID}) to part.\n ${part[".."] ? `${part[".."].key} {\n ${part.key} : "${part.host}" // <--- this part \n }` : part.host}`)
|
|
23
|
+
warn(`Route ID Update Warning: Reassigned route ID (${part.routeID}) to part.\n ${part[".."] ? `${part[".."].key} {\n ${part.key} : "${part.host}" // <--- this part \n }` : part.host}`)
|
|
24
24
|
|
|
25
25
|
part.dirty = true
|
|
@@ -43,7 +43,7 @@ declare interface IPart<TOwner, TSubpart>
|
|
|
43
43
|
readonly notify(EVENT_TYPE: string): void
|
|
44
44
|
/** Computes the cardinality of the part from its subparts and defines any other necessary properties. */
|
|
45
45
|
readonly build(): void
|
|
46
|
-
/** Calls loop on the part and then propagates the call leafward to all subparts. */
|
|
46
|
+
/** Calls `loop()` on the part and then propagates the call leafward to all subparts. */
|
|
47
47
|
readonly distributeLoop(): void
|
|
48
48
|
/** Returns the subparts that meet the condition provided by FILTER_FUNCTION. */
|
|
49
49
|
readonly filter(FILTER_FUNCTION: (subpart: TSubpart, index: number, part: IPart<TOwner, TSubpart>) => TSubpart): TSubpart[]
|
|
@@ -53,7 +53,7 @@ declare interface IPart<TOwner, TSubpart>
|
|
|
53
53
|
readonly includes(SUBPART: TSubpart): boolean
|
|
54
54
|
readonly reduce(REDUCE_FUNCTION: (result: TResult, subpart: TSubpart, index: number, part: IPart<TOwner, TSubpart>) => TResult, INITIAL_VALUE: TResult): TResult
|
|
55
55
|
/** If defined, the per-frame update method for the part. Useful for implementing game features. */
|
|
56
|
-
readonly loop?(
|
|
56
|
+
readonly loop?(): void
|
|
57
57
|
/** Performs MAP_FUNCTION on every subpart of the part and returns an array of the results. */
|
|
58
58
|
readonly map(MAP_FUNCTION: (subpart: TSubpart, index: number, part: IPart<TOwner, TSubpart>) => TResult): TResult[]
|
|
59
59
|
/** Converts the given model to a routeID without modifying the state of the part. */
|
|
@@ -158,9 +158,7 @@ declare interface IPart<TOwner, TSubpart>
|
|
|
158
158
|
/** The previous route of the part, changed at the last call to distributeRouteID or collectRouteID. */
|
|
159
159
|
readonly previousRouteID: bigint
|
|
160
160
|
readonly Property: typeof Property
|
|
161
|
-
/** The part's prototype part.
|
|
162
|
-
*
|
|
163
|
-
* *Note: The part `part.abstract.parts` does not have a prototype part.* */
|
|
161
|
+
/** The part's prototype part. @remarks The part `part.abstract.parts` does not have a prototype part. */
|
|
164
162
|
readonly prototype?: IPartAny
|
|
165
163
|
/** The current route of the part expressed as a bigint index in the virtual array of all of its routes. */
|
|
166
164
|
readonly routeID: bigint
|
|
@@ -227,13 +225,9 @@ declare function base(...args): any
|
|
|
227
225
|
declare function recurse(...args): any
|
|
228
226
|
/** Sets the current ecosystem route ID and host application as the undo point (the state the back button will return to). */
|
|
229
227
|
declare function setUndoPoint(): void
|
|
230
|
-
/** The current incoming route ID argument.
|
|
231
|
-
*
|
|
232
|
-
* *Available within distributeRouteID, setRouteID and updateRouteID methods only.* */
|
|
228
|
+
/** The current incoming route ID argument. @remarks Available within distributeRouteID, setRouteID and updateRouteID methods only. */
|
|
233
229
|
declare const ROUTE_ID: bigint
|
|
234
|
-
/** The part on which an event occurred
|
|
235
|
-
*
|
|
236
|
-
* **where available as an argument passed to a part listener callback.* */
|
|
230
|
+
/** The part on which an event occurred. @remarks Where available as an argument passed to a part listener callback. */
|
|
237
231
|
declare const SENDER: IPartAny
|
|
238
232
|
/** If on the client, returns whether or not the server-rendered view has been taken over by the client-side MVC framework. */
|
|
239
233
|
declare const hydrated: boolean | null
|
|
@@ -33,24 +33,16 @@ declare interface IPermutation<TOwner, TInstance, TModel, TSubject>
|
|
|
33
33
|
readonly supersetSize: bigint
|
|
34
34
|
/** The maximum number of instances that can exist at one time. */
|
|
35
35
|
readonly maxInstanceCount: bigint
|
|
36
|
-
|
|
37
36
|
/** A subindex representing which permutation of k instances is assigned. */
|
|
38
37
|
readonly permutationRouteID: bigint
|
|
39
|
-
/**
|
|
40
|
-
*
|
|
41
|
-
* The most recent permutation route ID, used to quickly determine if the instance arrangement has changed since the last view population. */
|
|
38
|
+
/** The most recent permutation route ID, used to quickly determine if the instance arrangement has changed since the last view population. @remarks Client-only */
|
|
42
39
|
readonly viewedPermutationRouteID?: bigint
|
|
43
|
-
|
|
44
40
|
/** A subindex representing the combined per-instance payload data for the k instances. */
|
|
45
41
|
readonly payloadRouteID: bigint
|
|
46
|
-
|
|
47
42
|
/** The distributed instance data. */
|
|
48
43
|
readonly instances: TInstance[]
|
|
49
|
-
/**
|
|
50
|
-
*
|
|
51
|
-
* The set of viewed instance objects corresponding to the current `part.viewedPermutationRouteID`. */
|
|
44
|
+
/** The set of viewed instance objects corresponding to the current `part.viewedPermutationRouteID`. @remarks Client-only */
|
|
52
45
|
readonly viewedInstances: TInstance[]
|
|
53
|
-
|
|
54
46
|
/** A Fenwick tree that allows performant ranking and unranking of permutation indices. */
|
|
55
47
|
readonly tree: FenwickTree
|
|
56
48
|
}
|
|
@@ -3,91 +3,82 @@ await Promise.all([
|
|
|
3
3
|
worker.takeControlAsync()
|
|
4
4
|
])
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
if (+_.haltHydration && !production)
|
|
8
|
-
warn('Intentionally blocked hydration.')
|
|
9
|
-
else {
|
|
10
|
-
|
|
11
|
-
// To preview FOUC
|
|
12
|
-
if (_.hangHydration > 0 && !production)
|
|
13
|
-
logScope(0, `Intentionally hanging the main thread for ${_.hangHydration} milliseconds.`, log => {
|
|
14
|
-
const start = _.now
|
|
15
|
-
let iteration = -1, elapsedMilliseconds, remainingMilliseconds
|
|
16
|
-
do {
|
|
17
|
-
elapsedMilliseconds = Math.trunc(_.now - start)
|
|
18
|
-
const newRemainingMilliseconds = _.hangHydration - elapsedMilliseconds
|
|
19
|
-
Math.sin(iteration++)
|
|
20
|
-
if (Math.trunc(newRemainingMilliseconds / 100) !== Math.trunc(remainingMilliseconds / 100))
|
|
21
|
-
log("t: -" + newRemainingMilliseconds)
|
|
22
|
-
remainingMilliseconds = newRemainingMilliseconds
|
|
23
|
-
} while (remainingMilliseconds > 0)
|
|
24
|
-
log(`Main thread hang finished at iteration ${iteration}.`)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
log("Defining Client-Only Methods, Globals and Default Listeners")
|
|
28
|
-
if (environment === "client") {
|
|
29
|
-
Object.assign(globalThis, {
|
|
30
|
-
Q(...args) {
|
|
31
|
-
return document.querySelector(...args)
|
|
32
|
-
},
|
|
33
|
-
inRect(pointerEvent, boundingClientRect) {
|
|
34
|
-
return pointerEvent.clientX >= boundingClientRect.left
|
|
35
|
-
&& pointerEvent.clientX <= boundingClientRect.right
|
|
36
|
-
&& pointerEvent.clientY >= boundingClientRect.top
|
|
37
|
-
&& pointerEvent.clientY <= boundingClientRect.bottom
|
|
38
|
-
},
|
|
39
|
-
setUndoPoint() {
|
|
40
|
-
_.parts.core.addressBar.setUndoPoint()
|
|
41
|
-
},
|
|
42
|
-
pointer: _.parts.core.pointer,
|
|
43
|
-
client: _.parts.core.client
|
|
44
|
-
})
|
|
6
|
+
if (!production) {
|
|
45
7
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
log("Activating Body")
|
|
53
|
-
document.body.classList.remove("unhydrated")
|
|
8
|
+
// To debug FOUC.
|
|
9
|
+
if (+_.haltHydration) {
|
|
10
|
+
warn('Intentionally blocked hydration.')
|
|
11
|
+
return
|
|
12
|
+
}
|
|
54
13
|
|
|
55
|
-
|
|
56
|
-
|
|
14
|
+
// To simulate slow loading time.
|
|
15
|
+
if (+_.hangHydration > 0) {
|
|
16
|
+
warn(`Intentionally hanging the main thread for ${_.hangHydration} milliseconds.`)
|
|
17
|
+
const start = _.now
|
|
18
|
+
while (_.hangHydration - (_.now - start) > 0)
|
|
19
|
+
Math.sin(Math.random())
|
|
20
|
+
}
|
|
21
|
+
}
|
|
57
22
|
|
|
58
|
-
|
|
59
|
-
document.addEventListener("click", pointerEvent => {
|
|
60
|
-
pointerEvent.preventDefault()
|
|
61
|
-
pointerEvent.stopPropagation()
|
|
62
|
-
}, { capture: true })
|
|
23
|
+
logScope(0, "Finalizing Hydration", log => {
|
|
63
24
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
25
|
+
log("Defining Client-Only Methods, Globals and Default Listeners")
|
|
26
|
+
Object.assign(globalThis, {
|
|
27
|
+
Q(...args) {
|
|
28
|
+
return document.querySelector(...args)
|
|
29
|
+
},
|
|
30
|
+
inRect(pointerEvent, boundingClientRect) {
|
|
31
|
+
return pointerEvent.clientX >= boundingClientRect.left
|
|
32
|
+
&& pointerEvent.clientX <= boundingClientRect.right
|
|
33
|
+
&& pointerEvent.clientY >= boundingClientRect.top
|
|
34
|
+
&& pointerEvent.clientY <= boundingClientRect.bottom
|
|
35
|
+
},
|
|
36
|
+
setUndoPoint() {
|
|
37
|
+
_.parts.core.addressBar.setUndoPoint()
|
|
38
|
+
},
|
|
39
|
+
pointer: _.parts.core.pointer,
|
|
40
|
+
client: _.parts.core.client
|
|
41
|
+
})
|
|
68
42
|
|
|
69
|
-
|
|
70
|
-
|
|
43
|
+
// Handle navigation. TODO: fix bugs here.
|
|
44
|
+
window.addEventListener("pageshow", pageTransitionEvent => {
|
|
45
|
+
log("Setting Initial State")
|
|
46
|
+
addressBar.useRoute()
|
|
71
47
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
48
|
+
log("Activating Body")
|
|
49
|
+
document.body.classList.remove("unhydrated")
|
|
75
50
|
|
|
51
|
+
debug('remove these preview classes', document.querySelectorAll(`[class^="preview-"]`))
|
|
52
|
+
})
|
|
53
|
+
globalThis.addEventListener("popstate", () => {
|
|
76
54
|
log("Setting Initial State")
|
|
77
55
|
addressBar.useRoute()
|
|
78
56
|
|
|
79
57
|
log("Activating Body")
|
|
80
58
|
document.body.classList.remove("unhydrated")
|
|
81
59
|
|
|
82
|
-
|
|
83
|
-
|
|
60
|
+
debug('remove these preview classes', document.querySelectorAll(`[class^="preview-"]`))
|
|
61
|
+
})
|
|
84
62
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
63
|
+
// Prevent normal click events to ensure the pointerdown event always takes precedence.
|
|
64
|
+
document.addEventListener("click", pointerEvent => {
|
|
65
|
+
pointerEvent.preventDefault()
|
|
66
|
+
pointerEvent.stopPropagation()
|
|
67
|
+
}, { capture: true })
|
|
89
68
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
69
|
+
log("Setting Initial State")
|
|
70
|
+
// Propagate the initial state (matched to the snapshot exactly).
|
|
71
|
+
addressBar.useRoute()
|
|
72
|
+
|
|
73
|
+
log("Activating Interaction")
|
|
74
|
+
// Enable HTML event listeners.
|
|
75
|
+
globalThis._ = _
|
|
76
|
+
// Disable pre-hydration presentation.
|
|
77
|
+
document.body.classList.remove("unhydrated")
|
|
78
|
+
// Switch to post-hydration state propagation.
|
|
79
|
+
client.hydrated = true
|
|
80
|
+
|
|
81
|
+
log("Starting Engine Loop")
|
|
82
|
+
// One-frame lag to capture refresh-rate-specific initial timestamp.
|
|
83
|
+
requestAnimationFrame(now => client.requestLoop(now))
|
|
93
84
|
})
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
hydrated: { value: false, writable: true }
|
|
1
|
+
client.define({
|
|
2
|
+
hydrated: { value: false, writable: true },
|
|
3
|
+
fps: { value: 60, writable: true },
|
|
4
|
+
now: { value: null, writable: true },
|
|
5
|
+
deltaTime: { value: null, writable: true },
|
|
6
|
+
meanFrameTime: { value: 1000 / 60, writable: true },
|
|
3
7
|
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
requestAnimationFrame(now => {
|
|
2
|
+
const deltaTime = now - REQUEST_TIME
|
|
3
|
+
// Use 60fps delta if returning to tab or heavy lag.
|
|
4
|
+
client.deltaTime = deltaTime > 100 ? (1000 / 60) : deltaTime
|
|
5
|
+
client.meanFrameTime += (client.deltaTime - client.meanFrameTime) / 20
|
|
6
|
+
client.fps = Math.round(1000 / client.meanFrameTime)
|
|
7
|
+
client.now = now
|
|
8
|
+
_.distributeLoop()
|
|
9
|
+
recurse(now)
|
|
10
|
+
})
|
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
declare interface IClient
|
|
2
2
|
extends IFacet<ICore> {
|
|
3
3
|
|
|
4
|
+
// Serialized Properties.
|
|
5
|
+
/** Requests an animation frame that distributes the loop function throughout the ecosystem and calls itself again. @remarks Can only be run on the client. */
|
|
6
|
+
readonly requestLoop(REQUEST_TIME: DOMHighResTimeStamp): void
|
|
7
|
+
|
|
4
8
|
// Runtime Properties.
|
|
5
|
-
/** Whether or not the server-rendered page has been fully "taken over" by the client-side framework. */
|
|
9
|
+
/** Whether or not the server-rendered page has been fully "taken over" by the client-side framework. @remarks Can only become true in the client environment. */
|
|
6
10
|
readonly hydrated: boolean
|
|
11
|
+
/** The averaged framerate of the application over a certain time window. @remarks Defaults to `60` before the first loop and when not in the client environment. */
|
|
12
|
+
readonly fps?: number
|
|
13
|
+
/** The current session time, aligned to the client refresh rate by `requestAnimationFrame`. @remarks Defaults to 'null' on first loop and when not in the client environment.*/
|
|
14
|
+
readonly now: number
|
|
15
|
+
/** The difference between the session time of the current frame and the session time of the previous frame. @remarks Defaults to `1000 / 60` before the first loop and when not in the client environment. */
|
|
16
|
+
readonly deltaTime: number
|
|
17
|
+
/** The average length of time each frame is on screen in milliseconds. @remarks Defaults to `1000 / 60` before the first loop and when not in the client environment. */
|
|
18
|
+
readonly meanFrameTime?: number
|
|
7
19
|
}
|
|
8
20
|
|
|
9
|
-
/** A facet which helps toggle the page style between a locked loading state (before hydration) and a fully-interactive state (after hydration). */
|
|
10
|
-
declare const client: IClient
|
|
21
|
+
/** A facet which helps toggle the page style between a locked loading state (before hydration) and a fully-interactive state (after hydration). It also starts and continues the engine loop. */
|
|
22
|
+
declare const client: IClient
|
|
23
|
+
/** The animation frame timestamp when the method was called. @remarks Only available in `client.requestLoop()`. */
|
|
24
|
+
declare const REQUEST_TIME: DOMHighResTimeStamp
|
|
@@ -37,6 +37,7 @@ globalThis.addEventListener("keyup", keyboardEvent => {
|
|
|
37
37
|
})
|
|
38
38
|
|
|
39
39
|
globalThis.addEventListener("keydown", keyboardEvent => {
|
|
40
|
+
|
|
40
41
|
if (!keyboardEvent.repeat) {
|
|
41
42
|
/* This handles the edge case when the user is holding modifier keys that
|
|
42
43
|
* they pressed while not focused on this instance of the ecosystem. */
|
|
@@ -64,15 +65,24 @@ globalThis.addEventListener("keydown", keyboardEvent => {
|
|
|
64
65
|
|
|
65
66
|
hotKeys.pressed.add(keyboardEvent.code)
|
|
66
67
|
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
const doCombo = () => {
|
|
69
|
+
const combo = hotKeys.combo
|
|
70
|
+
const methodName = JSON.parse(_.application["hot-keys.json"] ?? "{}")[combo] ?? hotKeys.table[combo]
|
|
71
|
+
const method = methodName && (_.application[methodName] ?? hotKeys[methodName])
|
|
70
72
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
if (methodName) {
|
|
74
|
+
keyboardEvent.preventDefault()
|
|
75
|
+
if (typeof method === "function")
|
|
76
|
+
method()
|
|
77
|
+
// else warn(`Hot Keys Warning: method called ${methodName} is not defined on either the hot keys manager or the current application.`)
|
|
78
|
+
}
|
|
76
79
|
}
|
|
80
|
+
|
|
81
|
+
// Prevent a bug when the user presses keys too early.
|
|
82
|
+
if (client.hydrated)
|
|
83
|
+
doCombo()
|
|
84
|
+
else
|
|
85
|
+
client.promise.then(doCombo)
|
|
86
|
+
|
|
77
87
|
}
|
|
78
88
|
})
|
|
@@ -36,13 +36,9 @@ declare interface IPointerConfig {
|
|
|
36
36
|
doubleClick: (pointerEvent: PointerEvent) => void,
|
|
37
37
|
/** The action that should finally execute whenever the user's pointer action is complete (whether successfully or through a native pointer event cancellation). */
|
|
38
38
|
reset: () => void,
|
|
39
|
-
/**
|
|
40
|
-
*
|
|
41
|
-
* The pointerdown event responsible for launching the pointer session. */
|
|
39
|
+
/** The pointerdown event responsible for launching the pointer session. @remarks Required for all pointer handling. */
|
|
42
40
|
readonly POINTER_EVENT: PointerEvent,
|
|
43
|
-
/**
|
|
44
|
-
*
|
|
45
|
-
* The target HTML element whose pointerdown listener responsible for launching the pointer session. */
|
|
41
|
+
/** The target HTML element whose pointerdown listener responsible for launching the pointer session. @remarks Required for all pointer handling. */
|
|
46
42
|
readonly TARGET_ELEMENT: HTMLElement,
|
|
47
43
|
/** Optional helper that can automate calling TARGET_ELEMENT.focus() during the pointer session. */
|
|
48
44
|
readonly focus?: "none" | "down" | "click",
|
|
@@ -51,11 +47,7 @@ declare interface IPointerConfig {
|
|
|
51
47
|
declare const pointer: IPointer
|
|
52
48
|
|
|
53
49
|
declare const POINTER_CONFIG: IPointerConfig
|
|
54
|
-
/** The pointerdown event that the current listener is reacting to.
|
|
55
|
-
*
|
|
56
|
-
* *Available only in pointerdown event listeners.* */
|
|
50
|
+
/** The pointerdown event that the current listener is reacting to. @remarks Only in pointerdown event listeners. */
|
|
57
51
|
declare const POINTER_EVENT: PointerEvent
|
|
58
|
-
/** The element that the current pointerdown event is reacting to.
|
|
59
|
-
*
|
|
60
|
-
* *Available only in pointerdown event listeners.* */
|
|
52
|
+
/** The element that the current pointerdown event is reacting to. @remarks Only in pointerdown event listeners. */
|
|
61
53
|
declare const TARGET_ELEMENT: HTMLElement
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
declare interface IStats
|
|
2
2
|
extends ITrayItem {
|
|
3
3
|
|
|
4
|
-
// Runtime Properties.
|
|
5
|
-
/** If in the client environment, the computed framerate of the application. Null otherwise. */
|
|
6
|
-
readonly fps?: number
|
|
7
|
-
/** If in the client environment, the time (taken from _.now) of the last loop evaluation. Null otherwise. */
|
|
8
|
-
readonly mark?: DOMHighResTimeStamp
|
|
9
|
-
/** If in the client environment, the average length of time each frame is on screen in milliseconds. Null otherwise. */
|
|
10
|
-
readonly meanFrameTime?: number
|
|
11
4
|
}
|
|
12
5
|
|
|
13
6
|
/** The framerate monitor, which sits in the taskbar tray. */
|
|
@@ -23,9 +23,7 @@ declare interface IDesktopWindowModel {
|
|
|
23
23
|
readonly height: number
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/** The host of the application to render.
|
|
27
|
-
*
|
|
28
|
-
* *Only available in `renderTaskHTML` methods.* */
|
|
26
|
+
/** The host of the application to render. @remarks Only in `renderTaskHTML` methods. */
|
|
29
27
|
declare const APPLICATION_HOST: string
|
|
30
28
|
declare const INSTANCES: IDesktopWindow[]
|
|
31
29
|
declare const INSTANCE: IDesktopWindow
|
package/src/type.d.ts
CHANGED
|
@@ -50,9 +50,7 @@ declare interface IEcosystem
|
|
|
50
50
|
readonly setRoute(REQUEST_URL: string): void
|
|
51
51
|
/** Performs automated build-time unit tests to validate the state of the build. */
|
|
52
52
|
readonly validate(): void
|
|
53
|
-
/**
|
|
54
|
-
*
|
|
55
|
-
* Navigates to the given host by setting the current location. An undo point is automatically set by the browser. */
|
|
53
|
+
/** Navigates to the given host by setting the current location. An undo point is automatically set by the browser. @remarks Client-only */
|
|
56
54
|
readonly gotoApplication(HOST: string): void
|
|
57
55
|
|
|
58
56
|
// Runtime Properties.
|
|
@@ -68,15 +66,11 @@ declare interface IEcosystem
|
|
|
68
66
|
readonly landingModel: string
|
|
69
67
|
/** The routeID of the desired landing page, as computed from data parameters during the initial boot process. */
|
|
70
68
|
readonly landingRouteID: bigint
|
|
71
|
-
/** If in the client environment, an integer ID representing the application frame loop's current pending frame request. Null, otherwise. */
|
|
72
|
-
readonly frameRequest: number | null
|
|
73
69
|
/** A boolean that is set to `true` as soon as the route ID is set for the first time. */
|
|
74
70
|
readonly initialized: undefined | true
|
|
75
71
|
}
|
|
76
72
|
|
|
77
|
-
/**
|
|
78
|
-
*
|
|
79
|
-
* The serialized version should not include any values that are added during or after recursively hydrating the part tree. */
|
|
73
|
+
/** This is the root part of the ecosystem, considered the ecosystem itself. @remarks When JSON stringified, it should inline all information compiled from the git repo in node by the build process. The serialized version should not include any values that are added during or after recursively hydrating the part tree. This means that all runtime values should be non-enumerable and defined using the `define()` method. */
|
|
80
74
|
declare const _: IEcosystem
|
|
81
75
|
/** A shorthand for document.querySelector */
|
|
82
76
|
declare const Q: typeof document.querySelector
|
|
@@ -261,9 +255,7 @@ declare class MethodConstant {
|
|
|
261
255
|
/** Idempotent function that ensures that the constant and all its recursive dependencies (`requirements`) are declared (added to the source file output). Marks the constant as used. */
|
|
262
256
|
ensureDeclarationAndDependencies(): void
|
|
263
257
|
}
|
|
264
|
-
/** The incoming request url string.
|
|
265
|
-
*
|
|
266
|
-
* Available only in _.setRoute(). */
|
|
258
|
+
/** The incoming request url string. @remarks Only in _.setRoute(). */
|
|
267
259
|
declare const REQUEST_URL: string
|
|
268
260
|
/** A host-keyed map of all parts in the ecosystem. */
|
|
269
261
|
declare const partsByHost: Record<string, IPartAny>
|
|
@@ -316,6 +308,18 @@ declare class FenwickTree {
|
|
|
316
308
|
declare class Vector {
|
|
317
309
|
static magnitude(vector: IVector): number
|
|
318
310
|
static normalize(vector: IVector): IVector
|
|
311
|
+
static sign(vector: IVector): IVector
|
|
312
|
+
static floor(vector: IVector): IVector
|
|
313
|
+
/** Performs the given binary operation on the two values, which can each be either a vector or a number. If at least one of the values is a vector, returns a vector. Otherwise, returns a number. */
|
|
314
|
+
static operate(value1: IVector | number, value2: IVector | number, operation: (a, b) => number): IVector | number
|
|
315
|
+
/** Adds the two values, which can each be either a vector or a number. If at least one of the values is a vector, returns a vector. Otherwise, returns a number. */
|
|
316
|
+
static add(value1: IVector | number, value2: IVector | number): IVector | number
|
|
317
|
+
/** Subtracts the two values, which can each be either a vector or a number. If at least one of the values is a vector, returns a vector. Otherwise, returns a number. */
|
|
318
|
+
static subtract(value1: IVector | number, value2: IVector | number): IVector | number
|
|
319
|
+
/** Multiplies the two values, which can each be either a vector or a number. If at least one of the values is a vector, returns a vector. Otherwise, returns a number. */
|
|
320
|
+
static multiply(value1: IVector | number, value2: IVector | number): IVector | number
|
|
321
|
+
static dot(vector1: IVector, vector2: IVector): number
|
|
319
322
|
}
|
|
320
323
|
|
|
321
|
-
declare type IVector = Record<string, number>
|
|
324
|
+
declare type IVector = Record<string, number>
|
|
325
|
+
declare type IVector2 = { x: number, y: number }
|
package/src/loop-distribute.js
DELETED