brep-io-kernel 1.0.20 → 1.0.22
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/README.md +4 -1
- package/dist-kernel/brep-kernel.js +10858 -9938
- package/package.json +3 -2
- package/src/BREP/Edge.js +2 -0
- package/src/BREP/Face.js +2 -0
- package/src/BREP/SolidMethods/visualize.js +372 -365
- package/src/BREP/Vertex.js +2 -17
- package/src/PartHistory.js +4 -25
- package/src/SketchSolver2D.js +3 -0
- package/src/UI/AccordionWidget.js +1 -1
- package/src/UI/EnvMonacoEditor.js +0 -3
- package/src/UI/HistoryWidget.js +12 -4
- package/src/UI/SceneListing.js +45 -7
- package/src/UI/SelectionFilter.js +903 -438
- package/src/UI/SelectionState.js +464 -0
- package/src/UI/assembly/AssemblyConstraintCollectionWidget.js +41 -1
- package/src/UI/assembly/AssemblyConstraintsWidget.js +21 -3
- package/src/UI/assembly/constraintSelectionUtils.js +3 -182
- package/src/UI/{assembly/constraintFaceUtils.js → faceUtils.js} +30 -5
- package/src/UI/featureDialogs.js +154 -69
- package/src/UI/history/HistoryCollectionWidget.js +65 -0
- package/src/UI/pmi/AnnotationCollectionWidget.js +1 -0
- package/src/UI/pmi/BaseAnnotation.js +37 -0
- package/src/UI/pmi/LabelOverlay.js +32 -0
- package/src/UI/pmi/PMIMode.js +27 -0
- package/src/UI/pmi/dimensions/AngleDimensionAnnotation.js +5 -0
- package/src/UI/pmi/dimensions/ExplodeBodyAnnotation.js +5 -0
- package/src/UI/pmi/dimensions/HoleCalloutAnnotation.js +57 -0
- package/src/UI/pmi/dimensions/LeaderAnnotation.js +5 -0
- package/src/UI/pmi/dimensions/LinearDimensionAnnotation.js +22 -16
- package/src/UI/pmi/dimensions/NoteAnnotation.js +9 -0
- package/src/UI/pmi/dimensions/RadialDimensionAnnotation.js +81 -16
- package/src/UI/toolbarButtons/orientToFaceButton.js +3 -36
- package/src/UI/toolbarButtons/registerDefaultButtons.js +2 -0
- package/src/UI/toolbarButtons/selectionStateButton.js +206 -0
- package/src/UI/viewer.js +34 -13
- package/src/assemblyConstraints/AssemblyConstraintHistory.js +18 -42
- package/src/assemblyConstraints/constraints/AngleConstraint.js +1 -0
- package/src/assemblyConstraints/constraints/DistanceConstraint.js +1 -0
- package/src/features/selectionUtils.js +21 -5
- package/src/features/sketch/SketchFeature.js +2 -2
- package/src/features/sketch/sketchSolver2D/constraintDefinitions.js +3 -2
- package/src/utils/selectionResolver.js +258 -0
|
@@ -1,185 +1,6 @@
|
|
|
1
|
+
import { resolveSelectionObject as resolveSelectionObjectBase, scoreObjectForNormal } from '../../utils/selectionResolver.js';
|
|
2
|
+
|
|
1
3
|
export function resolveSelectionObject(scene, selection, options = {}) {
|
|
2
4
|
const scoreFn = typeof options.scoreFn === 'function' ? options.scoreFn : scoreObjectForNormal;
|
|
3
|
-
return
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
function internalResolveSelectionObject(scene, selection, options) {
|
|
7
|
-
if (!scene || selection == null) return null;
|
|
8
|
-
if (selection.isObject3D) return selection;
|
|
9
|
-
|
|
10
|
-
if (Array.isArray(selection)) {
|
|
11
|
-
for (const item of selection) {
|
|
12
|
-
const resolved = internalResolveSelectionObject(scene, item, options);
|
|
13
|
-
if (resolved) return resolved;
|
|
14
|
-
}
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
if (typeof selection === 'string') {
|
|
19
|
-
return resolveObjectFromString(scene, selection, options);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (typeof selection === 'object') {
|
|
23
|
-
if (selection.isObject3D) return selection;
|
|
24
|
-
const {
|
|
25
|
-
uuid,
|
|
26
|
-
name,
|
|
27
|
-
id,
|
|
28
|
-
path,
|
|
29
|
-
reference,
|
|
30
|
-
target,
|
|
31
|
-
} = selection;
|
|
32
|
-
|
|
33
|
-
if (typeof uuid === 'string') {
|
|
34
|
-
try {
|
|
35
|
-
const found = scene.getObjectByProperty?.('uuid', uuid);
|
|
36
|
-
if (found) return found;
|
|
37
|
-
} catch { /* ignore */ }
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const resolveCandidate = (candidate) => (
|
|
41
|
-
typeof candidate === 'string'
|
|
42
|
-
? resolveObjectFromString(scene, candidate, options)
|
|
43
|
-
: null
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
const nameCandidate = typeof name === 'string'
|
|
47
|
-
? name
|
|
48
|
-
: (typeof selection?.selectionName === 'string' ? selection.selectionName : null);
|
|
49
|
-
const idCandidate = typeof id === 'string' ? id : null;
|
|
50
|
-
|
|
51
|
-
const nameResolved = resolveCandidate(nameCandidate);
|
|
52
|
-
if (nameResolved) return nameResolved;
|
|
53
|
-
|
|
54
|
-
const idResolved = resolveCandidate(idCandidate);
|
|
55
|
-
if (idResolved) return idResolved;
|
|
56
|
-
|
|
57
|
-
if (Array.isArray(path)) {
|
|
58
|
-
for (let i = path.length - 1; i >= 0; i -= 1) {
|
|
59
|
-
const segment = path[i];
|
|
60
|
-
if (typeof segment !== 'string') continue;
|
|
61
|
-
const resolved = resolveObjectFromString(scene, segment, options);
|
|
62
|
-
if (resolved) return resolved;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (reference != null) {
|
|
67
|
-
const resolved = internalResolveSelectionObject(scene, reference, options);
|
|
68
|
-
if (resolved) return resolved;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (target != null) {
|
|
72
|
-
const resolved = internalResolveSelectionObject(scene, target, options);
|
|
73
|
-
if (resolved) return resolved;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function resolveObjectFromString(scene, value, options) {
|
|
81
|
-
if (!scene || typeof value !== 'string') return null;
|
|
82
|
-
const trimmed = value.trim();
|
|
83
|
-
if (!trimmed) return null;
|
|
84
|
-
|
|
85
|
-
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
86
|
-
try {
|
|
87
|
-
const parsed = JSON.parse(trimmed);
|
|
88
|
-
if (parsed != null) {
|
|
89
|
-
const resolved = internalResolveSelectionObject(scene, parsed, options);
|
|
90
|
-
if (resolved) return resolved;
|
|
91
|
-
}
|
|
92
|
-
} catch { /* ignore JSON parse errors */ }
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const direct = findObjectByName(scene, trimmed, options.scoreFn);
|
|
96
|
-
if (direct) return direct;
|
|
97
|
-
|
|
98
|
-
if (looksLikeUUID(trimmed)) {
|
|
99
|
-
try {
|
|
100
|
-
const byUuid = scene.getObjectByProperty?.('uuid', trimmed);
|
|
101
|
-
if (byUuid) return byUuid;
|
|
102
|
-
} catch { /* ignore */ }
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const candidates = new Set();
|
|
106
|
-
candidates.add(trimmed);
|
|
107
|
-
|
|
108
|
-
const splitByDelims = trimmed.split(/›|>|\/|\||→|->/);
|
|
109
|
-
if (splitByDelims.length > 1) {
|
|
110
|
-
for (const segment of splitByDelims) {
|
|
111
|
-
const s = segment.trim();
|
|
112
|
-
if (s) candidates.add(s);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (trimmed.includes(':')) {
|
|
117
|
-
for (const segment of trimmed.split(':')) {
|
|
118
|
-
const s = segment.trim();
|
|
119
|
-
if (s) candidates.add(s);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
for (const candidate of candidates) {
|
|
124
|
-
const found = findObjectByName(scene, candidate, options.scoreFn);
|
|
125
|
-
if (found) return found;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
let fallback = null;
|
|
129
|
-
try {
|
|
130
|
-
scene.traverse?.((obj) => {
|
|
131
|
-
if (fallback || !obj?.name) return;
|
|
132
|
-
if (!trimmed.includes(obj.name)) return;
|
|
133
|
-
if (!fallback) {
|
|
134
|
-
fallback = obj;
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
const currentScore = options.scoreFn(fallback);
|
|
138
|
-
const nextScore = options.scoreFn(obj);
|
|
139
|
-
if (nextScore > currentScore || obj.name.length > fallback.name.length) {
|
|
140
|
-
fallback = obj;
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
} catch { /* ignore */ }
|
|
144
|
-
|
|
145
|
-
return fallback;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function findObjectByName(scene, name, scoreFn) {
|
|
149
|
-
if (!scene || typeof name !== 'string' || !name) return null;
|
|
150
|
-
|
|
151
|
-
if (typeof scene.traverse !== 'function') {
|
|
152
|
-
return scene?.getObjectByName?.(name) || null;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
let best = null;
|
|
156
|
-
scene.traverse((obj) => {
|
|
157
|
-
if (!obj || obj.name !== name) return;
|
|
158
|
-
if (!best) {
|
|
159
|
-
best = obj;
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
const currentScore = scoreFn(best);
|
|
163
|
-
const newScore = scoreFn(obj);
|
|
164
|
-
if (newScore > currentScore) best = obj;
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
if (best) return best;
|
|
168
|
-
if (typeof scene.getObjectByName === 'function') return scene.getObjectByName(name);
|
|
169
|
-
return null;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function looksLikeUUID(value) {
|
|
173
|
-
if (typeof value !== 'string') return false;
|
|
174
|
-
const trimmed = value.trim();
|
|
175
|
-
if (trimmed.length !== 36) return false;
|
|
176
|
-
return /^[0-9a-fA-F-]{36}$/.test(trimmed);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function scoreObjectForNormal(object) {
|
|
180
|
-
if (!object) return -Infinity;
|
|
181
|
-
const type = object.userData?.type || object.userData?.brepType || object.type;
|
|
182
|
-
if (String(type).toUpperCase() === 'FACE') return 3;
|
|
183
|
-
if (object.geometry) return 2;
|
|
184
|
-
return 1;
|
|
5
|
+
return resolveSelectionObjectBase(scene, selection, { ...options, scoreFn });
|
|
185
6
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
|
-
import { objectRepresentativePoint } from '
|
|
2
|
+
import { objectRepresentativePoint } from './pmi/annUtils.js';
|
|
3
3
|
|
|
4
4
|
export function isFaceObject(object) {
|
|
5
5
|
if (!object) return false;
|
|
@@ -35,6 +35,21 @@ export function computeFaceOrigin(object) {
|
|
|
35
35
|
return null;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export function computeFaceCenter(object) {
|
|
39
|
+
if (!object) return null;
|
|
40
|
+
try { object.updateMatrixWorld?.(true); } catch { /* ignore */ }
|
|
41
|
+
try {
|
|
42
|
+
const box = new THREE.Box3().setFromObject(object);
|
|
43
|
+
if (!box.isEmpty()) return box.getCenter(new THREE.Vector3());
|
|
44
|
+
} catch { /* ignore */ }
|
|
45
|
+
try {
|
|
46
|
+
const geom = object.geometry;
|
|
47
|
+
const bs = geom?.boundingSphere || (geom?.computeBoundingSphere && (geom.computeBoundingSphere(), geom.boundingSphere));
|
|
48
|
+
if (bs) return object.localToWorld(bs.center.clone());
|
|
49
|
+
} catch { /* ignore */ }
|
|
50
|
+
return computeFaceOrigin(object);
|
|
51
|
+
}
|
|
52
|
+
|
|
38
53
|
export function computeFaceNormal(object) {
|
|
39
54
|
if (!object) return null;
|
|
40
55
|
try {
|
|
@@ -45,9 +60,11 @@ export function computeFaceNormal(object) {
|
|
|
45
60
|
} catch { /* ignore */ }
|
|
46
61
|
|
|
47
62
|
const geom = object.geometry;
|
|
48
|
-
if (!geom?.isBufferGeometry)
|
|
63
|
+
if (!geom?.isBufferGeometry) {
|
|
64
|
+
return fallbackQuaternionNormal(object);
|
|
65
|
+
}
|
|
49
66
|
const pos = geom.getAttribute?.('position');
|
|
50
|
-
if (!pos || pos.itemSize !== 3 || pos.count < 3) return
|
|
67
|
+
if (!pos || pos.itemSize !== 3 || pos.count < 3) return fallbackQuaternionNormal(object);
|
|
51
68
|
const index = geom.getIndex?.();
|
|
52
69
|
|
|
53
70
|
const v0 = new THREE.Vector3();
|
|
@@ -94,10 +111,10 @@ export function computeFaceNormal(object) {
|
|
|
94
111
|
}
|
|
95
112
|
}
|
|
96
113
|
|
|
97
|
-
if (count === 0) return
|
|
114
|
+
if (count === 0) return fallbackQuaternionNormal(object);
|
|
98
115
|
|
|
99
116
|
accum.divideScalar(count);
|
|
100
|
-
if (accum.lengthSq() <= 1e-10) return
|
|
117
|
+
if (accum.lengthSq() <= 1e-10) return fallbackQuaternionNormal(object);
|
|
101
118
|
|
|
102
119
|
return accum.normalize();
|
|
103
120
|
}
|
|
@@ -113,3 +130,11 @@ export function estimateArrowLength(object) {
|
|
|
113
130
|
}
|
|
114
131
|
return 10;
|
|
115
132
|
}
|
|
133
|
+
|
|
134
|
+
function fallbackQuaternionNormal(object) {
|
|
135
|
+
try {
|
|
136
|
+
const q = object?.getWorldQuaternion?.(new THREE.Quaternion());
|
|
137
|
+
if (q) return new THREE.Vector3(0, 0, 1).applyQuaternion(q).normalize();
|
|
138
|
+
} catch { /* ignore */ }
|
|
139
|
+
return null;
|
|
140
|
+
}
|
package/src/UI/featureDialogs.js
CHANGED
|
@@ -455,6 +455,52 @@ export class SchemaForm {
|
|
|
455
455
|
return false;
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
+
// Focus the first available field in this form (or activate a reference selection when needed).
|
|
459
|
+
focusFirstField() {
|
|
460
|
+
const canFocus = (el) => {
|
|
461
|
+
if (!el || typeof el.focus !== 'function') return false;
|
|
462
|
+
if (el.disabled) return false;
|
|
463
|
+
const ariaDisabled = el.getAttribute ? el.getAttribute('aria-disabled') : null;
|
|
464
|
+
if (ariaDisabled === 'true') return false;
|
|
465
|
+
return true;
|
|
466
|
+
};
|
|
467
|
+
const tryFocus = (el) => {
|
|
468
|
+
if (!canFocus(el)) return false;
|
|
469
|
+
try { el.focus(); } catch (_) { return false; }
|
|
470
|
+
return true;
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
for (const key in this.schema) {
|
|
474
|
+
if (!Object.prototype.hasOwnProperty.call(this.schema, key)) continue;
|
|
475
|
+
if (this._excludedKeys.has(key)) continue;
|
|
476
|
+
const def = this.schema[key];
|
|
477
|
+
const row = this._fieldsWrap?.querySelector?.(`[data-key="${key}"]`) || null;
|
|
478
|
+
|
|
479
|
+
// Reference selections should auto-activate instead of just focusing the display button.
|
|
480
|
+
if (def && def.type === 'reference_selection') {
|
|
481
|
+
if (this.activateField(key)) return true;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Prefer direct inputs first.
|
|
485
|
+
if (row) {
|
|
486
|
+
const input = row.querySelector('input:not([type="hidden"]), select, textarea');
|
|
487
|
+
if (tryFocus(input)) return true;
|
|
488
|
+
const btn = row.querySelector('button, [tabindex]:not([tabindex="-1"])');
|
|
489
|
+
if (tryFocus(btn)) return true;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const inputEl = this._inputs.get(key);
|
|
493
|
+
if (inputEl && inputEl.getAttribute && inputEl.getAttribute('type') !== 'hidden') {
|
|
494
|
+
if (tryFocus(inputEl)) return true;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const root = this._shadow || this.uiElement;
|
|
499
|
+
const any = root?.querySelector?.('input:not([type="hidden"]), select, textarea, button, [tabindex]:not([tabindex="-1"])');
|
|
500
|
+
if (tryFocus(any)) return true;
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
|
|
458
504
|
readFieldValue(key) {
|
|
459
505
|
const widget = this._widgets.get(key);
|
|
460
506
|
if (widget && typeof widget.readValue === 'function') {
|
|
@@ -1001,50 +1047,50 @@ export class SchemaForm {
|
|
|
1001
1047
|
for (const name of list) {
|
|
1002
1048
|
if (!name) continue;
|
|
1003
1049
|
const existing = cache.get(name) || null;
|
|
1004
|
-
const snapshot = store ? store[name] : null;
|
|
1005
|
-
if (snapshot) {
|
|
1006
|
-
const snapType = String(snapshot.type || '').toUpperCase();
|
|
1007
|
-
const snapEdges = Array.isArray(snapshot.edgePositions) ? snapshot.edgePositions : null;
|
|
1008
|
-
const isFaceSnap = (snapType === 'FACE' || snapType === 'PLANE') && snapEdges && snapEdges.length;
|
|
1009
|
-
const isEdgeSnap = snapType === 'EDGE' && Array.isArray(snapshot.positions) && snapshot.positions.length >= 6;
|
|
1010
|
-
const isVertexSnap = snapType === 'VERTEX' && Array.isArray(snapshot.position) && snapshot.position.length >= 3;
|
|
1011
|
-
if (isFaceSnap || isEdgeSnap || isVertexSnap) {
|
|
1012
|
-
const shouldOverride = !existing || !existing.fromSnapshot || isFaceSnap;
|
|
1013
|
-
if (shouldOverride) {
|
|
1014
|
-
const ghost = this._buildReferencePreviewFromSnapshot(name, snapshot);
|
|
1015
|
-
if (ghost) {
|
|
1016
|
-
cache.set(name, {
|
|
1017
|
-
object: ghost,
|
|
1018
|
-
type: snapshot.type || null,
|
|
1019
|
-
sourceUuid: snapshot.sourceUuid || null,
|
|
1020
|
-
sourceFeatureId: snapshot.sourceFeatureId || null,
|
|
1021
|
-
showWhenOriginalPresent: isFaceSnap || !!ghost?.userData?.previewHasEdges,
|
|
1022
|
-
fromSnapshot: true,
|
|
1023
|
-
});
|
|
1024
|
-
continue;
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
if (cache.has(name)) continue;
|
|
1030
1050
|
const obj = scene.getObjectByName(name);
|
|
1031
1051
|
if (obj && !obj?.userData?.refPreview) {
|
|
1032
|
-
|
|
1052
|
+
const objType = String(obj.type || '').toUpperCase();
|
|
1053
|
+
const isEdgeObj = objType === SelectionFilter.EDGE || objType === 'EDGE';
|
|
1054
|
+
const objTimestamp = (obj.timestamp ?? obj.userData?.timestamp ?? null);
|
|
1055
|
+
const existingTimestamp = existing?.sourceTimestamp ?? null;
|
|
1056
|
+
const shouldRefresh = !existing
|
|
1057
|
+
|| existing.fromSnapshot
|
|
1058
|
+
|| (!!existing?.sourceUuid && !!obj.uuid && existing.sourceUuid !== obj.uuid)
|
|
1059
|
+
|| (!existing?.sourceUuid && !!obj.uuid)
|
|
1060
|
+
|| (objTimestamp != null && existingTimestamp !== objTimestamp)
|
|
1061
|
+
|| (isEdgeObj && !existing?.showWhenOriginalPresent);
|
|
1062
|
+
if (shouldRefresh) {
|
|
1063
|
+
this._storeReferencePreviewSnapshot(inputEl, def, obj);
|
|
1064
|
+
}
|
|
1033
1065
|
continue;
|
|
1034
1066
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1067
|
+
const snapshot = store ? store[name] : null;
|
|
1068
|
+
if (!snapshot) continue;
|
|
1069
|
+
const snapType = String(snapshot.type || '').toUpperCase();
|
|
1070
|
+
const snapEdges = Array.isArray(snapshot.edgePositions) ? snapshot.edgePositions : null;
|
|
1071
|
+
const isFaceSnap = (snapType === 'FACE' || snapType === 'PLANE') && snapEdges && snapEdges.length;
|
|
1072
|
+
const isEdgeSnap = snapType === 'EDGE' && Array.isArray(snapshot.positions) && snapshot.positions.length >= 6;
|
|
1073
|
+
const isVertexSnap = snapType === 'VERTEX' && Array.isArray(snapshot.position) && snapshot.position.length >= 3;
|
|
1074
|
+
if (!(isFaceSnap || isEdgeSnap || isVertexSnap)) continue;
|
|
1075
|
+
const snapTimestamp = snapshot.sourceTimestamp ?? null;
|
|
1076
|
+
const shouldOverride = !existing
|
|
1077
|
+
|| !existing.fromSnapshot
|
|
1078
|
+
|| isFaceSnap
|
|
1079
|
+
|| (!!snapshot.sourceUuid && !!existing?.sourceUuid && snapshot.sourceUuid !== existing.sourceUuid)
|
|
1080
|
+
|| (snapTimestamp != null && (existing?.sourceTimestamp ?? null) !== snapTimestamp)
|
|
1081
|
+
|| (isEdgeSnap && !existing?.showWhenOriginalPresent);
|
|
1082
|
+
if (!shouldOverride) continue;
|
|
1083
|
+
const ghost = this._buildReferencePreviewFromSnapshot(name, snapshot);
|
|
1084
|
+
if (ghost) {
|
|
1085
|
+
cache.set(name, {
|
|
1086
|
+
object: ghost,
|
|
1087
|
+
type: snapshot.type || null,
|
|
1088
|
+
sourceUuid: snapshot.sourceUuid || null,
|
|
1089
|
+
sourceFeatureId: snapshot.sourceFeatureId || null,
|
|
1090
|
+
sourceTimestamp: snapTimestamp,
|
|
1091
|
+
showWhenOriginalPresent: isFaceSnap || isEdgeSnap || !!ghost?.userData?.previewHasEdges,
|
|
1092
|
+
fromSnapshot: true,
|
|
1093
|
+
});
|
|
1048
1094
|
}
|
|
1049
1095
|
}
|
|
1050
1096
|
}
|
|
@@ -1060,21 +1106,24 @@ export class SchemaForm {
|
|
|
1060
1106
|
if (!ghost) return;
|
|
1061
1107
|
const sourceUuid = obj.uuid || null;
|
|
1062
1108
|
const sourceFeatureId = this._getOwningFeatureIdForObject(obj);
|
|
1109
|
+
const sourceTimestamp = (obj.timestamp ?? obj.userData?.timestamp ?? null);
|
|
1110
|
+
const objType = String(obj.type || '').toUpperCase();
|
|
1111
|
+
const isEdge = objType === SelectionFilter.EDGE || objType === 'EDGE';
|
|
1063
1112
|
cache.set(refName, {
|
|
1064
1113
|
object: ghost,
|
|
1065
1114
|
type: obj.type || null,
|
|
1066
1115
|
sourceUuid,
|
|
1067
1116
|
sourceFeatureId,
|
|
1068
|
-
|
|
1117
|
+
sourceTimestamp,
|
|
1118
|
+
showWhenOriginalPresent: isEdge || !!ghost?.userData?.previewHasEdges,
|
|
1069
1119
|
});
|
|
1070
1120
|
try {
|
|
1071
1121
|
const store = this._getReferencePreviewPersistentBucket(inputEl);
|
|
1072
1122
|
if (store) {
|
|
1073
|
-
const objType = String(obj.type || '').toUpperCase();
|
|
1074
1123
|
if (objType === SelectionFilter.EDGE || objType === 'EDGE') {
|
|
1075
1124
|
const positions = this._extractEdgeWorldPositions(obj);
|
|
1076
1125
|
if (positions && positions.length >= 6) {
|
|
1077
|
-
store[refName] = { type: 'EDGE', positions, sourceUuid, sourceFeatureId };
|
|
1126
|
+
store[refName] = { type: 'EDGE', positions, sourceUuid, sourceFeatureId, sourceTimestamp };
|
|
1078
1127
|
}
|
|
1079
1128
|
} else if (objType === SelectionFilter.VERTEX || objType === 'VERTEX') {
|
|
1080
1129
|
const pos = new THREE.Vector3();
|
|
@@ -1082,7 +1131,7 @@ export class SchemaForm {
|
|
|
1082
1131
|
if (typeof obj.getWorldPosition === 'function') obj.getWorldPosition(pos);
|
|
1083
1132
|
else pos.set(obj.position?.x || 0, obj.position?.y || 0, obj.position?.z || 0);
|
|
1084
1133
|
} catch (_) { }
|
|
1085
|
-
store[refName] = { type: 'VERTEX', position: [pos.x, pos.y, pos.z], sourceUuid, sourceFeatureId };
|
|
1134
|
+
store[refName] = { type: 'VERTEX', position: [pos.x, pos.y, pos.z], sourceUuid, sourceFeatureId, sourceTimestamp };
|
|
1086
1135
|
}
|
|
1087
1136
|
}
|
|
1088
1137
|
} catch (_) { }
|
|
@@ -1229,64 +1278,100 @@ export class SchemaForm {
|
|
|
1229
1278
|
_hoverReferenceSelectionItem(inputEl, def, name) {
|
|
1230
1279
|
try {
|
|
1231
1280
|
if (!inputEl) return;
|
|
1232
|
-
const
|
|
1233
|
-
if (activeInput !== inputEl) {
|
|
1234
|
-
const wrap = inputEl.closest?.('.ref-active') || null;
|
|
1235
|
-
if (!wrap) return;
|
|
1236
|
-
}
|
|
1281
|
+
const isActive = (SchemaForm.__activeRefInput || null) === inputEl;
|
|
1237
1282
|
const normalized = normalizeReferenceName(name);
|
|
1238
1283
|
if (!normalized) return;
|
|
1239
1284
|
const scene = this._getReferenceSelectionScene();
|
|
1240
1285
|
if (!scene) return;
|
|
1241
1286
|
try { console.log('[ReferenceSelection] Hover', { name: normalized }); } catch (_) { }
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
try { this._syncActiveReferenceSelectionPreview(inputEl, def); } catch (_) { }
|
|
1245
|
-
|
|
1246
|
-
let target = null;
|
|
1247
|
-
try { target = scene.getObjectByName(normalized); } catch (_) { target = null; }
|
|
1248
|
-
if (!target) {
|
|
1249
|
-
const cache = this._getReferencePreviewCache(inputEl);
|
|
1250
|
-
const entry = cache ? cache.get(normalized) : null;
|
|
1251
|
-
target = entry?.object || entry || null;
|
|
1287
|
+
if (isActive) {
|
|
1288
|
+
try { this._ensureReferencePreviewSnapshots(inputEl, def); } catch (_) { }
|
|
1252
1289
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1290
|
+
try { this._seedReferencePreviewCacheFromScene(inputEl, def, [normalized], scene); } catch (_) { }
|
|
1291
|
+
if (isActive) {
|
|
1292
|
+
try { this._syncActiveReferenceSelectionPreview(inputEl, def); } catch (_) { }
|
|
1255
1293
|
}
|
|
1256
|
-
|
|
1257
|
-
|
|
1294
|
+
|
|
1295
|
+
const resolveHoverCandidate = (obj) => {
|
|
1296
|
+
if (!obj) return null;
|
|
1297
|
+
if (obj.material) return obj;
|
|
1298
|
+
if (!obj.traverse) return obj;
|
|
1258
1299
|
let candidate = null;
|
|
1259
1300
|
try {
|
|
1260
|
-
|
|
1301
|
+
obj.traverse((child) => {
|
|
1261
1302
|
if (!child || candidate) return;
|
|
1262
1303
|
if (child.type === 'REF_PREVIEW_EDGE') { candidate = child; return; }
|
|
1263
1304
|
if (child.material) candidate = child;
|
|
1264
1305
|
});
|
|
1265
1306
|
} catch (_) { }
|
|
1266
|
-
|
|
1307
|
+
return candidate || obj;
|
|
1308
|
+
};
|
|
1309
|
+
|
|
1310
|
+
const targets = [];
|
|
1311
|
+
const pushTarget = (obj) => {
|
|
1312
|
+
if (!obj || !obj.isObject3D) return;
|
|
1313
|
+
const candidate = resolveHoverCandidate(obj);
|
|
1314
|
+
if (!candidate) return;
|
|
1315
|
+
if (!targets.includes(candidate)) targets.push(candidate);
|
|
1316
|
+
};
|
|
1317
|
+
|
|
1318
|
+
let sceneObj = null;
|
|
1319
|
+
try { sceneObj = scene.getObjectByName(normalized); } catch (_) { sceneObj = null; }
|
|
1320
|
+
if (sceneObj && !sceneObj?.userData?.refPreview) pushTarget(sceneObj);
|
|
1321
|
+
|
|
1322
|
+
const cache = this._getReferencePreviewCache(inputEl);
|
|
1323
|
+
const entry = cache ? cache.get(normalized) : null;
|
|
1324
|
+
const ghost = entry?.object || entry || null;
|
|
1325
|
+
if (ghost && ghost !== sceneObj) pushTarget(ghost);
|
|
1326
|
+
|
|
1327
|
+
let namedPreview = null;
|
|
1328
|
+
try { namedPreview = scene.getObjectByName(`__refPreview__${normalized}`); } catch (_) { namedPreview = null; }
|
|
1329
|
+
if (namedPreview && namedPreview !== sceneObj && namedPreview !== ghost) pushTarget(namedPreview);
|
|
1330
|
+
|
|
1331
|
+
if (!targets.length) return;
|
|
1332
|
+
if (!isActive && ghost && ghost.isObject3D && !ghost.parent) {
|
|
1333
|
+
try {
|
|
1334
|
+
const group = this._ensureReferencePreviewGroup(inputEl);
|
|
1335
|
+
if (group && ghost.parent !== group) {
|
|
1336
|
+
group.add(ghost);
|
|
1337
|
+
inputEl.__refPreviewHoverGroup = true;
|
|
1338
|
+
}
|
|
1339
|
+
} catch (_) { }
|
|
1267
1340
|
}
|
|
1268
1341
|
try {
|
|
1269
1342
|
console.log('[ReferenceSelection] Hover target', {
|
|
1270
1343
|
name: normalized,
|
|
1271
|
-
|
|
1272
|
-
inScene: !!(target && target.parent),
|
|
1344
|
+
targetCount: targets.length,
|
|
1273
1345
|
});
|
|
1274
1346
|
} catch (_) { }
|
|
1275
1347
|
inputEl.__refChipHoverActive = true;
|
|
1276
|
-
try { SelectionFilter.
|
|
1348
|
+
try { SelectionFilter.setHoverObjects(targets, { ignoreFilter: true }); } catch (_) { }
|
|
1277
1349
|
} catch (_) { }
|
|
1278
1350
|
}
|
|
1279
1351
|
|
|
1280
1352
|
_clearReferenceSelectionHover(inputEl) {
|
|
1281
1353
|
try {
|
|
1282
|
-
if (!inputEl
|
|
1354
|
+
if (!inputEl) return;
|
|
1283
1355
|
if (!inputEl.__refChipHoverActive) return;
|
|
1284
1356
|
inputEl.__refChipHoverActive = false;
|
|
1285
1357
|
SelectionFilter.clearHover();
|
|
1358
|
+
if (SchemaForm.__activeRefInput !== inputEl && inputEl.__refPreviewHoverGroup) {
|
|
1359
|
+
inputEl.__refPreviewHoverGroup = false;
|
|
1360
|
+
try { this._removeReferencePreviewGroup(inputEl); } catch (_) { }
|
|
1361
|
+
}
|
|
1286
1362
|
} catch (_) { }
|
|
1287
1363
|
}
|
|
1288
1364
|
|
|
1289
1365
|
_activateReferenceSelection(inputEl, def) {
|
|
1366
|
+
// If switching between reference fields, fully stop the previous session so
|
|
1367
|
+
// selection filters restore correctly (prevents sticky FACE-only state).
|
|
1368
|
+
try {
|
|
1369
|
+
const prevActive = SchemaForm.__activeRefInput || null;
|
|
1370
|
+
if (prevActive && prevActive !== inputEl) {
|
|
1371
|
+
this._stopActiveReferenceSelection();
|
|
1372
|
+
}
|
|
1373
|
+
} catch (_) { }
|
|
1374
|
+
|
|
1290
1375
|
// Clear any lingering scene selection so the new reference starts fresh
|
|
1291
1376
|
try {
|
|
1292
1377
|
const scene = this._getReferenceSelectionScene();
|
|
@@ -95,6 +95,8 @@ export class HistoryCollectionWidget {
|
|
|
95
95
|
this._itemEls = new Map();
|
|
96
96
|
this._forms = new Map();
|
|
97
97
|
this._uiFieldSignatures = new Map();
|
|
98
|
+
this._autoFocusOnExpand = false;
|
|
99
|
+
this._pendingFocusEntryId = null;
|
|
98
100
|
this._boundHistoryListener = null;
|
|
99
101
|
this._listenerUnsub = null;
|
|
100
102
|
this._suppressHistoryListener = false;
|
|
@@ -157,6 +159,7 @@ export class HistoryCollectionWidget {
|
|
|
157
159
|
|
|
158
160
|
if (!entries.length) {
|
|
159
161
|
this._setContextSuppression(false);
|
|
162
|
+
this._pendingFocusEntryId = null;
|
|
160
163
|
const empty = document.createElement('div');
|
|
161
164
|
empty.className = 'hc-empty';
|
|
162
165
|
empty.textContent = 'No entries yet.';
|
|
@@ -198,6 +201,8 @@ export class HistoryCollectionWidget {
|
|
|
198
201
|
const itemEl = this._renderEntry(entry, id, i, targetId === id, entries.length);
|
|
199
202
|
this._listEl.appendChild(itemEl);
|
|
200
203
|
}
|
|
204
|
+
|
|
205
|
+
this._applyPendingFocus();
|
|
201
206
|
}
|
|
202
207
|
|
|
203
208
|
_destroyForm(id) {
|
|
@@ -223,6 +228,36 @@ export class HistoryCollectionWidget {
|
|
|
223
228
|
return this._forms.get(String(id)) || null;
|
|
224
229
|
}
|
|
225
230
|
|
|
231
|
+
// Close any expanded entry dialog and optionally clear stored open state.
|
|
232
|
+
collapseExpandedEntries({ clearOpenState = true, notify = true } = {}) {
|
|
233
|
+
const prevId = this._expandedId != null ? String(this._expandedId) : null;
|
|
234
|
+
let prevEntry = null;
|
|
235
|
+
if (prevId) {
|
|
236
|
+
const info = this._findEntryInfoById(prevId);
|
|
237
|
+
prevEntry = info?.entry || null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (clearOpenState && this._autoSyncOpenState) {
|
|
241
|
+
if (prevEntry) {
|
|
242
|
+
this._applyOpenState(prevEntry, false);
|
|
243
|
+
} else {
|
|
244
|
+
const entries = this._getEntries();
|
|
245
|
+
for (const entry of entries) {
|
|
246
|
+
this._applyOpenState(entry, false);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!prevId) {
|
|
252
|
+
this._setContextSuppression(false);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
this._expandedId = null;
|
|
257
|
+
this.render();
|
|
258
|
+
if (notify) this._notifyEntryToggle(prevEntry, false);
|
|
259
|
+
}
|
|
260
|
+
|
|
226
261
|
_getEntries() {
|
|
227
262
|
if (!this.history) return [];
|
|
228
263
|
if (Array.isArray(this.history.entries)) return this.history.entries;
|
|
@@ -414,6 +449,7 @@ export class HistoryCollectionWidget {
|
|
|
414
449
|
this._applyOpenState(targetEntry, false);
|
|
415
450
|
}
|
|
416
451
|
this._expandedId = null;
|
|
452
|
+
this._pendingFocusEntryId = null;
|
|
417
453
|
this.render();
|
|
418
454
|
this._notifyEntryToggle(targetEntry, false);
|
|
419
455
|
return;
|
|
@@ -425,6 +461,9 @@ export class HistoryCollectionWidget {
|
|
|
425
461
|
if (targetEntry) this._applyOpenState(targetEntry, true);
|
|
426
462
|
}
|
|
427
463
|
this._expandedId = targetEntry ? targetId : null;
|
|
464
|
+
if (this._autoFocusOnExpand && targetEntry) {
|
|
465
|
+
this._pendingFocusEntryId = targetId;
|
|
466
|
+
}
|
|
428
467
|
this.render();
|
|
429
468
|
if (previousInfo?.entry) this._notifyEntryToggle(previousInfo.entry, false);
|
|
430
469
|
if (targetEntry) this._notifyEntryToggle(targetEntry, true);
|
|
@@ -613,6 +652,30 @@ export class HistoryCollectionWidget {
|
|
|
613
652
|
}
|
|
614
653
|
}
|
|
615
654
|
|
|
655
|
+
_applyPendingFocus() {
|
|
656
|
+
if (!this._autoFocusOnExpand) return;
|
|
657
|
+
const targetId = this._pendingFocusEntryId;
|
|
658
|
+
if (!targetId) return;
|
|
659
|
+
if (!this._expandedId || String(this._expandedId) !== String(targetId)) {
|
|
660
|
+
this._pendingFocusEntryId = null;
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const form = this.getFormForEntry(targetId);
|
|
664
|
+
if (!form) {
|
|
665
|
+
this._pendingFocusEntryId = null;
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const focus = () => {
|
|
669
|
+
try {
|
|
670
|
+
if (typeof form.focusFirstField === 'function') form.focusFirstField();
|
|
671
|
+
else if (typeof form.activateFirstReferenceSelection === 'function') form.activateFirstReferenceSelection();
|
|
672
|
+
} catch (_) { /* ignore */ }
|
|
673
|
+
};
|
|
674
|
+
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => focus());
|
|
675
|
+
else setTimeout(focus, 0);
|
|
676
|
+
this._pendingFocusEntryId = null;
|
|
677
|
+
}
|
|
678
|
+
|
|
616
679
|
async _moveEntry(id, delta) {
|
|
617
680
|
if (!id) return;
|
|
618
681
|
const entries = this._getEntries();
|
|
@@ -671,6 +734,7 @@ export class HistoryCollectionWidget {
|
|
|
671
734
|
}
|
|
672
735
|
if (this._autoSyncOpenState) this._applyOpenState(entry, true);
|
|
673
736
|
this._expandedId = normalizedId;
|
|
737
|
+
if (this._autoFocusOnExpand) this._pendingFocusEntryId = normalizedId;
|
|
674
738
|
createdEntryId = normalizedId;
|
|
675
739
|
}
|
|
676
740
|
} catch (_) { /* ignore */ }
|
|
@@ -693,6 +757,7 @@ export class HistoryCollectionWidget {
|
|
|
693
757
|
}
|
|
694
758
|
if (this._autoSyncOpenState) this._applyOpenState(entry, true);
|
|
695
759
|
this._expandedId = normalizedId;
|
|
760
|
+
if (this._autoFocusOnExpand) this._pendingFocusEntryId = normalizedId;
|
|
696
761
|
createdEntryId = normalizedId;
|
|
697
762
|
}
|
|
698
763
|
this.render();
|