@ubuilder-ai/workspace 0.1.0-alpha.1
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 +7 -0
- package/dist/floorplan-annotation-obstacles.d.ts +4 -0
- package/dist/floorplan-annotation-obstacles.d.ts.map +1 -0
- package/dist/floorplan-annotation-obstacles.js +25 -0
- package/dist/floorplan-camera-sync.d.ts +24 -0
- package/dist/floorplan-camera-sync.d.ts.map +1 -0
- package/dist/floorplan-camera-sync.js +187 -0
- package/dist/floorplan-coordinates.d.ts +10 -0
- package/dist/floorplan-coordinates.d.ts.map +1 -0
- package/dist/floorplan-coordinates.js +25 -0
- package/dist/floorplan-dimension-renderer.d.ts +42 -0
- package/dist/floorplan-dimension-renderer.d.ts.map +1 -0
- package/dist/floorplan-dimension-renderer.js +245 -0
- package/dist/floorplan-geometry-renderer.d.ts +56 -0
- package/dist/floorplan-geometry-renderer.d.ts.map +1 -0
- package/dist/floorplan-geometry-renderer.js +319 -0
- package/dist/floorplan-initial-view.d.ts +29 -0
- package/dist/floorplan-initial-view.d.ts.map +1 -0
- package/dist/floorplan-initial-view.js +30 -0
- package/dist/floorplan-label-angle.d.ts +24 -0
- package/dist/floorplan-label-angle.d.ts.map +1 -0
- package/dist/floorplan-label-angle.js +60 -0
- package/dist/floorplan-navigation-presentation.d.ts +57 -0
- package/dist/floorplan-navigation-presentation.d.ts.map +1 -0
- package/dist/floorplan-navigation-presentation.js +81 -0
- package/dist/floorplan-navigation-shell.d.ts +87 -0
- package/dist/floorplan-navigation-shell.d.ts.map +1 -0
- package/dist/floorplan-navigation-shell.js +1178 -0
- package/dist/floorplan-navigation-types.d.ts +10 -0
- package/dist/floorplan-navigation-types.d.ts.map +1 -0
- package/dist/floorplan-navigation-types.js +1 -0
- package/dist/floorplan-registry-projection.d.ts +84 -0
- package/dist/floorplan-registry-projection.d.ts.map +1 -0
- package/dist/floorplan-registry-projection.js +260 -0
- package/dist/floorplan-render-context.d.ts +56 -0
- package/dist/floorplan-render-context.d.ts.map +1 -0
- package/dist/floorplan-render-context.js +75 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/spatial-workspace-camera-action-control.d.ts +18 -0
- package/dist/spatial-workspace-camera-action-control.d.ts.map +1 -0
- package/dist/spatial-workspace-camera-action-control.js +13 -0
- package/dist/spatial-workspace-display-control.d.ts +64 -0
- package/dist/spatial-workspace-display-control.d.ts.map +1 -0
- package/dist/spatial-workspace-display-control.js +81 -0
- package/dist/spatial-workspace-gis-control.d.ts +58 -0
- package/dist/spatial-workspace-gis-control.d.ts.map +1 -0
- package/dist/spatial-workspace-gis-control.js +38 -0
- package/dist/spatial-workspace-instance.d.ts +2 -0
- package/dist/spatial-workspace-instance.d.ts.map +1 -0
- package/dist/spatial-workspace-instance.js +14 -0
- package/dist/spatial-workspace-level-selector.d.ts +21 -0
- package/dist/spatial-workspace-level-selector.d.ts.map +1 -0
- package/dist/spatial-workspace-level-selector.js +11 -0
- package/dist/spatial-workspace-level-wall-control.d.ts +38 -0
- package/dist/spatial-workspace-level-wall-control.d.ts.map +1 -0
- package/dist/spatial-workspace-level-wall-control.js +41 -0
- package/dist/spatial-workspace-mode.d.ts +11 -0
- package/dist/spatial-workspace-mode.d.ts.map +1 -0
- package/dist/spatial-workspace-mode.js +29 -0
- package/dist/spatial-workspace-styles.d.ts +3 -0
- package/dist/spatial-workspace-styles.d.ts.map +1 -0
- package/dist/spatial-workspace-styles.js +72 -0
- package/dist/spatial-workspace-view-mode-control.d.ts +19 -0
- package/dist/spatial-workspace-view-mode-control.d.ts.map +1 -0
- package/dist/spatial-workspace-view-mode-control.js +14 -0
- package/dist/spatial-workspace-viewport.d.ts +25 -0
- package/dist/spatial-workspace-viewport.d.ts.map +1 -0
- package/dist/spatial-workspace-viewport.js +75 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# `@ubuilder-ai/workspace`
|
|
2
|
+
|
|
3
|
+
Shared read-only spatial workbench consumed by uBuilder-AI and its runtime adapters.
|
|
4
|
+
|
|
5
|
+
It owns native Building 3D/2D/split composition, floorplan projection, navigation chrome, neutral workspace controls, divider behavior, and the hidden-but-mounted 3D runtime invariant.
|
|
6
|
+
|
|
7
|
+
The package depends only on `@ubuilder-ai/core`, `@ubuilder-ai/floorplan-contract`, and React. It does not create a Canvas or import Viewer, Editor, Nodes, app code, or Three.js. Hosts inject their 3D surface and authoring behavior through explicit composition points. See `wiki/architecture/workspace.md`.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { FloorplanGeometry } from '@ubuilder-ai/core';
|
|
2
|
+
export declare function isFloorplanAnnotationObstacleGeometry(geometry: FloorplanGeometry): boolean;
|
|
3
|
+
export declare function floorplanAnnotationObstacleMode(geometry: FloorplanGeometry): 'bounds' | 'outline' | '' | undefined;
|
|
4
|
+
//# sourceMappingURL=floorplan-annotation-obstacles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"floorplan-annotation-obstacles.d.ts","sourceRoot":"","sources":["../src/floorplan-annotation-obstacles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAG1D,wBAAgB,qCAAqC,CAAC,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAQ1F;AAED,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,iBAAiB,GAC1B,QAAQ,GAAG,SAAS,GAAG,EAAE,GAAG,SAAS,CAavC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readFloorplanGeometryMetadata } from '@ubuilder-ai/floorplan-contract';
|
|
2
|
+
export function isFloorplanAnnotationObstacleGeometry(geometry) {
|
|
3
|
+
if (floorplanAnnotationObstacleMode(geometry))
|
|
4
|
+
return true;
|
|
5
|
+
if (geometry.kind !== 'group')
|
|
6
|
+
return false;
|
|
7
|
+
const hasPlate = geometry.children.some((child) => child.kind === 'rect' || child.kind === 'circle');
|
|
8
|
+
const hasUprightText = geometry.children.some((child) => child.kind === 'text' && child.upright);
|
|
9
|
+
return hasPlate && hasUprightText;
|
|
10
|
+
}
|
|
11
|
+
export function floorplanAnnotationObstacleMode(geometry) {
|
|
12
|
+
const metadata = readFloorplanGeometryMetadata(geometry);
|
|
13
|
+
if (metadata.annotationObstacle)
|
|
14
|
+
return metadata.annotationObstacle;
|
|
15
|
+
switch (metadata.annotationRole) {
|
|
16
|
+
case 'room-label':
|
|
17
|
+
return geometry.kind === 'text' ? 'bounds' : undefined;
|
|
18
|
+
case 'column-center':
|
|
19
|
+
return geometry.kind === 'line' || geometry.kind === 'text' ? 'bounds' : undefined;
|
|
20
|
+
case 'stair-annotation':
|
|
21
|
+
return geometry.kind === 'polyline' || geometry.kind === 'line' ? 'outline' : 'bounds';
|
|
22
|
+
default:
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CameraPose } from '@ubuilder-ai/core';
|
|
2
|
+
import type { FloorplanNavigationPose, FloorplanNavigationPoseInput } from './floorplan-navigation-types';
|
|
3
|
+
export declare function cameraPoseToFloorplanNavigationPose(pose: CameraPose): FloorplanNavigationPoseInput | null;
|
|
4
|
+
export declare function floorplanNavigationPoseToCameraPose(navigationPose: FloorplanNavigationPose, cameraPose: CameraPose): CameraPose;
|
|
5
|
+
export declare function floorplanNavigationPoseToInspectionCameraPose(navigationPose: FloorplanNavigationPose, cameraPose: CameraPose): CameraPose;
|
|
6
|
+
export type FloorplanCameraSyncBridge = {
|
|
7
|
+
handoffTo3d: () => void;
|
|
8
|
+
receiveCameraPose: (pose: CameraPose) => void;
|
|
9
|
+
receiveNavigationPose: (pose: FloorplanNavigationPose | null) => void;
|
|
10
|
+
setActive: (active: boolean, options?: {
|
|
11
|
+
alignedAzimuth?: number;
|
|
12
|
+
}) => void;
|
|
13
|
+
};
|
|
14
|
+
export type FloorplanCameraNavigationChannel = {
|
|
15
|
+
publish: (pose: FloorplanNavigationPoseInput) => void;
|
|
16
|
+
subscribe: (listener: (pose: FloorplanNavigationPose) => void) => () => void;
|
|
17
|
+
};
|
|
18
|
+
export declare function createFloorplanCameraNavigationChannel(): FloorplanCameraNavigationChannel;
|
|
19
|
+
export declare function createFloorplanCameraSyncBridge({ active: initialActive, applyCameraPose, publishNavigationPose, }: {
|
|
20
|
+
active?: boolean;
|
|
21
|
+
applyCameraPose: (pose: CameraPose) => void;
|
|
22
|
+
publishNavigationPose: (pose: FloorplanNavigationPoseInput) => void;
|
|
23
|
+
}): FloorplanCameraSyncBridge;
|
|
24
|
+
//# sourceMappingURL=floorplan-camera-sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"floorplan-camera-sync.d.ts","sourceRoot":"","sources":["../src/floorplan-camera-sync.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,KAAK,EACV,uBAAuB,EACvB,4BAA4B,EAC7B,MAAM,8BAA8B,CAAA;AAsCrC,wBAAgB,mCAAmC,CACjD,IAAI,EAAE,UAAU,GACf,4BAA4B,GAAG,IAAI,CAWrC;AAED,wBAAgB,mCAAmC,CACjD,cAAc,EAAE,uBAAuB,EACvC,UAAU,EAAE,UAAU,GACrB,UAAU,CAgBZ;AAED,wBAAgB,6CAA6C,CAC3D,cAAc,EAAE,uBAAuB,EACvC,UAAU,EAAE,UAAU,GACrB,UAAU,CAoBZ;AAED,MAAM,MAAM,yBAAyB,GAAG;IACtC,WAAW,EAAE,MAAM,IAAI,CAAA;IACvB,iBAAiB,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAA;IAC7C,qBAAqB,EAAE,CAAC,IAAI,EAAE,uBAAuB,GAAG,IAAI,KAAK,IAAI,CAAA;IACrE,SAAS,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;CAC5E,CAAA;AAED,MAAM,MAAM,gCAAgC,GAAG;IAC7C,OAAO,EAAE,CAAC,IAAI,EAAE,4BAA4B,KAAK,IAAI,CAAA;IACrD,SAAS,EAAE,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,uBAAuB,KAAK,IAAI,KAAK,MAAM,IAAI,CAAA;CAC7E,CAAA;AAED,wBAAgB,sCAAsC,IAAI,gCAAgC,CAiBzF;AAED,wBAAgB,+BAA+B,CAAC,EAC9C,MAAM,EAAE,aAAoB,EAC5B,eAAe,EACf,qBAAqB,GACtB,EAAE;IACD,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,eAAe,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAA;IAC3C,qBAAqB,EAAE,CAAC,IAAI,EAAE,4BAA4B,KAAK,IAAI,CAAA;CACpE,GAAG,yBAAyB,CAqG5B"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
const POSITION_EPSILON = 0.001;
|
|
2
|
+
const AZIMUTH_EPSILON = 1e-4;
|
|
3
|
+
const VIEW_WIDTH_EPSILON = 0.001;
|
|
4
|
+
const FLOORPLAN_3D_HANDOFF_POLAR_ANGLE = Math.PI / 6;
|
|
5
|
+
function angleDeltaRadians(a, b) {
|
|
6
|
+
return Math.atan2(Math.sin(a - b), Math.cos(a - b));
|
|
7
|
+
}
|
|
8
|
+
function normalizeNearZero(value) {
|
|
9
|
+
return Math.abs(value) < Number.EPSILON * 10 ? 0 : value;
|
|
10
|
+
}
|
|
11
|
+
function navigationSnapshot(pose) {
|
|
12
|
+
return {
|
|
13
|
+
target: [...pose.target],
|
|
14
|
+
azimuth: pose.azimuth,
|
|
15
|
+
viewWidth: pose.viewWidth,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function navigationSnapshotsEqual(previous, next) {
|
|
19
|
+
return (Math.abs(previous.target[0] - next.target[0]) < POSITION_EPSILON &&
|
|
20
|
+
Math.abs(previous.target[1] - next.target[1]) < POSITION_EPSILON &&
|
|
21
|
+
Math.abs(previous.target[2] - next.target[2]) < POSITION_EPSILON &&
|
|
22
|
+
Math.abs(angleDeltaRadians(previous.azimuth, next.azimuth)) < AZIMUTH_EPSILON &&
|
|
23
|
+
Math.abs(previous.viewWidth - next.viewWidth) < VIEW_WIDTH_EPSILON);
|
|
24
|
+
}
|
|
25
|
+
export function cameraPoseToFloorplanNavigationPose(pose) {
|
|
26
|
+
if (!(pose.viewWidth !== undefined && Number.isFinite(pose.viewWidth) && pose.viewWidth > 0)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
source: '3d',
|
|
31
|
+
target: [...pose.target],
|
|
32
|
+
azimuth: Math.atan2(pose.position[0] - pose.target[0], pose.position[2] - pose.target[2]),
|
|
33
|
+
viewWidth: pose.viewWidth,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function floorplanNavigationPoseToCameraPose(navigationPose, cameraPose) {
|
|
37
|
+
const offsetX = cameraPose.position[0] - cameraPose.target[0];
|
|
38
|
+
const offsetY = cameraPose.position[1] - cameraPose.target[1];
|
|
39
|
+
const offsetZ = cameraPose.position[2] - cameraPose.target[2];
|
|
40
|
+
const horizontalDistance = Math.hypot(offsetX, offsetZ);
|
|
41
|
+
const horizontalX = normalizeNearZero(Math.sin(navigationPose.azimuth) * horizontalDistance);
|
|
42
|
+
const horizontalZ = normalizeNearZero(Math.cos(navigationPose.azimuth) * horizontalDistance);
|
|
43
|
+
const target = [...navigationPose.target];
|
|
44
|
+
return {
|
|
45
|
+
...(cameraPose.fov === undefined ? {} : { fov: cameraPose.fov }),
|
|
46
|
+
position: [target[0] + horizontalX, target[1] + offsetY, target[2] + horizontalZ],
|
|
47
|
+
projection: cameraPose.projection,
|
|
48
|
+
target,
|
|
49
|
+
viewWidth: navigationPose.viewWidth,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function floorplanNavigationPoseToInspectionCameraPose(navigationPose, cameraPose) {
|
|
53
|
+
const offsetX = cameraPose.position[0] - cameraPose.target[0];
|
|
54
|
+
const offsetY = cameraPose.position[1] - cameraPose.target[1];
|
|
55
|
+
const offsetZ = cameraPose.position[2] - cameraPose.target[2];
|
|
56
|
+
const distance = Math.max(0.001, Math.hypot(offsetX, offsetY, offsetZ));
|
|
57
|
+
const horizontalDistance = Math.sin(FLOORPLAN_3D_HANDOFF_POLAR_ANGLE) * distance;
|
|
58
|
+
const verticalDistance = Math.cos(FLOORPLAN_3D_HANDOFF_POLAR_ANGLE) * distance;
|
|
59
|
+
const target = [...navigationPose.target];
|
|
60
|
+
return {
|
|
61
|
+
...(cameraPose.fov === undefined ? {} : { fov: cameraPose.fov }),
|
|
62
|
+
position: [
|
|
63
|
+
target[0] + Math.sin(navigationPose.azimuth) * horizontalDistance,
|
|
64
|
+
target[1] + verticalDistance,
|
|
65
|
+
target[2] + Math.cos(navigationPose.azimuth) * horizontalDistance,
|
|
66
|
+
],
|
|
67
|
+
projection: cameraPose.projection,
|
|
68
|
+
target,
|
|
69
|
+
viewWidth: navigationPose.viewWidth,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export function createFloorplanCameraNavigationChannel() {
|
|
73
|
+
const listeners = new Set();
|
|
74
|
+
let revision = 0;
|
|
75
|
+
return {
|
|
76
|
+
publish: (pose) => {
|
|
77
|
+
revision += 1;
|
|
78
|
+
const revisedPose = { ...pose, revision };
|
|
79
|
+
for (const listener of listeners) {
|
|
80
|
+
listener(revisedPose);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
subscribe: (listener) => {
|
|
84
|
+
listeners.add(listener);
|
|
85
|
+
return () => listeners.delete(listener);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export function createFloorplanCameraSyncBridge({ active: initialActive = true, applyCameraPose, publishNavigationPose, }) {
|
|
90
|
+
let active = initialActive;
|
|
91
|
+
let latestCameraPose = null;
|
|
92
|
+
let latestFloorplanNavigationPose = null;
|
|
93
|
+
let pendingNavigationPose = null;
|
|
94
|
+
let lastAppliedNavigationRevision = 0;
|
|
95
|
+
let lastPublishedNavigation = null;
|
|
96
|
+
let applyInactiveFloorplanNavigation = false;
|
|
97
|
+
let activeAlignedAzimuth = null;
|
|
98
|
+
let alignedFloorplanRevision = 0;
|
|
99
|
+
const applyPendingNavigationPose = (force = false) => {
|
|
100
|
+
if (!(latestCameraPose && pendingNavigationPose))
|
|
101
|
+
return false;
|
|
102
|
+
if (!force && pendingNavigationPose.revision === lastAppliedNavigationRevision) {
|
|
103
|
+
pendingNavigationPose = null;
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
const appliedPose = applyInactiveFloorplanNavigation
|
|
107
|
+
? floorplanNavigationPoseToInspectionCameraPose(pendingNavigationPose, latestCameraPose)
|
|
108
|
+
: floorplanNavigationPoseToCameraPose(pendingNavigationPose, latestCameraPose);
|
|
109
|
+
const appliedNavigationPose = cameraPoseToFloorplanNavigationPose(appliedPose);
|
|
110
|
+
latestCameraPose = appliedPose;
|
|
111
|
+
lastAppliedNavigationRevision = pendingNavigationPose.revision;
|
|
112
|
+
pendingNavigationPose = null;
|
|
113
|
+
if (appliedNavigationPose) {
|
|
114
|
+
lastPublishedNavigation = navigationSnapshot(appliedNavigationPose);
|
|
115
|
+
}
|
|
116
|
+
applyCameraPose(appliedPose);
|
|
117
|
+
return true;
|
|
118
|
+
};
|
|
119
|
+
const publishCameraNavigationPose = (pose, alignedAzimuth) => {
|
|
120
|
+
const navigationPose = cameraPoseToFloorplanNavigationPose(pose);
|
|
121
|
+
if (!navigationPose)
|
|
122
|
+
return;
|
|
123
|
+
if (alignedAzimuth !== undefined)
|
|
124
|
+
navigationPose.azimuth = alignedAzimuth;
|
|
125
|
+
const nextSnapshot = navigationSnapshot(navigationPose);
|
|
126
|
+
if (lastPublishedNavigation &&
|
|
127
|
+
navigationSnapshotsEqual(lastPublishedNavigation, nextSnapshot)) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
lastPublishedNavigation = nextSnapshot;
|
|
131
|
+
publishNavigationPose(navigationPose);
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
handoffTo3d: () => {
|
|
135
|
+
applyInactiveFloorplanNavigation = true;
|
|
136
|
+
if (!latestFloorplanNavigationPose)
|
|
137
|
+
return;
|
|
138
|
+
pendingNavigationPose = latestFloorplanNavigationPose;
|
|
139
|
+
applyPendingNavigationPose(true);
|
|
140
|
+
},
|
|
141
|
+
receiveCameraPose: (pose) => {
|
|
142
|
+
latestCameraPose = pose;
|
|
143
|
+
if ((active || applyInactiveFloorplanNavigation) && applyPendingNavigationPose())
|
|
144
|
+
return;
|
|
145
|
+
if (!active)
|
|
146
|
+
applyInactiveFloorplanNavigation = false;
|
|
147
|
+
publishCameraNavigationPose(pose, activeAlignedAzimuth ?? undefined);
|
|
148
|
+
},
|
|
149
|
+
receiveNavigationPose: (pose) => {
|
|
150
|
+
if (pose?.source === '2d') {
|
|
151
|
+
latestFloorplanNavigationPose = pose;
|
|
152
|
+
if (pose.revision > alignedFloorplanRevision) {
|
|
153
|
+
activeAlignedAzimuth = null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if ((!active && !applyInactiveFloorplanNavigation) ||
|
|
157
|
+
pose?.source !== '2d' ||
|
|
158
|
+
pose.revision === lastAppliedNavigationRevision ||
|
|
159
|
+
pose.revision === pendingNavigationPose?.revision) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
pendingNavigationPose = pose;
|
|
163
|
+
applyPendingNavigationPose();
|
|
164
|
+
},
|
|
165
|
+
setActive: (nextActive, options) => {
|
|
166
|
+
const alignedAzimuth = options?.alignedAzimuth;
|
|
167
|
+
if (active === nextActive && alignedAzimuth === undefined)
|
|
168
|
+
return;
|
|
169
|
+
active = nextActive;
|
|
170
|
+
if (active) {
|
|
171
|
+
applyInactiveFloorplanNavigation = false;
|
|
172
|
+
activeAlignedAzimuth = alignedAzimuth ?? null;
|
|
173
|
+
alignedFloorplanRevision = latestFloorplanNavigationPose?.revision ?? 0;
|
|
174
|
+
}
|
|
175
|
+
if (!active) {
|
|
176
|
+
activeAlignedAzimuth = null;
|
|
177
|
+
alignedFloorplanRevision = 0;
|
|
178
|
+
if (!applyInactiveFloorplanNavigation)
|
|
179
|
+
pendingNavigationPose = null;
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (latestCameraPose) {
|
|
183
|
+
publishCameraNavigationPose(latestCameraPose, alignedAzimuth);
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type FloorplanPoint = {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
};
|
|
5
|
+
export declare function worldToFloorplanLocalPoint(worldX: number, worldZ: number, buildingPosition: readonly [number, number, number], buildingRotationY: number): FloorplanPoint;
|
|
6
|
+
export declare function floorplanLocalToWorldPoint(point: FloorplanPoint | readonly [number, number], buildingPosition: readonly [number, number, number], buildingRotationY: number): {
|
|
7
|
+
x: number;
|
|
8
|
+
z: number;
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=floorplan-coordinates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"floorplan-coordinates.d.ts","sourceRoot":"","sources":["../src/floorplan-coordinates.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG;IAC3B,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACV,CAAA;AAMD,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,gBAAgB,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EACnD,iBAAiB,EAAE,MAAM,GACxB,cAAc,CAUhB;AAGD,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,cAAc,GAAG,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,EACjD,gBAAgB,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EACnD,iBAAiB,EAAE,MAAM,GACxB;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAU1B"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Converts a world X/Z point into the floor-plan-local (building-local)
|
|
2
|
+
// frame used by the SVG scene `<g>` and every stored node position. The
|
|
3
|
+
// inverse of `floorplanLocalToWorldPoint`. Shared so every Floorplan surface
|
|
4
|
+
// resolves the same frame instead of feeding world coordinates to local math.
|
|
5
|
+
export function worldToFloorplanLocalPoint(worldX, worldZ, buildingPosition, buildingRotationY) {
|
|
6
|
+
const dx = worldX - buildingPosition[0];
|
|
7
|
+
const dz = worldZ - buildingPosition[2];
|
|
8
|
+
const cos = Math.cos(buildingRotationY);
|
|
9
|
+
const sin = Math.sin(buildingRotationY);
|
|
10
|
+
return {
|
|
11
|
+
x: dx * cos - dz * sin,
|
|
12
|
+
y: dx * sin + dz * cos,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
// Inverse of `worldToFloorplanLocalPoint`: floor-plan-local X/Y to world X/Z.
|
|
16
|
+
export function floorplanLocalToWorldPoint(point, buildingPosition, buildingRotationY) {
|
|
17
|
+
const localX = 'x' in point ? point.x : point[0];
|
|
18
|
+
const localY = 'y' in point ? point.y : point[1];
|
|
19
|
+
const cos = Math.cos(buildingRotationY);
|
|
20
|
+
const sin = Math.sin(buildingRotationY);
|
|
21
|
+
return {
|
|
22
|
+
x: buildingPosition[0] + localX * cos + localY * sin,
|
|
23
|
+
z: buildingPosition[2] - localX * sin + localY * cos,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { FloorplanGeometry, FloorplanPoint } from '@ubuilder-ai/core';
|
|
2
|
+
type FloorplanDimensionRenderMode = 'screen' | 'pdf';
|
|
3
|
+
type DimensionGeometry = Extract<FloorplanGeometry, {
|
|
4
|
+
kind: 'dimension';
|
|
5
|
+
}>;
|
|
6
|
+
type DimensionStringGeometry = Extract<FloorplanGeometry, {
|
|
7
|
+
kind: 'dimension-string';
|
|
8
|
+
}>;
|
|
9
|
+
export type ArchitecturalDimensionLayout = {
|
|
10
|
+
dimensionStart: FloorplanPoint;
|
|
11
|
+
dimensionEnd: FloorplanPoint;
|
|
12
|
+
dimensionLineStart: FloorplanPoint;
|
|
13
|
+
dimensionLineEnd: FloorplanPoint;
|
|
14
|
+
extensionStart: FloorplanPoint;
|
|
15
|
+
extensionEnd: FloorplanPoint;
|
|
16
|
+
extensionStartTip: FloorplanPoint;
|
|
17
|
+
extensionEndTip: FloorplanPoint;
|
|
18
|
+
tickHalfVector: FloorplanPoint;
|
|
19
|
+
labelPoint: FloorplanPoint;
|
|
20
|
+
labelAngleDeg: number;
|
|
21
|
+
labelPlacement: 'inside' | 'outside-end';
|
|
22
|
+
outsideStartLabelPoint?: FloorplanPoint;
|
|
23
|
+
outsideStartDimensionLineStart?: FloorplanPoint;
|
|
24
|
+
};
|
|
25
|
+
export declare function floorplanDimensionAnnotationPriority(offsetDistance: number): number;
|
|
26
|
+
export declare function computeArchitecturalDimensionLayout(geometry: DimensionGeometry, sceneRotationDeg: number, annotationUnitsPerPoint?: number): ArchitecturalDimensionLayout | null;
|
|
27
|
+
export declare function FloorplanDimensionRenderer({ geometry, sceneRotationDeg, stroke, annotationUnitsPerPoint, renderMode, }: {
|
|
28
|
+
geometry: DimensionGeometry;
|
|
29
|
+
sceneRotationDeg?: number;
|
|
30
|
+
stroke?: string;
|
|
31
|
+
annotationUnitsPerPoint?: number;
|
|
32
|
+
renderMode?: FloorplanDimensionRenderMode;
|
|
33
|
+
}): React.ReactElement | null;
|
|
34
|
+
export declare function FloorplanDimensionStringRenderer({ geometry, sceneRotationDeg, stroke, annotationUnitsPerPoint, renderMode, }: {
|
|
35
|
+
geometry: DimensionStringGeometry;
|
|
36
|
+
sceneRotationDeg?: number;
|
|
37
|
+
stroke?: string;
|
|
38
|
+
annotationUnitsPerPoint?: number;
|
|
39
|
+
renderMode?: FloorplanDimensionRenderMode;
|
|
40
|
+
}): React.ReactElement | null;
|
|
41
|
+
export {};
|
|
42
|
+
//# sourceMappingURL=floorplan-dimension-renderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"floorplan-dimension-renderer.d.ts","sourceRoot":"","sources":["../src/floorplan-dimension-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAqB1E,KAAK,4BAA4B,GAAG,QAAQ,GAAG,KAAK,CAAA;AAEpD,KAAK,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,EAAE;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC,CAAA;AAC1E,KAAK,uBAAuB,GAAG,OAAO,CAAC,iBAAiB,EAAE;IAAE,IAAI,EAAE,kBAAkB,CAAA;CAAE,CAAC,CAAA;AAGvF,MAAM,MAAM,4BAA4B,GAAG;IACzC,cAAc,EAAE,cAAc,CAAA;IAC9B,YAAY,EAAE,cAAc,CAAA;IAC5B,kBAAkB,EAAE,cAAc,CAAA;IAClC,gBAAgB,EAAE,cAAc,CAAA;IAChC,cAAc,EAAE,cAAc,CAAA;IAC9B,YAAY,EAAE,cAAc,CAAA;IAC5B,iBAAiB,EAAE,cAAc,CAAA;IACjC,eAAe,EAAE,cAAc,CAAA;IAC/B,cAAc,EAAE,cAAc,CAAA;IAC9B,UAAU,EAAE,cAAc,CAAA;IAC1B,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,QAAQ,GAAG,aAAa,CAAA;IACxC,sBAAsB,CAAC,EAAE,cAAc,CAAA;IACvC,8BAA8B,CAAC,EAAE,cAAc,CAAA;CAChD,CAAA;AAED,wBAAgB,oCAAoC,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED,wBAAgB,mCAAmC,CACjD,QAAQ,EAAE,iBAAiB,EAC3B,gBAAgB,EAAE,MAAM,EACxB,uBAAuB,CAAC,EAAE,MAAM,GAC/B,4BAA4B,GAAG,IAAI,CAuFrC;AAkBD,wBAAgB,0BAA0B,CAAC,EACzC,QAAQ,EACR,gBAAoB,EACpB,MAAqC,EACrC,uBAAuB,EACvB,UAAqB,GACtB,EAAE;IACD,QAAQ,EAAE,iBAAiB,CAAA;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAChC,UAAU,CAAC,EAAE,4BAA4B,CAAA;CAC1C,GAAG,KAAK,CAAC,YAAY,GAAG,IAAI,CAyH5B;AAED,wBAAgB,gCAAgC,CAAC,EAC/C,QAAQ,EACR,gBAAoB,EACpB,MAAqC,EACrC,uBAAuB,EACvB,UAAqB,GACtB,EAAE;IACD,QAAQ,EAAE,uBAAuB,CAAA;IACjC,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAChC,UAAU,CAAC,EAAE,4BAA4B,CAAA;CAC1C,GAAG,KAAK,CAAC,YAAY,GAAG,IAAI,CAoK5B"}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { createElement as _createElement } from "react";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { resolveFloorplanLabelAngle } from './floorplan-label-angle';
|
|
4
|
+
const EXTENSION_START_GAP = 0.075;
|
|
5
|
+
const TICK_HALF_LENGTH = 0.09;
|
|
6
|
+
const LABEL_FONT_SIZE = 0.15;
|
|
7
|
+
const LABEL_BASELINE_OFFSET = 0.12;
|
|
8
|
+
const LABEL_CHARACTER_WIDTH_RATIO = 0.62;
|
|
9
|
+
const LABEL_END_GAP = 0.075;
|
|
10
|
+
const DOCUMENT_EXTENSION_START_GAP_PT = 3;
|
|
11
|
+
const DOCUMENT_EXTENSION_OVERSHOOT_PT = 4;
|
|
12
|
+
const DOCUMENT_TICK_HALF_LENGTH_PT = 4.5;
|
|
13
|
+
const DOCUMENT_LABEL_FONT_SIZE_PT = 8;
|
|
14
|
+
const DOCUMENT_LABEL_BASELINE_OFFSET_PT = 5;
|
|
15
|
+
const DOCUMENT_LABEL_END_GAP_PT = 3;
|
|
16
|
+
const LINE_STROKE_WIDTH_PX = 0.9;
|
|
17
|
+
const TICK_STROKE_WIDTH_PX = 1.35;
|
|
18
|
+
const PDF_LINE_STROKE_WIDTH_PT = 0.5;
|
|
19
|
+
const PDF_TICK_STROKE_WIDTH_PT = 0.75;
|
|
20
|
+
const SQRT_ONE_HALF = Math.SQRT1_2;
|
|
21
|
+
export function floorplanDimensionAnnotationPriority(offsetDistance) {
|
|
22
|
+
return 100 + Math.round(Math.abs(offsetDistance) * 100);
|
|
23
|
+
}
|
|
24
|
+
export function computeArchitecturalDimensionLayout(geometry, sceneRotationDeg, annotationUnitsPerPoint) {
|
|
25
|
+
const extensionStartGap = annotationUnitsPerPoint
|
|
26
|
+
? DOCUMENT_EXTENSION_START_GAP_PT * annotationUnitsPerPoint
|
|
27
|
+
: (geometry.extensionStartGap ?? EXTENSION_START_GAP);
|
|
28
|
+
const extensionOvershoot = annotationUnitsPerPoint
|
|
29
|
+
? DOCUMENT_EXTENSION_OVERSHOOT_PT * annotationUnitsPerPoint
|
|
30
|
+
: geometry.extensionOvershoot;
|
|
31
|
+
const tickHalfLength = annotationUnitsPerPoint
|
|
32
|
+
? DOCUMENT_TICK_HALF_LENGTH_PT * annotationUnitsPerPoint
|
|
33
|
+
: TICK_HALF_LENGTH;
|
|
34
|
+
const labelFontSize = annotationUnitsPerPoint
|
|
35
|
+
? DOCUMENT_LABEL_FONT_SIZE_PT * annotationUnitsPerPoint
|
|
36
|
+
: LABEL_FONT_SIZE;
|
|
37
|
+
const labelEndGap = annotationUnitsPerPoint
|
|
38
|
+
? DOCUMENT_LABEL_END_GAP_PT * annotationUnitsPerPoint
|
|
39
|
+
: LABEL_END_GAP;
|
|
40
|
+
const offsetX = geometry.offsetNormal[0] * geometry.offsetDistance;
|
|
41
|
+
const offsetY = geometry.offsetNormal[1] * geometry.offsetDistance;
|
|
42
|
+
const dimensionStart = geometry.dimensionStart ?? [
|
|
43
|
+
geometry.start[0] + offsetX,
|
|
44
|
+
geometry.start[1] + offsetY,
|
|
45
|
+
];
|
|
46
|
+
const dimensionEnd = geometry.dimensionEnd ?? [
|
|
47
|
+
geometry.end[0] + offsetX,
|
|
48
|
+
geometry.end[1] + offsetY,
|
|
49
|
+
];
|
|
50
|
+
const dx = dimensionEnd[0] - dimensionStart[0];
|
|
51
|
+
const dy = dimensionEnd[1] - dimensionStart[1];
|
|
52
|
+
const length = Math.hypot(dx, dy);
|
|
53
|
+
if (length < 1e-6)
|
|
54
|
+
return null;
|
|
55
|
+
const dirX = dx / length;
|
|
56
|
+
const dirY = dy / length;
|
|
57
|
+
const startOffsetDistance = dot(subtract(dimensionStart, geometry.start), geometry.offsetNormal);
|
|
58
|
+
const endOffsetDistance = dot(subtract(dimensionEnd, geometry.end), geometry.offsetNormal);
|
|
59
|
+
const startExtensionGap = Math.min(extensionStartGap, Math.max(0, startOffsetDistance - 0.01));
|
|
60
|
+
const endExtensionGap = Math.min(extensionStartGap, Math.max(0, endOffsetDistance - 0.01));
|
|
61
|
+
// A 45-degree architectural slash. Every terminator within one string uses
|
|
62
|
+
// this same vector instead of rotating independently around its endpoint.
|
|
63
|
+
const tickHalfVector = [
|
|
64
|
+
(dirX + dirY) * tickHalfLength * SQRT_ONE_HALF,
|
|
65
|
+
(dirY - dirX) * tickHalfLength * SQRT_ONE_HALF,
|
|
66
|
+
];
|
|
67
|
+
const labelWidth = Math.max(labelFontSize, geometry.text.length * labelFontSize * LABEL_CHARACTER_WIDTH_RATIO);
|
|
68
|
+
const labelPlacement = length >= labelWidth + labelEndGap * 2 ? 'inside' : 'outside-end';
|
|
69
|
+
const direction = [dirX, dirY];
|
|
70
|
+
const labelPoint = labelPlacement === 'inside'
|
|
71
|
+
? [
|
|
72
|
+
(dimensionStart[0] + dimensionEnd[0]) / 2,
|
|
73
|
+
(dimensionStart[1] + dimensionEnd[1]) / 2,
|
|
74
|
+
]
|
|
75
|
+
: addScaled(dimensionEnd, direction, labelEndGap + labelWidth / 2);
|
|
76
|
+
const dimensionLineEnd = labelPlacement === 'inside'
|
|
77
|
+
? dimensionEnd
|
|
78
|
+
: addScaled(dimensionEnd, direction, labelEndGap * 2 + labelWidth);
|
|
79
|
+
const outsideStartLabelPoint = labelPlacement === 'outside-end'
|
|
80
|
+
? addScaled(dimensionStart, direction, -(labelEndGap + labelWidth / 2))
|
|
81
|
+
: undefined;
|
|
82
|
+
const outsideStartDimensionLineStart = labelPlacement === 'outside-end'
|
|
83
|
+
? addScaled(dimensionStart, direction, -(labelEndGap * 2 + labelWidth))
|
|
84
|
+
: undefined;
|
|
85
|
+
return {
|
|
86
|
+
dimensionStart,
|
|
87
|
+
dimensionEnd,
|
|
88
|
+
dimensionLineStart: dimensionStart,
|
|
89
|
+
dimensionLineEnd,
|
|
90
|
+
extensionStart: addScaled(geometry.start, geometry.offsetNormal, startExtensionGap),
|
|
91
|
+
extensionEnd: addScaled(geometry.end, geometry.offsetNormal, endExtensionGap),
|
|
92
|
+
extensionStartTip: addScaled(dimensionStart, geometry.offsetNormal, extensionOvershoot),
|
|
93
|
+
extensionEndTip: addScaled(dimensionEnd, geometry.offsetNormal, extensionOvershoot),
|
|
94
|
+
tickHalfVector,
|
|
95
|
+
labelPoint,
|
|
96
|
+
labelAngleDeg: resolveFloorplanLabelAngle(Math.atan2(dy, dx), sceneRotationDeg),
|
|
97
|
+
labelPlacement,
|
|
98
|
+
outsideStartLabelPoint,
|
|
99
|
+
outsideStartDimensionLineStart,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function subtract(left, right) {
|
|
103
|
+
return [left[0] - right[0], left[1] - right[1]];
|
|
104
|
+
}
|
|
105
|
+
function dot(left, right) {
|
|
106
|
+
return left[0] * right[0] + left[1] * right[1];
|
|
107
|
+
}
|
|
108
|
+
function addScaled(point, direction, distance) {
|
|
109
|
+
return [point[0] + direction[0] * distance, point[1] + direction[1] * distance];
|
|
110
|
+
}
|
|
111
|
+
export function FloorplanDimensionRenderer({ geometry, sceneRotationDeg = 0, stroke = geometry.stroke ?? '#334155', annotationUnitsPerPoint, renderMode = 'screen', }) {
|
|
112
|
+
const layout = computeArchitecturalDimensionLayout(geometry, sceneRotationDeg, annotationUnitsPerPoint);
|
|
113
|
+
if (!layout)
|
|
114
|
+
return null;
|
|
115
|
+
const labelFontSize = annotationUnitsPerPoint
|
|
116
|
+
? DOCUMENT_LABEL_FONT_SIZE_PT * annotationUnitsPerPoint
|
|
117
|
+
: LABEL_FONT_SIZE;
|
|
118
|
+
const labelBaselineOffset = annotationUnitsPerPoint
|
|
119
|
+
? DOCUMENT_LABEL_BASELINE_OFFSET_PT * annotationUnitsPerPoint
|
|
120
|
+
: LABEL_BASELINE_OFFSET;
|
|
121
|
+
const labelY = geometry.textPosition === 'centered' ? labelFontSize * 0.35 : -labelBaselineOffset;
|
|
122
|
+
const lineProps = {
|
|
123
|
+
stroke,
|
|
124
|
+
strokeLinecap: 'butt',
|
|
125
|
+
strokeWidth: renderMode === 'pdf' ? PDF_LINE_STROKE_WIDTH_PT : LINE_STROKE_WIDTH_PX,
|
|
126
|
+
vectorEffect: 'non-scaling-stroke',
|
|
127
|
+
};
|
|
128
|
+
const tickStrokeWidth = renderMode === 'pdf' ? PDF_TICK_STROKE_WIDTH_PT : TICK_STROKE_WIDTH_PX;
|
|
129
|
+
const terminator = geometry.terminator ?? 'architectural-tick';
|
|
130
|
+
const labelTransform = `translate(${layout.labelPoint[0]} ${layout.labelPoint[1]}) rotate(${layout.labelAngleDeg})`;
|
|
131
|
+
const outsideStartLocalShift = layout.outsideStartLabelPoint
|
|
132
|
+
? rotateVector(subtract(layout.outsideStartLabelPoint, layout.labelPoint), (-layout.labelAngleDeg * Math.PI) / 180)
|
|
133
|
+
: undefined;
|
|
134
|
+
return (_jsxs("g", { "data-floorplan-dimension": "", pointerEvents: "none", children: [_jsx("line", { ...lineProps, x1: layout.extensionStart[0], x2: layout.extensionStartTip[0], y1: layout.extensionStart[1], y2: layout.extensionStartTip[1] }), _jsx("line", { ...lineProps, x1: layout.extensionEnd[0], x2: layout.extensionEndTip[0], y1: layout.extensionEnd[1], y2: layout.extensionEndTip[1] }), _jsx("line", { ...lineProps, "data-floorplan-dimension-default-x1": layout.dimensionLineStart[0], "data-floorplan-dimension-default-x2": layout.dimensionLineEnd[0], "data-floorplan-dimension-default-y1": layout.dimensionLineStart[1], "data-floorplan-dimension-default-y2": layout.dimensionLineEnd[1], "data-floorplan-dimension-line": "", "data-floorplan-dimension-outside-start-x1": layout.outsideStartDimensionLineStart?.[0], "data-floorplan-dimension-outside-start-x2": layout.dimensionEnd[0], "data-floorplan-dimension-outside-start-y1": layout.outsideStartDimensionLineStart?.[1], "data-floorplan-dimension-outside-start-y2": layout.dimensionEnd[1], x1: layout.dimensionLineStart[0], x2: layout.dimensionLineEnd[0], y1: layout.dimensionLineStart[1], y2: layout.dimensionLineEnd[1] }), renderTerminator(terminator, layout.dimensionStart, layout.dimensionEnd, layout, lineProps, tickStrokeWidth), renderTerminator(terminator, layout.dimensionEnd, layout.dimensionStart, layout, lineProps, tickStrokeWidth), layout.labelPlacement === 'outside-end' && annotationUnitsPerPoint !== undefined ? (_jsx("line", { ...lineProps, "data-floorplan-dimension-leader": "", visibility: "hidden", x1: layout.dimensionEnd[0], x2: layout.labelPoint[0], y1: layout.dimensionEnd[1], y2: layout.labelPoint[1] })) : null, _jsx("g", { "data-floorplan-annotation-angle-radians": Math.atan2(geometry.end[1] - geometry.start[1], geometry.end[0] - geometry.start[0]), "data-floorplan-annotation-default-transform": labelTransform, "data-floorplan-annotation-label": "", "data-floorplan-annotation-priority": floorplanDimensionAnnotationPriority(geometry.offsetDistance), "data-floorplan-annotation-transform-before-rotation": `translate(${layout.labelPoint[0]} ${layout.labelPoint[1]})`, "data-floorplan-dimension-label-placement": layout.labelPlacement, "data-floorplan-dimension-outside-start-local-x": outsideStartLocalShift?.[0], "data-floorplan-dimension-outside-start-local-y": outsideStartLocalShift?.[1], "data-floorplan-dimension-start-x": layout.dimensionStart[0], "data-floorplan-dimension-start-y": layout.dimensionStart[1], "data-floorplan-dimension-end-x": layout.dimensionEnd[0], "data-floorplan-dimension-end-y": layout.dimensionEnd[1], transform: labelTransform, children: _jsx(DimensionLabel, { fontSize: labelFontSize, renderMode: renderMode, stroke: stroke, text: geometry.text, y: labelY }) })] }));
|
|
135
|
+
}
|
|
136
|
+
export function FloorplanDimensionStringRenderer({ geometry, sceneRotationDeg = 0, stroke = geometry.stroke ?? '#334155', annotationUnitsPerPoint, renderMode = 'screen', }) {
|
|
137
|
+
const segmentLayouts = geometry.segments.flatMap((segment, index) => {
|
|
138
|
+
const segmentGeometry = {
|
|
139
|
+
kind: 'dimension',
|
|
140
|
+
start: segment.start,
|
|
141
|
+
end: segment.end,
|
|
142
|
+
dimensionStart: segment.dimensionStart,
|
|
143
|
+
dimensionEnd: segment.dimensionEnd,
|
|
144
|
+
offsetNormal: geometry.offsetNormal,
|
|
145
|
+
offsetDistance: geometry.offsetDistance,
|
|
146
|
+
extensionOvershoot: geometry.extensionOvershoot,
|
|
147
|
+
extensionStartGap: geometry.extensionStartGap,
|
|
148
|
+
terminator: geometry.terminator,
|
|
149
|
+
textPosition: geometry.textPosition,
|
|
150
|
+
text: segment.text,
|
|
151
|
+
stroke: geometry.stroke,
|
|
152
|
+
};
|
|
153
|
+
const layout = computeArchitecturalDimensionLayout(segmentGeometry, sceneRotationDeg, annotationUnitsPerPoint);
|
|
154
|
+
return layout ? [{ index, layout, segment: segmentGeometry }] : [];
|
|
155
|
+
});
|
|
156
|
+
if (segmentLayouts.length === 0)
|
|
157
|
+
return null;
|
|
158
|
+
const labelFontSize = annotationUnitsPerPoint
|
|
159
|
+
? DOCUMENT_LABEL_FONT_SIZE_PT * annotationUnitsPerPoint
|
|
160
|
+
: LABEL_FONT_SIZE;
|
|
161
|
+
const labelBaselineOffset = annotationUnitsPerPoint
|
|
162
|
+
? DOCUMENT_LABEL_BASELINE_OFFSET_PT * annotationUnitsPerPoint
|
|
163
|
+
: LABEL_BASELINE_OFFSET;
|
|
164
|
+
const labelY = geometry.textPosition === 'centered' ? labelFontSize * 0.35 : -labelBaselineOffset;
|
|
165
|
+
const lineProps = {
|
|
166
|
+
stroke,
|
|
167
|
+
strokeLinecap: 'butt',
|
|
168
|
+
strokeWidth: renderMode === 'pdf' ? PDF_LINE_STROKE_WIDTH_PT : LINE_STROKE_WIDTH_PX,
|
|
169
|
+
vectorEffect: 'non-scaling-stroke',
|
|
170
|
+
};
|
|
171
|
+
const tickStrokeWidth = renderMode === 'pdf' ? PDF_TICK_STROKE_WIDTH_PT : TICK_STROKE_WIDTH_PX;
|
|
172
|
+
const extensionLines = new Map();
|
|
173
|
+
const ticks = new Map();
|
|
174
|
+
for (const { layout } of segmentLayouts) {
|
|
175
|
+
extensionLines.set(pointKey(layout.dimensionStart), {
|
|
176
|
+
start: layout.extensionStart,
|
|
177
|
+
tip: layout.extensionStartTip,
|
|
178
|
+
});
|
|
179
|
+
extensionLines.set(pointKey(layout.dimensionEnd), {
|
|
180
|
+
start: layout.extensionEnd,
|
|
181
|
+
tip: layout.extensionEndTip,
|
|
182
|
+
});
|
|
183
|
+
ticks.set(pointKey(layout.dimensionStart), {
|
|
184
|
+
point: layout.dimensionStart,
|
|
185
|
+
toward: layout.dimensionEnd,
|
|
186
|
+
tickHalfVector: layout.tickHalfVector,
|
|
187
|
+
});
|
|
188
|
+
ticks.set(pointKey(layout.dimensionEnd), {
|
|
189
|
+
point: layout.dimensionEnd,
|
|
190
|
+
toward: layout.dimensionStart,
|
|
191
|
+
tickHalfVector: layout.tickHalfVector,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
const terminator = geometry.terminator ?? 'architectural-tick';
|
|
195
|
+
return (_jsxs("g", { "data-floorplan-dimension-string": "", pointerEvents: "none", children: [[...extensionLines.values()].map((line, index) => (_createElement("line", { ...lineProps, key: `extension-${index}`, x1: line.start[0], x2: line.tip[0], y1: line.start[1], y2: line.tip[1] }))), segmentLayouts.map(({ index, layout }) => (_createElement("line", { ...lineProps, "data-floorplan-dimension-default-x1": layout.dimensionLineStart[0], "data-floorplan-dimension-default-x2": layout.dimensionLineEnd[0], "data-floorplan-dimension-default-y1": layout.dimensionLineStart[1], "data-floorplan-dimension-default-y2": layout.dimensionLineEnd[1], "data-floorplan-dimension-line": "", "data-floorplan-dimension-outside-start-x1": layout.outsideStartDimensionLineStart?.[0], "data-floorplan-dimension-outside-start-x2": layout.dimensionEnd[0], "data-floorplan-dimension-outside-start-y1": layout.outsideStartDimensionLineStart?.[1], "data-floorplan-dimension-outside-start-y2": layout.dimensionEnd[1], key: `dimension-line-${index}`, x1: layout.dimensionLineStart[0], x2: layout.dimensionLineEnd[0], y1: layout.dimensionLineStart[1], y2: layout.dimensionLineEnd[1] }))), [...ticks.values()].map(({ point, toward, tickHalfVector }, index) => renderTerminator(terminator, point, toward, { tickHalfVector }, lineProps, tickStrokeWidth, `tick-${index}`)), segmentLayouts.map(({ index, layout, segment }) => {
|
|
196
|
+
const labelTransform = `translate(${layout.labelPoint[0]} ${layout.labelPoint[1]}) rotate(${layout.labelAngleDeg})`;
|
|
197
|
+
const outsideStartLocalShift = layout.outsideStartLabelPoint
|
|
198
|
+
? rotateVector(subtract(layout.outsideStartLabelPoint, layout.labelPoint), (-layout.labelAngleDeg * Math.PI) / 180)
|
|
199
|
+
: undefined;
|
|
200
|
+
return (_jsxs("g", { children: [layout.labelPlacement === 'outside-end' && annotationUnitsPerPoint !== undefined ? (_jsx("line", { ...lineProps, "data-floorplan-dimension-leader": "", visibility: "hidden", x1: layout.dimensionEnd[0], x2: layout.labelPoint[0], y1: layout.dimensionEnd[1], y2: layout.labelPoint[1] })) : null, _jsx("g", { "data-floorplan-annotation-angle-radians": Math.atan2(segment.end[1] - segment.start[1], segment.end[0] - segment.start[0]), "data-floorplan-annotation-default-transform": labelTransform, "data-floorplan-annotation-label": "", "data-floorplan-annotation-priority": floorplanDimensionAnnotationPriority(geometry.offsetDistance), "data-floorplan-annotation-transform-before-rotation": `translate(${layout.labelPoint[0]} ${layout.labelPoint[1]})`, "data-floorplan-dimension-label-placement": layout.labelPlacement, "data-floorplan-dimension-outside-start-local-x": outsideStartLocalShift?.[0], "data-floorplan-dimension-outside-start-local-y": outsideStartLocalShift?.[1], "data-floorplan-dimension-start-x": layout.dimensionStart[0], "data-floorplan-dimension-start-y": layout.dimensionStart[1], "data-floorplan-dimension-end-x": layout.dimensionEnd[0], "data-floorplan-dimension-end-y": layout.dimensionEnd[1], transform: labelTransform, children: _jsx(DimensionLabel, { fontSize: labelFontSize, renderMode: renderMode, stroke: stroke, text: segment.text, y: labelY }) })] }, `label-${index}`));
|
|
201
|
+
})] }));
|
|
202
|
+
}
|
|
203
|
+
function rotateVector(vector, radians) {
|
|
204
|
+
const cosine = Math.cos(radians);
|
|
205
|
+
const sine = Math.sin(radians);
|
|
206
|
+
return [vector[0] * cosine - vector[1] * sine, vector[0] * sine + vector[1] * cosine];
|
|
207
|
+
}
|
|
208
|
+
function DimensionLabel({ text, y, fontSize, stroke, renderMode, }) {
|
|
209
|
+
const width = Math.max(fontSize, text.length * fontSize * LABEL_CHARACTER_WIDTH_RATIO);
|
|
210
|
+
const plateWidth = width + fontSize * 0.5;
|
|
211
|
+
const plateHeight = fontSize * 1.2;
|
|
212
|
+
const plateY = y - fontSize * 0.82;
|
|
213
|
+
return (_jsxs(_Fragment, { children: [renderMode === 'pdf' ? (_jsx("rect", { "data-floorplan-dimension-label-plate": "", fill: "#ffffff", height: plateHeight, rx: fontSize * 0.12, ry: fontSize * 0.12, width: plateWidth, x: -plateWidth / 2, y: plateY })) : null, _jsx("text", { fill: stroke, fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: fontSize, fontWeight: 500, paintOrder: renderMode === 'pdf' ? undefined : 'stroke', stroke: renderMode === 'pdf' ? undefined : '#ffffff', strokeLinejoin: renderMode === 'pdf' ? undefined : 'round', strokeWidth: renderMode === 'pdf' ? undefined : 3, textAnchor: "middle", vectorEffect: renderMode === 'pdf' ? undefined : 'non-scaling-stroke', x: 0, y: y, children: text })] }));
|
|
214
|
+
}
|
|
215
|
+
function renderTerminator(terminator, point, toward, layout, lineProps, tickStrokeWidth, key) {
|
|
216
|
+
const direction = normalized(point, toward);
|
|
217
|
+
if (!direction)
|
|
218
|
+
return null;
|
|
219
|
+
const tickHalfLength = Math.hypot(layout.tickHalfVector[0], layout.tickHalfVector[1]);
|
|
220
|
+
if (terminator === 'dot') {
|
|
221
|
+
return (_jsx("circle", { fill: lineProps.stroke, r: tickHalfLength * 0.45, vectorEffect: "non-scaling-stroke", cx: point[0], cy: point[1] }, key));
|
|
222
|
+
}
|
|
223
|
+
if (terminator === 'filled-arrow' || terminator === 'open-arrow') {
|
|
224
|
+
const base = addScaled(point, direction, tickHalfLength * 1.7);
|
|
225
|
+
const normal = [-direction[1], direction[0]];
|
|
226
|
+
const wing = tickHalfLength * 0.65;
|
|
227
|
+
const left = [base[0] + normal[0] * wing, base[1] + normal[1] * wing];
|
|
228
|
+
const right = [base[0] - normal[0] * wing, base[1] - normal[1] * wing];
|
|
229
|
+
if (terminator === 'filled-arrow') {
|
|
230
|
+
return (_jsx("polygon", { fill: lineProps.stroke, points: `${point[0]},${point[1]} ${left[0]},${left[1]} ${right[0]},${right[1]}`, vectorEffect: "non-scaling-stroke" }, key));
|
|
231
|
+
}
|
|
232
|
+
return (_jsxs("g", { children: [_jsx("line", { ...lineProps, strokeWidth: tickStrokeWidth, x1: point[0], x2: left[0], y1: point[1], y2: left[1] }), _jsx("line", { ...lineProps, strokeWidth: tickStrokeWidth, x1: point[0], x2: right[0], y1: point[1], y2: right[1] })] }, key));
|
|
233
|
+
}
|
|
234
|
+
const [tickX, tickY] = layout.tickHalfVector;
|
|
235
|
+
return (_createElement("line", { ...lineProps, key: key, strokeWidth: tickStrokeWidth, x1: point[0] - tickX, x2: point[0] + tickX, y1: point[1] - tickY, y2: point[1] + tickY }));
|
|
236
|
+
}
|
|
237
|
+
function normalized(start, end) {
|
|
238
|
+
const dx = end[0] - start[0];
|
|
239
|
+
const dy = end[1] - start[1];
|
|
240
|
+
const magnitude = Math.hypot(dx, dy);
|
|
241
|
+
return magnitude <= 1e-6 ? null : [dx / magnitude, dy / magnitude];
|
|
242
|
+
}
|
|
243
|
+
function pointKey(point) {
|
|
244
|
+
return `${point[0].toFixed(6)},${point[1].toFixed(6)}`;
|
|
245
|
+
}
|