@waica/engine 0.19.0 → 0.19.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.
@@ -1,4 +1,5 @@
1
1
  import { anchoredPiecesOf } from './ui.js';
2
+ import { componentClassOf } from './component-registry.js';
2
3
  export const RUNTIME_PROJECTION_LIMITS = {
3
4
  depth: 5,
4
5
  entries: 100,
@@ -11,7 +12,7 @@ function marker(context, path, kind, detail = {}) {
11
12
  return { $waica: kind, ...detail };
12
13
  }
13
14
  function isPlainRecord(value) {
14
- const prototype = Object.getPrototypeOf(value);
15
+ const prototype = Reflect.getPrototypeOf(value);
15
16
  return prototype === Object.prototype || prototype === null;
16
17
  }
17
18
  const textEncoder = new TextEncoder();
@@ -41,17 +42,8 @@ function projectValue(value, path, context, depth = 0) {
41
42
  }
42
43
  if (value === null || typeof value === 'boolean')
43
44
  return value;
44
- if (typeof value === 'string') {
45
- const bytes = utf8Bytes(value);
46
- if (bytes <= RUNTIME_PROJECTION_LIMITS.stringBytes)
47
- return value;
48
- return marker(context, path, 'truncated', {
49
- reason: 'string',
50
- preview: stringPreview(value, RUNTIME_PROJECTION_LIMITS.stringBytes),
51
- originalLength: value.length,
52
- originalBytes: bytes,
53
- });
54
- }
45
+ if (typeof value === 'string')
46
+ return projectString(value, path, context);
55
47
  if (typeof value === 'bigint')
56
48
  return { $waica: 'bigint', value: value.toString() };
57
49
  if (typeof value === 'number') {
@@ -66,69 +58,93 @@ function projectValue(value, path, context, depth = 0) {
66
58
  if (previousPath)
67
59
  return marker(context, path, 'cycle', { path: previousPath });
68
60
  context.seen.set(value, path);
61
+ return projectObject(value, { path, depth }, context);
62
+ }
63
+ function projectString(value, path, context) {
64
+ const bytes = utf8Bytes(value);
65
+ if (bytes <= RUNTIME_PROJECTION_LIMITS.stringBytes)
66
+ return value;
67
+ return marker(context, path, 'truncated', {
68
+ reason: 'string',
69
+ preview: stringPreview(value, RUNTIME_PROJECTION_LIMITS.stringBytes),
70
+ originalLength: value.length,
71
+ originalBytes: bytes,
72
+ });
73
+ }
74
+ /** Projects a not-yet-seen object: dates, collections and plain records; anything else is unsupported. */
75
+ function projectObject(value, site, context) {
76
+ const { path } = site;
69
77
  if (value instanceof Date) {
70
78
  return Number.isFinite(value.getTime())
71
79
  ? { $waica: 'date', value: value.toISOString() }
72
80
  : marker(context, path, 'error', { message: 'Invalid Date' });
73
81
  }
74
- if (Array.isArray(value)) {
75
- const entries = value.slice(0, RUNTIME_PROJECTION_LIMITS.entries)
76
- .map((entry, index) => projectValue(entry, `${path}[${index}]`, context, depth + 1));
77
- const omitted = value.length - entries.length;
78
- if (omitted > 0) {
79
- entries.push(marker(context, path, 'truncated', { reason: 'entries', omitted }));
80
- }
81
- return entries;
82
- }
83
- if (value instanceof Map) {
84
- const sourceEntries = [...value.entries()];
85
- const entries = sourceEntries.slice(0, RUNTIME_PROJECTION_LIMITS.entries)
86
- .map(([key, entry], index) => [
87
- projectValue(key, `${path}.entries[${index}].key`, context, depth + 1),
88
- projectValue(entry, `${path}.entries[${index}].value`, context, depth + 1),
89
- ]);
90
- const omitted = sourceEntries.length - entries.length;
91
- return {
92
- $waica: 'map',
93
- entries,
94
- ...(omitted > 0
95
- ? { truncated: marker(context, path, 'truncated', { reason: 'entries', omitted }) }
96
- : {}),
97
- };
98
- }
99
- if (value instanceof Set) {
100
- const sourceValues = [...value];
101
- const values = sourceValues.slice(0, RUNTIME_PROJECTION_LIMITS.entries)
102
- .map((entry, index) => projectValue(entry, `${path}.values[${index}]`, context, depth + 1));
103
- const omitted = sourceValues.length - values.length;
104
- return {
105
- $waica: 'set',
106
- values,
107
- ...(omitted > 0
108
- ? { truncated: marker(context, path, 'truncated', { reason: 'entries', omitted }) }
109
- : {}),
110
- };
111
- }
112
- if (isPlainRecord(value)) {
113
- const keys = Object.keys(value).sort();
114
- const projected = Object.fromEntries(keys.slice(0, RUNTIME_PROJECTION_LIMITS.entries)
115
- .map((key) => [
116
- key,
117
- projectValue(value[key], `${path}.${key}`, context, depth + 1),
118
- ]));
119
- const omitted = keys.length - Object.keys(projected).length;
120
- if (omitted === 0)
121
- return projected;
122
- return marker(context, path, 'truncated', {
123
- reason: 'entries',
124
- omitted,
125
- value: projected,
126
- });
127
- }
82
+ if (Array.isArray(value))
83
+ return projectArray(value, site, context);
84
+ if (value instanceof Map)
85
+ return projectMap(value, site, context);
86
+ if (value instanceof Set)
87
+ return projectSet(value, site, context);
88
+ if (isPlainRecord(value))
89
+ return projectRecord(value, site, context);
128
90
  return marker(context, path, 'unsupported', {
129
91
  type: value.constructor?.name ?? 'object',
130
92
  });
131
93
  }
94
+ function projectArray(value, { path, depth }, context) {
95
+ const entries = value.slice(0, RUNTIME_PROJECTION_LIMITS.entries)
96
+ .map((entry, index) => projectValue(entry, `${path}[${index}]`, context, depth + 1));
97
+ const omitted = value.length - entries.length;
98
+ if (omitted > 0) {
99
+ entries.push(marker(context, path, 'truncated', { reason: 'entries', omitted }));
100
+ }
101
+ return entries;
102
+ }
103
+ function projectMap(value, { path, depth }, context) {
104
+ const sourceEntries = [...value.entries()];
105
+ const entries = sourceEntries.slice(0, RUNTIME_PROJECTION_LIMITS.entries)
106
+ .map(([key, entry], index) => [
107
+ projectValue(key, `${path}.entries[${index}].key`, context, depth + 1),
108
+ projectValue(entry, `${path}.entries[${index}].value`, context, depth + 1),
109
+ ]);
110
+ const omitted = sourceEntries.length - entries.length;
111
+ return {
112
+ $waica: 'map',
113
+ entries,
114
+ ...(omitted > 0
115
+ ? { truncated: marker(context, path, 'truncated', { reason: 'entries', omitted }) }
116
+ : {}),
117
+ };
118
+ }
119
+ function projectSet(value, { path, depth }, context) {
120
+ const sourceValues = [...value];
121
+ const values = sourceValues.slice(0, RUNTIME_PROJECTION_LIMITS.entries)
122
+ .map((entry, index) => projectValue(entry, `${path}.values[${index}]`, context, depth + 1));
123
+ const omitted = sourceValues.length - values.length;
124
+ return {
125
+ $waica: 'set',
126
+ values,
127
+ ...(omitted > 0
128
+ ? { truncated: marker(context, path, 'truncated', { reason: 'entries', omitted }) }
129
+ : {}),
130
+ };
131
+ }
132
+ function projectRecord(value, { path, depth }, context) {
133
+ const keys = Object.keys(value).sort();
134
+ const projected = Object.fromEntries(keys.slice(0, RUNTIME_PROJECTION_LIMITS.entries)
135
+ .map((key) => [
136
+ key,
137
+ projectValue(value[key], `${path}.${key}`, context, depth + 1),
138
+ ]));
139
+ const omitted = keys.length - Object.keys(projected).length;
140
+ if (omitted === 0)
141
+ return projected;
142
+ return marker(context, path, 'truncated', {
143
+ reason: 'entries',
144
+ omitted,
145
+ value: projected,
146
+ });
147
+ }
132
148
  function errorMessage(error) {
133
149
  return error instanceof Error ? error.message : String(error);
134
150
  }
@@ -154,7 +170,7 @@ function componentState(component, path, context) {
154
170
  continue;
155
171
  const valuePath = `${path}.${key}`;
156
172
  try {
157
- const value = component[key];
173
+ const value = Reflect.get(component, key);
158
174
  if (typeof value === 'function')
159
175
  continue;
160
176
  state[key] = projectValue(value, valuePath, context, 1);
@@ -208,6 +224,27 @@ function capAnchored(snapshot) {
208
224
  }
209
225
  return capped;
210
226
  }
227
+ /** An entity's position, rotation (with its Euler order) and scale, as plain numbers. */
228
+ function transformSnapshot(entity) {
229
+ return {
230
+ position: {
231
+ x: entity.position.x,
232
+ y: entity.position.y,
233
+ z: entity.position.z,
234
+ },
235
+ rotation: {
236
+ x: entity.node.rotation.x,
237
+ y: entity.node.rotation.y,
238
+ z: entity.node.rotation.z,
239
+ order: entity.node.rotation.order,
240
+ },
241
+ scale: {
242
+ x: entity.scale.x,
243
+ y: entity.scale.y,
244
+ z: entity.scale.z,
245
+ },
246
+ };
247
+ }
211
248
  export class RuntimeInspector {
212
249
  game;
213
250
  ids = new WeakMap();
@@ -227,7 +264,7 @@ export class RuntimeInspector {
227
264
  if (nameFilter && !nameFilter.has(entity.name))
228
265
  return [];
229
266
  const components = entity.components.flatMap((component, index) => {
230
- const Class = component.constructor;
267
+ const Class = componentClassOf(component);
231
268
  if (componentFilter && !componentFilter.has(Class.componentName))
232
269
  return [];
233
270
  const context = { issues: projectionIssues, seen: new Map() };
@@ -242,24 +279,7 @@ export class RuntimeInspector {
242
279
  return [{
243
280
  id,
244
281
  name: entity.name,
245
- transform: {
246
- position: {
247
- x: entity.position.x,
248
- y: entity.position.y,
249
- z: entity.position.z,
250
- },
251
- rotation: {
252
- x: entity.node.rotation.x,
253
- y: entity.node.rotation.y,
254
- z: entity.node.rotation.z,
255
- order: entity.node.rotation.order,
256
- },
257
- scale: {
258
- x: entity.scale.x,
259
- y: entity.scale.y,
260
- z: entity.scale.z,
261
- },
262
- },
282
+ transform: transformSnapshot(entity),
263
283
  components,
264
284
  }];
265
285
  });
package/dist/scene.d.ts CHANGED
@@ -86,7 +86,6 @@ export declare function resolveProps(props: Record<string, unknown> | undefined,
86
86
  * are never mutated; merged components are fresh objects.
87
87
  */
88
88
  export declare function resolveEntityComponents(entity: SceneEntityJson, prefabs?: Record<string, PrefabJson>): SceneComponentJson[];
89
- /** Instantiates a scene entity into the game. */
90
89
  export declare function spawnFromJson(game: Game, json: SceneEntityJson, registry: SceneRegistry): Entity;
91
90
  /**
92
91
  * Loads a full scene into the game, replacing whatever scene is already
package/dist/scene.js CHANGED
@@ -56,6 +56,39 @@ export function resolveEntityComponents(entity, prefabs) {
56
56
  return [...fromPrefab, ...inline];
57
57
  }
58
58
  /** Instantiates a scene entity into the game. */
59
+ /**
60
+ * Every member Component declares, checked against the class by `satisfies`:
61
+ * adding a member without listing it here fails to compile.
62
+ */
63
+ const COMPONENT_MEMBERS = {
64
+ entity: true,
65
+ game: true,
66
+ inspectState: true,
67
+ onReady: true,
68
+ onUpdate: true,
69
+ onProjectionChange: true,
70
+ onCollide: true,
71
+ onContact: true,
72
+ onInteract: true,
73
+ onDestroy: true,
74
+ };
75
+ /**
76
+ * Scene props set none of Component's own members, so as far as the type
77
+ * system is concerned they are a Partial<Component> (their other keys are the
78
+ * subclass's public props, which Entity.add assigns).
79
+ */
80
+ function isComponentProps(props) {
81
+ return Object.keys(props).every((key) => !Object.hasOwn(COMPONENT_MEMBERS, key));
82
+ }
83
+ /** Scene JSON props with any Component member dropped and reported. */
84
+ function sceneProps(props, type, entityName) {
85
+ const dropped = Object.keys(props).filter((key) => Object.hasOwn(COMPONENT_MEMBERS, key));
86
+ const kept = Object.fromEntries(Object.entries(props).filter(([key]) => !dropped.includes(key)));
87
+ if (dropped.length > 0) {
88
+ console.warn(`[waica] component "${type}" on "${entityName}" ignores scene props that name Component members: ${dropped.join(', ')}`);
89
+ }
90
+ return isComponentProps(kept) ? kept : {};
91
+ }
59
92
  export function spawnFromJson(game, json, registry) {
60
93
  const entity = game.spawn(json.name);
61
94
  if (json.position)
@@ -70,7 +103,7 @@ export function spawnFromJson(game, json, registry) {
70
103
  // code. A throwing constructor or onReady costs that one component —
71
104
  // never the rest of the scene, and never the editor hosting it.
72
105
  try {
73
- entity.add(Class, resolveProps(comp.props, registry));
106
+ entity.add(Class, sceneProps(resolveProps(comp.props, registry), comp.type, json.name));
74
107
  }
75
108
  catch (error) {
76
109
  const message = error instanceof Error ? error.message : String(error);
@@ -47,12 +47,23 @@ function eachCell(range, visit) {
47
47
  visit(cellKey(x, y));
48
48
  }
49
49
  }
50
- /**
51
- * Package-internal fresh uniform grid. Bodies above the fixed occupancy cap
52
- * remain in an overflow bucket and are conservatively visible everywhere.
53
- */
54
- export function createSpatialBroadphase(sources, cellSize, isIndexable = usableCollisionBody) {
55
- const entries = sources
50
+ /** The value of the indexed entry at `index`; buckets and overflow only hold indices into `entries`. */
51
+ function entryValue(entries, index) {
52
+ const entry = entries[index];
53
+ if (!entry)
54
+ throw new Error(`Spatial broadphase has no entry at index ${index}`);
55
+ return entry.value;
56
+ }
57
+ /** The entry index stored at `position` of a bucket that is iterated within its bounds. */
58
+ function indexAt(bucket, position) {
59
+ const index = bucket[position];
60
+ if (index === undefined)
61
+ throw new Error(`Spatial broadphase bucket has no index at position ${position}`);
62
+ return index;
63
+ }
64
+ /** The indexable sources in query order, each with the cell range it covers. */
65
+ function spatialEntries(sources, cellSize, isIndexable) {
66
+ return sources
56
67
  .map((source, serial) => {
57
68
  if (!isIndexable(source.body))
58
69
  return null;
@@ -64,6 +75,8 @@ export function createSpatialBroadphase(sources, cellSize, isIndexable = usableC
64
75
  })
65
76
  .filter((entry) => entry !== null)
66
77
  .sort((a, b) => a.order - b.order || a.serial - b.serial);
78
+ }
79
+ function spatialGrid(entries) {
67
80
  const buckets = new Map();
68
81
  const overflow = [];
69
82
  for (const [index, entry] of entries.entries()) {
@@ -79,6 +92,43 @@ export function createSpatialBroadphase(sources, cellSize, isIndexable = usableC
79
92
  buckets.set(key, [index]);
80
93
  });
81
94
  }
95
+ return { buckets, overflow };
96
+ }
97
+ /** Distinct index pairs sharing a cell, or involving an overflow entry, in ascending order. */
98
+ function candidatePairIndices(grid, entryCount) {
99
+ const pairKeys = new Set();
100
+ const width = entryCount;
101
+ const addPair = (left, right) => {
102
+ if (left === right)
103
+ return;
104
+ const first = Math.min(left, right);
105
+ const second = Math.max(left, right);
106
+ pairKeys.add(first * width + second);
107
+ };
108
+ for (const bucket of grid.buckets.values()) {
109
+ for (let left = 0; left < bucket.length; left += 1) {
110
+ for (let right = left + 1; right < bucket.length; right += 1) {
111
+ addPair(indexAt(bucket, left), indexAt(bucket, right));
112
+ }
113
+ }
114
+ }
115
+ for (const overflowIndex of grid.overflow) {
116
+ for (let index = 0; index < entryCount; index += 1) {
117
+ addPair(overflowIndex, index);
118
+ }
119
+ }
120
+ return [...pairKeys]
121
+ .map((key) => [Math.floor(key / width), key % width])
122
+ .sort(([a1, a2], [b1, b2]) => a1 - b1 || a2 - b2);
123
+ }
124
+ /**
125
+ * Package-internal fresh uniform grid. Bodies above the fixed occupancy cap
126
+ * remain in an overflow bucket and are conservatively visible everywhere.
127
+ */
128
+ export function createSpatialBroadphase(sources, cellSize, isIndexable = usableCollisionBody) {
129
+ const entries = spatialEntries(sources, cellSize, isIndexable);
130
+ const grid = spatialGrid(entries);
131
+ const { buckets, overflow } = grid;
82
132
  return {
83
133
  stats: { indexed: entries.length - overflow.length, overflow: overflow.length },
84
134
  candidates(bounds) {
@@ -92,36 +142,12 @@ export function createSpatialBroadphase(sources, cellSize, isIndexable = usableC
92
142
  });
93
143
  return [...selected]
94
144
  .sort((a, b) => a - b)
95
- .map((index) => entries[index].value);
145
+ .map((index) => entryValue(entries, index));
96
146
  },
97
147
  pairs() {
98
- const pairKeys = new Set();
99
- const width = entries.length;
100
- const addPair = (left, right) => {
101
- if (left === right)
102
- return;
103
- const first = Math.min(left, right);
104
- const second = Math.max(left, right);
105
- pairKeys.add(first * width + second);
106
- };
107
- for (const bucket of buckets.values()) {
108
- for (let left = 0; left < bucket.length; left += 1) {
109
- for (let right = left + 1; right < bucket.length; right += 1) {
110
- addPair(bucket[left], bucket[right]);
111
- }
112
- }
113
- }
114
- for (const overflowIndex of overflow) {
115
- for (let index = 0; index < entries.length; index += 1) {
116
- addPair(overflowIndex, index);
117
- }
118
- }
119
- return [...pairKeys]
120
- .map((key) => [Math.floor(key / width), key % width])
121
- .sort(([a1, a2], [b1, b2]) => a1 - b1 || a2 - b2)
122
- .map(([first, second]) => [
123
- entries[first].value,
124
- entries[second].value,
148
+ return candidatePairIndices(grid, entries.length).map(([first, second]) => [
149
+ entryValue(entries, first),
150
+ entryValue(entries, second),
125
151
  ]);
126
152
  },
127
153
  };
@@ -1,11 +1,18 @@
1
1
  import { collisionVertices, } from './collision-shape.js';
2
2
  /** Internal absolute tolerance shared by point and ray boundary rules. */
3
3
  export const SPATIAL_QUERY_EPSILON = 1e-9;
4
+ /** The item at an `index` the caller already bounded to `items.length`. */
5
+ function itemAt(items, index) {
6
+ const item = items[index];
7
+ if (item === undefined)
8
+ throw new Error(`Spatial query geometry has no item at index ${index} of ${items.length}`);
9
+ return item;
10
+ }
4
11
  function signedArea(points) {
5
12
  let twiceArea = 0;
6
13
  for (let index = 0; index < points.length; index += 1) {
7
- const point = points[index];
8
- const next = points[(index + 1) % points.length];
14
+ const point = itemAt(points, index);
15
+ const next = itemAt(points, (index + 1) % points.length);
9
16
  twiceArea += point[0] * next[1] - next[0] * point[1];
10
17
  }
11
18
  return twiceArea / 2;
@@ -46,8 +53,8 @@ function pointOnSegment(point, a, b) {
46
53
  function polygonPointLocation(polygon, point) {
47
54
  let inside = false;
48
55
  for (let index = 0, previous = polygon.length - 1; index < polygon.length; previous = index, index += 1) {
49
- const a = polygon[index];
50
- const b = polygon[previous];
56
+ const a = itemAt(polygon, index);
57
+ const b = itemAt(polygon, previous);
51
58
  if (pointOnSegment(point, a, b))
52
59
  return 'boundary';
53
60
  const crosses = (a[1] > point[1]) !== (b[1] > point[1]) &&
@@ -130,12 +137,13 @@ function groupEvents(events) {
130
137
  const sorted = [...events].sort((a, b) => a.distance === b.distance ? a.edgeOrder - b.edgeOrder : a.distance - b.distance);
131
138
  const groups = [];
132
139
  for (const event of sorted) {
133
- const current = groups.at(-1);
140
+ let current = groups.at(-1);
134
141
  if (!current || Math.abs(event.distance - current.distance) > SPATIAL_QUERY_EPSILON) {
135
- groups.push({ distance: event.distance, normals: [] });
142
+ current = { distance: event.distance, normals: [] };
143
+ groups.push(current);
136
144
  }
137
145
  if (event.normal) {
138
- groups.at(-1).normals.push({ edgeOrder: event.edgeOrder, normal: event.normal });
146
+ current.normals.push({ edgeOrder: event.edgeOrder, normal: event.normal });
139
147
  }
140
148
  }
141
149
  return groups.map((group) => ({
@@ -167,6 +175,30 @@ function selectVertexNormal(normals, direction, crossing) {
167
175
  }
168
176
  return selected;
169
177
  }
178
+ /** Every edge's ray events, for each edge with a usable outward normal. */
179
+ function polygonEdgeEvents(polygon, winding, ray) {
180
+ const events = [];
181
+ for (let index = 0; index < polygon.length; index += 1) {
182
+ const a = itemAt(polygon, index);
183
+ const b = itemAt(polygon, (index + 1) % polygon.length);
184
+ const normal = edgeNormal(a, b, winding);
185
+ if (!normal)
186
+ continue;
187
+ events.push(...edgeEvents(ray.origin, ray.direction, ray.maxDistance, a, b, normal, index));
188
+ }
189
+ return events;
190
+ }
191
+ /**
192
+ * Whether the ray enters or leaves the polygon at an event group, from where
193
+ * it was just before and just after it. A ray starting on the boundary and
194
+ * heading inside counts as entering.
195
+ */
196
+ function boundaryCrossing(distance, before, after) {
197
+ if ((before === 'outside' || (distance === 0 && before === 'boundary')) && after === 'inside') {
198
+ return 'entry';
199
+ }
200
+ return before === 'inside' && after === 'outside' ? 'exit' : null;
201
+ }
170
202
  function polygonRay(body, origin, direction, maxDistance) {
171
203
  const polygon = collisionVertices(body);
172
204
  if (!polygon.every(([x, y]) => Number.isFinite(x) && Number.isFinite(y)))
@@ -175,21 +207,12 @@ function polygonRay(body, origin, direction, maxDistance) {
175
207
  if (Math.abs(area) <= SPATIAL_QUERY_EPSILON)
176
208
  return null;
177
209
  const winding = area > 0 ? 1 : -1;
178
- const events = [];
179
- for (let index = 0; index < polygon.length; index += 1) {
180
- const a = polygon[index];
181
- const b = polygon[(index + 1) % polygon.length];
182
- const normal = edgeNormal(a, b, winding);
183
- if (!normal)
184
- continue;
185
- events.push(...edgeEvents(origin, direction, maxDistance, a, b, normal, index));
186
- }
187
- const groups = groupEvents(events);
210
+ const groups = groupEvents(polygonEdgeEvents(polygon, winding, { origin, direction, maxDistance }));
188
211
  const originLocation = polygonPointLocation(polygon, origin);
189
212
  const scale = Math.max(Math.abs(body.width), Math.abs(body.height), 1);
190
213
  const terminalProbe = Math.max(SPATIAL_QUERY_EPSILON * 4, Math.min(scale * 1e-6, 1e-3));
191
214
  for (let index = 0; index < groups.length; index += 1) {
192
- const group = groups[index];
215
+ const group = itemAt(groups, index);
193
216
  if (group.distance > maxDistance + SPATIAL_QUERY_EPSILON)
194
217
  break;
195
218
  const previous = groups[index - 1];
@@ -198,15 +221,7 @@ function polygonRay(body, origin, direction, maxDistance) {
198
221
  ? originLocation
199
222
  : polygonPointLocation(polygon, pointAlongRay(origin, direction, previous ? (previous.distance + group.distance) / 2 : group.distance / 2));
200
223
  const afterLocation = polygonPointLocation(polygon, pointAlongRay(origin, direction, next ? (group.distance + next.distance) / 2 : group.distance + terminalProbe));
201
- let crossing = null;
202
- if ((beforeLocation === 'outside' ||
203
- (group.distance === 0 && beforeLocation === 'boundary')) &&
204
- afterLocation === 'inside') {
205
- crossing = 'entry';
206
- }
207
- else if (beforeLocation === 'inside' && afterLocation === 'outside') {
208
- crossing = 'exit';
209
- }
224
+ const crossing = boundaryCrossing(group.distance, beforeLocation, afterLocation);
210
225
  if (!crossing)
211
226
  continue;
212
227
  const normal = selectVertexNormal(group.normals, direction, crossing);
@@ -257,14 +272,12 @@ function ellipseRay(body, origin, direction, maxDistance) {
257
272
  halfChordDistance <= SPATIAL_QUERY_EPSILON) {
258
273
  return null;
259
274
  }
260
- const roots = [
261
- (closestParameter - halfChordParameter) / directionScale,
262
- (closestParameter + halfChordParameter) / directionScale,
263
- ];
264
- const rawDistance = roots[0] >= -SPATIAL_QUERY_EPSILON
265
- ? roots[0]
266
- : roots[1] > SPATIAL_QUERY_EPSILON
267
- ? roots[1]
275
+ const nearRoot = (closestParameter - halfChordParameter) / directionScale;
276
+ const farRoot = (closestParameter + halfChordParameter) / directionScale;
277
+ const rawDistance = nearRoot >= -SPATIAL_QUERY_EPSILON
278
+ ? nearRoot
279
+ : farRoot > SPATIAL_QUERY_EPSILON
280
+ ? farRoot
268
281
  : undefined;
269
282
  if (rawDistance === undefined || !inRayRange(rawDistance, maxDistance))
270
283
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waica/engine",
3
- "version": "0.19.0",
3
+ "version": "0.19.2",
4
4
  "description": "Waica game engine core — archetype-driven, web-first, 2D & 3D",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -20,7 +20,7 @@
20
20
  }
21
21
  },
22
22
  "dependencies": {
23
- "@types/three": "^0.185.1",
24
- "three": "^0.185.1"
23
+ "@types/three": "^0.186.0",
24
+ "three": "^0.186.1"
25
25
  }
26
26
  }