Validate measurement snap cache signature

This commit is contained in:
flash555588 2026-07-06 20:48:55 +08:00
parent 60060e7223
commit ddd8417c1c
4 changed files with 477 additions and 418 deletions

View file

@ -10,6 +10,7 @@
- UI: require the locked measurement target to be the frontmost normal pick before snapping, preventing hidden target geometry behind another part from receiving ruler endpoints.
- UI: clear stale measurement snap status after cancelling a pending endpoint so the selected-object ruler returns to a neutral ready state.
- Performance: cache selected-target measurement snap candidates while measuring so hover previews do not repeatedly rebuild vertex and edge lists for the same object.
- Performance: validate selected-target measurement snap caches against target mesh signatures so transformed or updated geometry rebuilds snap candidates instead of reusing stale vertex and edge lists.
- UI: restyle distance measurements as orthographic drawing-style dimension callouts with extension lines, arrowheads, and compact drafting labels instead of thick freehand ruler lines.
- UI: remove the preview canvas native hover tooltip while preserving keyboard shortcut metadata for accessibility.
- UI: clarify render scale versus measurement scale by showing render resolution as percentages, syncing the toolbar to the active renderer budget, and applying the configured scale to `3dgrid`.

824
main.js

File diff suppressed because one or more lines are too long

View file

@ -315,6 +315,7 @@ export class BabylonModelPreview implements WorkbenchPreview {
private measurementTargetMeshes: AbstractMesh[] = [];
private measurementSnapInputCache: MeasurementGeometrySnapInput | null = null;
private measurementSnapInputCacheTargetId: number | null = null;
private measurementSnapInputCacheSignature: string | null = null;
private measurementSnapKind: MeasurementSnapKind | null = null;
private readonly measurementObservers = new Set<() => void>();
private pendingPoint: Vector3 | null = null;
@ -1955,6 +1956,7 @@ export class BabylonModelPreview implements WorkbenchPreview {
private invalidateMeasurementSnapInputCache(): void {
this.measurementSnapInputCache = null;
this.measurementSnapInputCacheTargetId = null;
this.measurementSnapInputCacheSignature = null;
}
private getMeasurementTargetName(): string | null {
@ -1997,11 +1999,16 @@ export class BabylonModelPreview implements WorkbenchPreview {
private createMeasurementGeometrySnapInput(): MeasurementGeometrySnapInput | null {
const target = this.measurementTargetNode;
if (!this.rootMesh || !target || isBabylonNodeDisposed(target)) return null;
if (this.measurementSnapInputCache && this.measurementSnapInputCacheTargetId === target.uniqueId) {
return this.measurementSnapInputCache;
}
const meshes = this.getMeasurementTargetMeshes(target);
if (meshes.length === 0) return null;
const signature = this.createMeasurementSnapInputSignature(meshes);
if (
this.measurementSnapInputCache &&
this.measurementSnapInputCacheTargetId === target.uniqueId &&
this.measurementSnapInputCacheSignature === signature
) {
return this.measurementSnapInputCache;
}
const vertices: MeasurementSnapVertexCandidate[] = [];
const edges: MeasurementSnapEdgeCandidate[] = [];
@ -2036,9 +2043,25 @@ export class BabylonModelPreview implements WorkbenchPreview {
};
this.measurementSnapInputCache = input;
this.measurementSnapInputCacheTargetId = target.uniqueId;
this.measurementSnapInputCacheSignature = signature;
return input;
}
private createMeasurementSnapInputSignature(meshes: readonly AbstractMesh[]): string {
return meshes.map((mesh) => {
const geometryId = "geometry" in mesh && mesh.geometry
? (mesh.geometry as { uniqueId?: number }).uniqueId ?? "geometry"
: "none";
return [
mesh.uniqueId,
geometryId,
mesh.getTotalVertices(),
mesh.getTotalIndices(),
mesh.computeWorldMatrix(true).asArray().map(formatMeasurementSnapSignatureNumber).join(","),
].join(":");
}).join("|");
}
private getMeasurementTargetMeshes(target = this.measurementTargetNode): AbstractMesh[] {
if (!this.rootMesh || !target || isBabylonNodeDisposed(target)) return [];
return this.getRenderableMeshes(this.rootMesh)
@ -2333,6 +2356,10 @@ export class BabylonModelPreview implements WorkbenchPreview {
}
}
function formatMeasurementSnapSignatureNumber(value: number): string {
return Number.isFinite(value) ? value.toPrecision(10) : String(value);
}
export function createBabylonModelPreview(canvas: HTMLCanvasElement): WorkbenchPreview {
return new BabylonModelPreview(canvas);
}

View file

@ -302,6 +302,7 @@ export class ThreeModelPreview implements WorkbenchPreview {
private measurementTargetHelper: BoxHelper | null = null;
private measurementSnapInputCache: MeasurementGeometrySnapInput | null = null;
private measurementSnapInputCacheTarget: Object3D | null = null;
private measurementSnapInputCacheSignature: string | null = null;
private measurementSnapKind: MeasurementSnapKind | null = null;
private readonly measurementObservers = new Set<() => void>();
private pendingPoint: Vector3 | null = null;
@ -2648,6 +2649,7 @@ export class ThreeModelPreview implements WorkbenchPreview {
private invalidateMeasurementSnapInputCache(): void {
this.measurementSnapInputCache = null;
this.measurementSnapInputCacheTarget = null;
this.measurementSnapInputCacheSignature = null;
}
private setMeasurementSnapKind(kind: MeasurementSnapKind | null, notify = true): void {
@ -2712,11 +2714,16 @@ export class ThreeModelPreview implements WorkbenchPreview {
private createMeasurementGeometrySnapInput(): MeasurementGeometrySnapInput | null {
const target = this.measurementTargetObject;
if (!target || !this.isObjectInLoadedRoot(target)) return null;
if (this.measurementSnapInputCache && this.measurementSnapInputCacheTarget === target) {
return this.measurementSnapInputCache;
}
const renderables = this.getMeasurementTargetRenderables();
if (renderables.length === 0) return null;
const signature = this.createMeasurementSnapInputSignature(renderables);
if (
this.measurementSnapInputCache &&
this.measurementSnapInputCacheTarget === target &&
this.measurementSnapInputCacheSignature === signature
) {
return this.measurementSnapInputCache;
}
const vertices: MeasurementSnapVertexCandidate[] = [];
const edges: MeasurementSnapEdgeCandidate[] = [];
const targetId = `three:${target.id}`;
@ -2749,9 +2756,29 @@ export class ThreeModelPreview implements WorkbenchPreview {
};
this.measurementSnapInputCache = input;
this.measurementSnapInputCacheTarget = target;
this.measurementSnapInputCacheSignature = signature;
return input;
}
private createMeasurementSnapInputSignature(renderables: readonly ThreeRenderableObject[]): string {
return renderables.map((object) => {
const geometry = object.geometry;
const position = geometry.getAttribute("position");
const index = geometry.getIndex();
const positionVersion = position && "version" in position ? (position as { version?: number }).version ?? 0 : 0;
object.updateWorldMatrix(true, false);
return [
object.id,
geometry.id,
position?.count ?? 0,
positionVersion,
index?.count ?? 0,
index?.version ?? 0,
object.matrixWorld.elements.map(formatMeasurementSnapSignatureNumber).join(","),
].join(":");
}).join("|");
}
private getMeasurementTargetRenderables(): ThreeRenderableObject[] {
const target = this.measurementTargetObject;
if (!this.rootObject || !target || !this.isObjectInLoadedRoot(target)) return [];
@ -3092,6 +3119,10 @@ export class ThreeModelPreview implements WorkbenchPreview {
}
function formatMeasurementSnapSignatureNumber(value: number): string {
return Number.isFinite(value) ? value.toPrecision(10) : String(value);
}
export function createThreeModelPreview(canvas: HTMLCanvasElement): WorkbenchPreview {
return new ThreeModelPreview(canvas);
}