@gnsx/three 0.185.14 → 0.185.16

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gnsx/three",
3
- "version": "0.185.14",
3
+ "version": "0.185.16",
4
4
  "description": "JavaScript 3D library",
5
5
  "type": "module",
6
6
  "main": "./build/three.cjs",
package/src/Three.Core.js CHANGED
@@ -102,6 +102,20 @@ export { InstancedBufferAttribute } from './core/InstancedBufferAttribute.js';
102
102
  export { GLBufferAttribute } from './core/GLBufferAttribute.js';
103
103
  export * from './core/BufferAttribute.js';
104
104
  export { Object3D } from './core/Object3D.js';
105
+ // WITH_GENESYS
106
+ export { NodePath } from './core/NodePath.js';
107
+ export {
108
+ isValidNodeId,
109
+ nodeIdToString,
110
+ nodeIdFromString,
111
+ configureNodeIdSeed,
112
+ generateNodeId,
113
+ allocateNodeId,
114
+ nodeIdFromKey,
115
+ collectSiblingNodeIds,
116
+ ensureUniqueNodeIdAmongParentChildren
117
+ } from './core/nodeId.js';
118
+ // !WITH_GENESYS
105
119
  export { Raycaster } from './core/Raycaster.js';
106
120
  export { Layers } from './core/Layers.js';
107
121
  export { EventDispatcher } from './core/EventDispatcher.js';
@@ -115,6 +129,9 @@ export { BezierInterpolant } from './math/interpolants/BezierInterpolant.js';
115
129
  export { Interpolant } from './math/Interpolant.js';
116
130
  export { Triangle } from './math/Triangle.js';
117
131
  export { MathUtils } from './math/MathUtils.js';
132
+ // WITH_GENESYS
133
+ export { hashStringToUint32, xorshift32, randomSeedUint32, XorShift32 } from './math/XorShift32.js';
134
+ // !WITH_GENESYS
118
135
  export { Spherical } from './math/Spherical.js';
119
136
  export { Cylindrical } from './math/Cylindrical.js';
120
137
  export { Plane } from './math/Plane.js';
@@ -0,0 +1,208 @@
1
+ // WITH_GENESYS
2
+ /**
3
+ * Path of sibling-local nodeIds identifying a node in the scene tree.
4
+ * String form: `"a1b2/c3d4"` (4-hex segments separated by `/`).
5
+ */
6
+
7
+ import { isValidNodeId, nodeIdFromString, nodeIdToString } from './nodeId.js';
8
+
9
+ class NodePath {
10
+
11
+ /**
12
+ * @param {readonly number[]} [ids]
13
+ */
14
+ constructor( ids = [] ) {
15
+
16
+ const normalized = [];
17
+ for ( const id of ids ) {
18
+
19
+ if ( ! isValidNodeId( id ) ) {
20
+
21
+ throw new Error( `[NodePath] Invalid NodeId segment: ${ String( id ) }` );
22
+
23
+ }
24
+
25
+ normalized.push( id & 0xffff );
26
+
27
+ }
28
+
29
+ this.ids = normalized;
30
+
31
+ }
32
+
33
+ /**
34
+ * @param {...number} ids
35
+ * @return {NodePath}
36
+ */
37
+ static fromIds( ...ids ) {
38
+
39
+ return new NodePath( ids );
40
+
41
+ }
42
+
43
+ /**
44
+ * Parses `"a1b2/c3d4"`. Empty / whitespace-only string yields {@link NodePath.empty}.
45
+ * @param {string} path
46
+ * @return {NodePath}
47
+ */
48
+ static fromString( path ) {
49
+
50
+ const trimmed = path.trim();
51
+ if ( trimmed.length === 0 ) {
52
+
53
+ return NodePath.empty;
54
+
55
+ }
56
+
57
+ const parts = trimmed.split( '/' );
58
+ const ids = [];
59
+ for ( const part of parts ) {
60
+
61
+ if ( part.length === 0 ) {
62
+
63
+ throw new Error( `[NodePath] Empty segment in path "${ path }"` );
64
+
65
+ }
66
+
67
+ const id = nodeIdFromString( part );
68
+ if ( id === null ) {
69
+
70
+ throw new Error( `[NodePath] Invalid segment "${ part }" in path "${ path }"` );
71
+
72
+ }
73
+
74
+ ids.push( id );
75
+
76
+ }
77
+
78
+ return new NodePath( ids );
79
+
80
+ }
81
+
82
+ /**
83
+ * @param {NodePath|string} path
84
+ * @return {NodePath}
85
+ */
86
+ static coerce( path ) {
87
+
88
+ return typeof path === 'string' ? NodePath.fromString( path ) : path;
89
+
90
+ }
91
+
92
+ /** @return {number} */
93
+ get length() {
94
+
95
+ return this.ids.length;
96
+
97
+ }
98
+
99
+ /** @return {boolean} */
100
+ get isEmpty() {
101
+
102
+ return this.ids.length === 0;
103
+
104
+ }
105
+
106
+ /** @return {string} */
107
+ toString() {
108
+
109
+ return this.ids.map( nodeIdToString ).join( '/' );
110
+
111
+ }
112
+
113
+ /**
114
+ * @param {NodePath} other
115
+ * @return {boolean}
116
+ */
117
+ equals( other ) {
118
+
119
+ if ( this.ids.length !== other.ids.length ) {
120
+
121
+ return false;
122
+
123
+ }
124
+
125
+ for ( let i = 0; i < this.ids.length; i ++ ) {
126
+
127
+ if ( this.ids[ i ] !== other.ids[ i ] ) {
128
+
129
+ return false;
130
+
131
+ }
132
+
133
+ }
134
+
135
+ return true;
136
+
137
+ }
138
+
139
+ /**
140
+ * @param {NodePath} prefix
141
+ * @return {boolean}
142
+ */
143
+ startsWith( prefix ) {
144
+
145
+ if ( prefix.ids.length > this.ids.length ) {
146
+
147
+ return false;
148
+
149
+ }
150
+
151
+ for ( let i = 0; i < prefix.ids.length; i ++ ) {
152
+
153
+ if ( this.ids[ i ] !== prefix.ids[ i ] ) {
154
+
155
+ return false;
156
+
157
+ }
158
+
159
+ }
160
+
161
+ return true;
162
+
163
+ }
164
+
165
+ /**
166
+ * @param {number} start
167
+ * @param {number} [end]
168
+ * @return {NodePath}
169
+ */
170
+ slice( start, end ) {
171
+
172
+ return new NodePath( this.ids.slice( start, end ) );
173
+
174
+ }
175
+
176
+ /**
177
+ * @param {NodePath} relative
178
+ * @return {NodePath}
179
+ */
180
+ concat( relative ) {
181
+
182
+ return new NodePath( [ ...this.ids, ...relative.ids ] );
183
+
184
+ }
185
+
186
+ /**
187
+ * If this path starts with `ancestor`, returns the remainder; otherwise `null`.
188
+ * @param {NodePath} ancestor
189
+ * @return {?NodePath}
190
+ */
191
+ relativeFrom( ancestor ) {
192
+
193
+ if ( ! this.startsWith( ancestor ) ) {
194
+
195
+ return null;
196
+
197
+ }
198
+
199
+ return this.slice( ancestor.length );
200
+
201
+ }
202
+
203
+ }
204
+
205
+ NodePath.empty = new NodePath( [] );
206
+
207
+ export { NodePath };
208
+ // !WITH_GENESYS
@@ -7,6 +7,15 @@ import { Layers } from './Layers.js';
7
7
  import { Matrix3 } from '../math/Matrix3.js';
8
8
  import { generateUUID } from '../math/MathUtils.js';
9
9
  import { error } from '../utils.js';
10
+ // WITH_GENESYS
11
+ import {
12
+ allocateNodeId,
13
+ ensureUniqueNodeIdAmongParentChildren,
14
+ generateNodeId,
15
+ isValidNodeId
16
+ } from './nodeId.js';
17
+ import { NodePath } from './NodePath.js';
18
+ // !WITH_GENESYS
10
19
 
11
20
  let _object3DId = 0;
12
21
 
@@ -96,6 +105,16 @@ class Object3D extends EventDispatcher {
96
105
  */
97
106
  this.uuid = generateUUID();
98
107
 
108
+ // WITH_GENESYS
109
+ /**
110
+ * Sibling-local 16-bit identity for scene-graph addressing ({@link NodePath}).
111
+ * `0` is valid. Reminted on parent attach when colliding with a sibling.
112
+ *
113
+ * @type {number}
114
+ */
115
+ this.nodeId = generateNodeId();
116
+ // !WITH_GENESYS
117
+
99
118
  /**
100
119
  * The name of the 3D object.
101
120
  *
@@ -778,6 +797,9 @@ class Object3D extends EventDispatcher {
778
797
  if ( object && object.isObject3D ) {
779
798
 
780
799
  object.removeFromParent();
800
+ // WITH_GENESYS
801
+ ensureUniqueNodeIdAmongParentChildren( object, this );
802
+ // !WITH_GENESYS
781
803
  object.parent = this;
782
804
  this.children.push( object );
783
805
 
@@ -903,6 +925,9 @@ class Object3D extends EventDispatcher {
903
925
  object.applyMatrix4( _m1 );
904
926
 
905
927
  object.removeFromParent();
928
+ // WITH_GENESYS
929
+ ensureUniqueNodeIdAmongParentChildren( object, this );
930
+ // !WITH_GENESYS
906
931
  object.parent = this;
907
932
  this.children.push( object );
908
933
 
@@ -918,6 +943,166 @@ class Object3D extends EventDispatcher {
918
943
 
919
944
  }
920
945
 
946
+ // WITH_GENESYS
947
+ /**
948
+ * Assigns a {@link nodeId} when unset or outside the uint16 range.
949
+ * @return {number}
950
+ */
951
+ ensureNodeId() {
952
+
953
+ if ( ! isValidNodeId( this.nodeId ) ) {
954
+
955
+ this.nodeId = generateNodeId();
956
+
957
+ }
958
+
959
+ return this.nodeId;
960
+
961
+ }
962
+
963
+ /**
964
+ * Replaces {@link nodeId} with a newly generated value (e.g. prefab instance roots).
965
+ * @param {Iterable<number>} [existingSiblingIds]
966
+ * @return {number}
967
+ */
968
+ remintNodeId( existingSiblingIds ) {
969
+
970
+ this.nodeId = existingSiblingIds !== undefined
971
+ ? allocateNodeId( existingSiblingIds )
972
+ : generateNodeId();
973
+ return this.nodeId;
974
+
975
+ }
976
+
977
+ /**
978
+ * Direct child with the given {@link nodeId}, or `null`.
979
+ * @param {number} id
980
+ * @return {?Object3D}
981
+ */
982
+ findChildByNodeId( id ) {
983
+
984
+ for ( let i = 0; i < this.children.length; i ++ ) {
985
+
986
+ const child = this.children[ i ];
987
+ if ( child.nodeId === id ) {
988
+
989
+ return child;
990
+
991
+ }
992
+
993
+ }
994
+
995
+ return null;
996
+
997
+ }
998
+
999
+ /**
1000
+ * Resolves a path relative to this node. An empty path returns `this`.
1001
+ * @param {NodePath|string} path
1002
+ * @return {?Object3D}
1003
+ */
1004
+ resolvePath( path ) {
1005
+
1006
+ const nodePath = NodePath.coerce( path );
1007
+ if ( nodePath.isEmpty ) {
1008
+
1009
+ return this;
1010
+
1011
+ }
1012
+
1013
+ let current = this;
1014
+ for ( let i = 0; i < nodePath.ids.length; i ++ ) {
1015
+
1016
+ const next = current.findChildByNodeId( nodePath.ids[ i ] );
1017
+ if ( ! next ) {
1018
+
1019
+ return null;
1020
+
1021
+ }
1022
+
1023
+ current = next;
1024
+
1025
+ }
1026
+
1027
+ return current;
1028
+
1029
+ }
1030
+
1031
+ /**
1032
+ * Builds a {@link NodePath} for this node.
1033
+ * - Without `root`: absolute path from the topmost non-Scene ancestor down to this node.
1034
+ * - With `root`: path relative to `root` (empty when `this === root`); `null` when not under `root`.
1035
+ * @param {Object3D} [root]
1036
+ * @return {?NodePath}
1037
+ */
1038
+ getNodePath( root ) {
1039
+
1040
+ if ( root !== undefined ) {
1041
+
1042
+ if ( this === root ) {
1043
+
1044
+ return NodePath.empty;
1045
+
1046
+ }
1047
+
1048
+ const absolute = this.getNodePath();
1049
+ const rootAbsolute = root.getNodePath();
1050
+ if ( ! absolute || ! rootAbsolute ) {
1051
+
1052
+ return null;
1053
+
1054
+ }
1055
+
1056
+ return absolute.relativeFrom( rootAbsolute );
1057
+
1058
+ }
1059
+
1060
+ const ids = [];
1061
+ let current = this;
1062
+ while ( current ) {
1063
+
1064
+ ids.push( current.ensureNodeId() );
1065
+ const parent = current.parent;
1066
+ // Scene / World containers are not part of authored NodePaths.
1067
+ if ( parent === null || parent.isScene === true ) {
1068
+
1069
+ break;
1070
+
1071
+ }
1072
+
1073
+ current = parent;
1074
+
1075
+ }
1076
+
1077
+ ids.reverse();
1078
+ return new NodePath( ids );
1079
+
1080
+ }
1081
+
1082
+ /**
1083
+ * Ensures every Object3D in this subtree has a valid {@link nodeId}, unique among its siblings.
1084
+ */
1085
+ ensureNodeIdsInSubtree() {
1086
+
1087
+ this.ensureNodeId();
1088
+ const taken = new Set();
1089
+ for ( let i = 0; i < this.children.length; i ++ ) {
1090
+
1091
+ const child = this.children[ i ];
1092
+ if ( ! isValidNodeId( child.nodeId ) || taken.has( child.nodeId ) ) {
1093
+
1094
+ child.nodeId = allocateNodeId( taken );
1095
+
1096
+ }
1097
+
1098
+ taken.add( child.nodeId );
1099
+ child.ensureNodeIdsInSubtree();
1100
+
1101
+ }
1102
+
1103
+ }
1104
+ // !WITH_GENESYS
1105
+
921
1106
  /**
922
1107
  * Searches through the 3D object and its children, starting with the 3D object
923
1108
  * itself, and returns the first with a matching ID.
@@ -1320,6 +1505,10 @@ class Object3D extends EventDispatcher {
1320
1505
  object.uuid = this.uuid;
1321
1506
  object.type = this.type;
1322
1507
 
1508
+ // WITH_GENESYS
1509
+ object.nodeId = this.nodeId;
1510
+ // !WITH_GENESYS
1511
+
1323
1512
  if ( this.name !== '' ) object.name = this.name;
1324
1513
  if ( this.castShadow === true ) object.castShadow = true;
1325
1514
  if ( this.receiveShadow === true ) object.receiveShadow = true;
@@ -1630,6 +1819,8 @@ class Object3D extends EventDispatcher {
1630
1819
  this.visible = source.visible;
1631
1820
  // WITH_GENESYS
1632
1821
  this.selfHidden = source.selfHidden;
1822
+ // Keep nodeId; reminted on attach if it collides with a sibling.
1823
+ this.nodeId = source.nodeId;
1633
1824
  // !WITH_GENESYS
1634
1825
 
1635
1826
  this.castShadow = source.castShadow;
@@ -0,0 +1,168 @@
1
+ // WITH_GENESYS
2
+ /**
3
+ * Sibling-local 16-bit node identities.
4
+ *
5
+ * `null` / `undefined` mean unset. `0` is a valid NodeId.
6
+ */
7
+
8
+ import { hashStringToUint32, randomSeedUint32, XorShift32 } from '../math/XorShift32.js';
9
+
10
+ /**
11
+ * True when `id` is a finite integer in the uint16 range (including 0).
12
+ * @param {unknown} id
13
+ * @return {boolean}
14
+ */
15
+ function isValidNodeId( id ) {
16
+
17
+ return typeof id === 'number' && Number.isInteger( id ) && id >= 0 && id <= 0xffff;
18
+
19
+ }
20
+
21
+ /**
22
+ * Canonical display form: 4 lowercase hex characters.
23
+ * @param {number} id
24
+ * @return {string}
25
+ */
26
+ function nodeIdToString( id ) {
27
+
28
+ return ( id & 0xffff ).toString( 16 ).padStart( 4, '0' );
29
+
30
+ }
31
+
32
+ /**
33
+ * Parses a hex node-id segment (1–4 hex digits).
34
+ * @param {string} value
35
+ * @return {?number}
36
+ */
37
+ function nodeIdFromString( value ) {
38
+
39
+ const trimmed = value.trim();
40
+ if ( ! /^[0-9a-fA-F]{1,4}$/.test( trimmed ) ) {
41
+
42
+ return null;
43
+
44
+ }
45
+
46
+ return Number.parseInt( trimmed, 16 ) & 0xffff;
47
+
48
+ }
49
+
50
+ const defaultNodeIdRng = new XorShift32();
51
+
52
+ /**
53
+ * Seeds the process-wide NodeId RNG for deterministic construction.
54
+ * Pass `undefined` / omit to restore a non-deterministic stream.
55
+ * @param {string} [seed]
56
+ */
57
+ function configureNodeIdSeed( seed ) {
58
+
59
+ if ( seed == null ) {
60
+
61
+ defaultNodeIdRng.setState( randomSeedUint32() );
62
+ return;
63
+
64
+ }
65
+
66
+ defaultNodeIdRng.setState( hashStringToUint32( seed ) );
67
+
68
+ }
69
+
70
+ /**
71
+ * Generates a random NodeId from the process-wide default RNG.
72
+ * @param {XorShift32} [rng]
73
+ * @return {number}
74
+ */
75
+ function generateNodeId( rng = defaultNodeIdRng ) {
76
+
77
+ return rng.nextUint16();
78
+
79
+ }
80
+
81
+ /**
82
+ * Allocates a NodeId not present in `existing`.
83
+ * @param {Iterable<number>|ReadonlySet<number>} existing
84
+ * @param {XorShift32} [rng]
85
+ * @return {number}
86
+ */
87
+ function allocateNodeId( existing, rng = defaultNodeIdRng ) {
88
+
89
+ const taken = existing instanceof Set ? existing : new Set( existing );
90
+ if ( taken.size >= 0x10000 ) {
91
+
92
+ throw new Error( '[nodeId] Cannot allocate NodeId: all 65536 sibling ids are in use' );
93
+
94
+ }
95
+
96
+ let id = rng.nextUint16();
97
+ while ( taken.has( id ) ) {
98
+
99
+ id = rng.nextUint16();
100
+
101
+ }
102
+
103
+ return id;
104
+
105
+ }
106
+
107
+ /**
108
+ * Deterministic NodeId from a stable key (first-try candidate for remints).
109
+ * @param {string} key
110
+ * @return {number}
111
+ */
112
+ function nodeIdFromKey( key ) {
113
+
114
+ return new XorShift32( key ).nextUint16();
115
+
116
+ }
117
+
118
+ /**
119
+ * Collects nodeId values from a sibling set.
120
+ * @param {Iterable<{nodeId?: ?number}>} nodes
121
+ * @param {Object} [except]
122
+ * @return {Set<number>}
123
+ */
124
+ function collectSiblingNodeIds( nodes, except ) {
125
+
126
+ const taken = new Set();
127
+ for ( const node of nodes ) {
128
+
129
+ if ( node !== except && isValidNodeId( node.nodeId ) ) {
130
+
131
+ taken.add( node.nodeId );
132
+
133
+ }
134
+
135
+ }
136
+
137
+ return taken;
138
+
139
+ }
140
+
141
+ /**
142
+ * Ensures `object.nodeId` is unique among `parent.children`.
143
+ * @param {import('./Object3D.js').Object3D} object
144
+ * @param {import('./Object3D.js').Object3D} parent
145
+ */
146
+ function ensureUniqueNodeIdAmongParentChildren( object, parent ) {
147
+
148
+ const taken = collectSiblingNodeIds( parent.children, object );
149
+ if ( ! isValidNodeId( object.nodeId ) || taken.has( object.nodeId ) ) {
150
+
151
+ object.nodeId = allocateNodeId( taken );
152
+
153
+ }
154
+
155
+ }
156
+
157
+ export {
158
+ isValidNodeId,
159
+ nodeIdToString,
160
+ nodeIdFromString,
161
+ configureNodeIdSeed,
162
+ generateNodeId,
163
+ allocateNodeId,
164
+ nodeIdFromKey,
165
+ collectSiblingNodeIds,
166
+ ensureUniqueNodeIdAmongParentChildren
167
+ };
168
+ // !WITH_GENESYS
@@ -1128,6 +1128,10 @@ class ObjectLoader extends Loader {
1128
1128
 
1129
1129
  object.uuid = data.uuid;
1130
1130
 
1131
+ // WITH_GENESYS
1132
+ if ( data.nodeId !== undefined ) object.nodeId = data.nodeId;
1133
+ // !WITH_GENESYS
1134
+
1131
1135
  if ( data.name !== undefined ) object.name = data.name;
1132
1136
 
1133
1137
  if ( data.matrix !== undefined ) {