@pascal-app/editor 1.0.7 → 1.1.0
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 +7 -7
- package/src/components/editor/custom-camera-controls.tsx +90 -34
- package/src/components/editor/duplicate-node.ts +2 -1
- package/src/components/editor/first-person-controls.tsx +5 -7
- package/src/components/editor/fit-scene-framing.test.ts +318 -0
- package/src/components/editor/fit-scene-framing.ts +142 -0
- package/src/components/editor/floating-action-menu.tsx +4 -2
- package/src/components/editor/floor-openings-overlay.tsx +1 -1
- package/src/components/editor/floor-region-controls.tsx +2 -2
- package/src/components/editor/floorplan-panel.tsx +173 -8
- package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
- package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
- package/src/components/editor/footprint-height-handle.tsx +1 -1
- package/src/components/editor/group-transform-shared.ts +3 -1
- package/src/components/editor/index.tsx +20 -0
- package/src/components/editor/measurement-pill.tsx +1 -1
- package/src/components/editor/node-arrow-handles.tsx +1 -1
- package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -2
- package/src/components/editor/open-wall-ends-hint.tsx +1 -1
- package/src/components/editor/quick-measurement-card.tsx +1 -1
- package/src/components/editor/room-controls.tsx +3 -2
- package/src/components/editor/room-handles-2d.tsx +2 -1
- package/src/components/editor/room-handles.tsx +3 -3
- package/src/components/editor/room-labels.tsx +4 -2
- package/src/components/editor/site-edge-labels.tsx +6 -2
- package/src/components/editor/wall-measurement-label.tsx +30 -14
- package/src/components/editor/wall-move-side-handles.tsx +10 -6
- package/src/components/editor/wall-region-handles.tsx +2 -2
- package/src/components/editor/wall-snap-beacon-layer.tsx +53 -32
- package/src/components/editor-2d/floorplan-group-move.tsx +1 -1
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +12 -12
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +1 -2
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +22 -4
- package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +13 -1
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
- package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +22 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +14 -2
- package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
- package/src/components/systems/roof/roof-edit-system.tsx +30 -19
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
- package/src/components/tools/distribution/distribution-run-tool.tsx +150 -32
- package/src/components/tools/item/move-tool.tsx +1 -6
- package/src/components/tools/item/use-placement-coordinator.tsx +1 -1
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +1 -1
- package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +38 -14
- package/src/components/tools/shared/placement-box.tsx +1 -5
- package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
- package/src/components/tools/shared/segment-angle.ts +0 -4
- package/src/components/tools/tool-manager.tsx +1 -24
- package/src/components/tools/wall/wall-typed-entry.test.ts +99 -0
- package/src/components/tools/wall/wall-typed-entry.ts +70 -0
- package/src/components/ui/controls/slider-control.tsx +18 -6
- package/src/components/ui/controls/tool-options-panel.tsx +74 -44
- package/src/components/ui/helpers/helper-manager.test.tsx +53 -0
- package/src/components/ui/helpers/helper-manager.tsx +54 -0
- package/src/components/ui/panels/floor-foundation-panel.tsx +1 -1
- package/src/components/ui/panels/floor-opening-panel.tsx +1 -1
- package/src/components/ui/panels/mobile-selection-bar.tsx +28 -24
- package/src/components/ui/panels/multi-height-mode.tsx +3 -3
- package/src/components/ui/panels/multi-selection-panel.tsx +1 -1
- package/src/components/ui/panels/panel-manager.tsx +4 -2
- package/src/components/ui/panels/panel-wrapper.tsx +41 -18
- package/src/components/ui/panels/room-construction-rows.tsx +1 -1
- package/src/components/ui/panels/room-openings-field.tsx +1 -1
- package/src/components/ui/panels/room-panel.tsx +1 -1
- package/src/components/ui/right-stack.test.ts +13 -6
- package/src/components/ui/right-stack.tsx +21 -10
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +1 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +1 -1
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +1 -1
- package/src/components/viewer/viewer-rooms.tsx +1 -1
- package/src/i18n/locales/en/messages.po +856 -124
- package/src/i18n/locales/en/messages.ts +1 -1
- package/src/i18n/locales/en-XA/messages.ts +1 -1
- package/src/i18n/locales/es/messages.po +2087 -257
- package/src/i18n/locales/fr/messages.po +2087 -257
- package/src/i18n/locales/ja/messages.po +2087 -257
- package/src/i18n/locales/zh-CN/messages.po +2087 -257
- package/src/index.tsx +74 -47
- package/src/lib/contextual-help.ts +6 -0
- package/src/lib/elevation-guides.ts +21 -6
- package/src/lib/elevator-support.ts +1 -72
- package/src/lib/floor-footprints.test.ts +1 -1
- package/src/lib/floor-footprints.ts +1 -9
- package/src/lib/floor-foundation-support.test.ts +8 -3
- package/src/lib/floor-opening-draft.ts +1 -1
- package/src/lib/floor-region-session.ts +1 -1
- package/src/lib/floorplan/open-wall-ends.ts +2 -1
- package/src/lib/gesture-lifecycle.ts +3 -1
- package/src/lib/interaction/scope.ts +1 -0
- package/src/lib/layer-thickness.ts +1 -1
- package/src/lib/level-selection.test.ts +18 -2
- package/src/lib/level-selection.ts +31 -8
- package/src/lib/linear-display.ts +1 -1
- package/src/lib/live-gesture-round-trip.test.ts +1 -1
- package/src/lib/measurement-label.test.ts +1 -1
- package/src/lib/measurements.test.ts +8 -7
- package/src/lib/measurements.ts +1 -218
- package/src/lib/node-action-movement.test.tsx +115 -0
- package/src/lib/node-action-movement.ts +2 -0
- package/src/lib/placement-metadata.ts +4 -0
- package/src/lib/point-ask/describe-target.ts +6 -8
- package/src/lib/point-ask/region.ts +1 -1
- package/src/lib/print-shell-compiler.ts +12 -1
- package/src/lib/print-wall-solids.test.ts +11 -0
- package/src/lib/print-wall-solids.ts +9 -0
- package/src/lib/room-assembly-overlay.ts +1 -1
- package/src/lib/room-built-on.test.ts +2 -5
- package/src/lib/room-built-on.ts +1 -15
- package/src/lib/room-construction-commands.ts +3 -2
- package/src/lib/room-controls-phase6.test.ts +1 -1
- package/src/lib/room-handle-drag.ts +6 -25
- package/src/lib/room-push-dimensions.test.ts +1 -1
- package/src/lib/scene-bounds.test.ts +144 -0
- package/src/lib/scene-bounds.ts +79 -15
- package/src/lib/scene-clipboard.ts +2 -1
- package/src/lib/snapping-mode.test.ts +23 -1
- package/src/lib/viewer-selection.ts +1 -1
- package/src/lib/wall-region-list.ts +3 -1
- package/src/lib/window-interaction.test.ts +4 -2
- package/src/store/use-editor.tsx +5 -0
- package/src/store/use-placement-typing.test.ts +165 -0
- package/src/store/use-placement-typing.ts +186 -0
- package/src/components/tools/elevator/elevator-defaults.ts +0 -8
- package/src/components/tools/elevator/elevator-tool.tsx +0 -372
- package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
- package/src/lib/door-interaction.ts +0 -102
- package/src/lib/measurement-label.ts +0 -68
- package/src/lib/window-interaction.ts +0 -142
|
@@ -301,7 +301,7 @@ function StructureThicknessHandle({
|
|
|
301
301
|
const structureHeight =
|
|
302
302
|
node.type === 'fence'
|
|
303
303
|
? (node.height ?? 1.8)
|
|
304
|
-
: getWallEffectiveHeightForNodes(node, useScene.getState().nodes)
|
|
304
|
+
: getWallEffectiveHeightForNodes(node, useScene.getState().nodes, 0.5)
|
|
305
305
|
const outward = new Vector2(frame.normal.x * side, frame.normal.y * side)
|
|
306
306
|
const faceOffset = getWallLocalFaceZ({ ...node, thickness }, side > 0 ? 'a' : 'b') * side + 0.006
|
|
307
307
|
const position: [number, number, number] = [
|
|
@@ -375,7 +375,7 @@ function StructureThicknessHandle({
|
|
|
375
375
|
const initialStructureHeight =
|
|
376
376
|
initialNode.type === 'fence'
|
|
377
377
|
? (initialNode.height ?? 1.8)
|
|
378
|
-
: getWallEffectiveHeightForNodes(initialNode, useScene.getState().nodes)
|
|
378
|
+
: getWallEffectiveHeightForNodes(initialNode, useScene.getState().nodes, 0.5)
|
|
379
379
|
const initialStructureThickness =
|
|
380
380
|
initialNode.type === 'fence'
|
|
381
381
|
? (initialNode.thickness ?? 0.08)
|
|
@@ -578,7 +578,11 @@ function WallCornerLeaderHandle({ wall, endpoint }: { wall: WallNode; endpoint:
|
|
|
578
578
|
const corner = endpoint === 'start' ? wall.start : wall.end
|
|
579
579
|
const x = corner[0]
|
|
580
580
|
const z = corner[1]
|
|
581
|
-
const wallHeight = getWallEffectiveHeightForNodes(
|
|
581
|
+
const wallHeight = getWallEffectiveHeightForNodes(
|
|
582
|
+
wall,
|
|
583
|
+
useScene.getState().nodes,
|
|
584
|
+
endpoint === 'start' ? 0 : 1,
|
|
585
|
+
)
|
|
582
586
|
|
|
583
587
|
const dashedGeometry = useMemo(() => buildDashedVerticalGeometry(wallHeight), [wallHeight])
|
|
584
588
|
const hitGeometry = useMemo(() => createEndpointHitAreaGeometry(CORNER_HEX_RADIUS), [])
|
|
@@ -784,7 +788,7 @@ function WallBaseElevationHandle({
|
|
|
784
788
|
levelObject.updateWorldMatrix(true, false)
|
|
785
789
|
const initialBase = getWallBaseElevationForNodes(initialNode, sceneApi.nodes())
|
|
786
790
|
const supportBase = initialBase - (initialNode.supportOffset ?? 0)
|
|
787
|
-
const initialHeight = getWallEffectiveHeightForNodes(initialNode, sceneApi.nodes())
|
|
791
|
+
const initialHeight = getWallEffectiveHeightForNodes(initialNode, sceneApi.nodes(), 0)
|
|
788
792
|
const midpointWorld = new Vector3(midpoint[0], initialBase, midpoint[1]).applyMatrix4(
|
|
789
793
|
levelObject.matrixWorld,
|
|
790
794
|
)
|
|
@@ -920,7 +924,7 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
|
|
|
920
924
|
const wallAngle = Math.atan2(-dirZ, dirX)
|
|
921
925
|
// `wall` is the override-merged effective wall (see
|
|
922
926
|
// WallMoveSideHandlesForWall), so this height is already live during a drag.
|
|
923
|
-
const wallHeight = getWallEffectiveHeightForNodes(wall, useScene.getState().nodes)
|
|
927
|
+
const wallHeight = getWallEffectiveHeightForNodes(wall, useScene.getState().nodes, 0.5)
|
|
924
928
|
const handleY = wallHeight + HEIGHT_HANDLE_OFFSET
|
|
925
929
|
|
|
926
930
|
const activateHeightResize = (event: ThreeEvent<PointerEvent>) => {
|
|
@@ -973,7 +977,7 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
|
|
|
973
977
|
|
|
974
978
|
// Dragging the top makes the wall custom-height; seed from the resolved
|
|
975
979
|
// effective height so a plane-bound wall's drag starts at its real top.
|
|
976
|
-
const initialHeight = getWallEffectiveHeightForNodes(wall, useScene.getState().nodes)
|
|
980
|
+
const initialHeight = getWallEffectiveHeightForNodes(wall, useScene.getState().nodes, 0)
|
|
977
981
|
const wallId = wall.id as AnyNodeId
|
|
978
982
|
let pendingHeight = initialHeight
|
|
979
983
|
let releaseSpatialCapture: (() => void) | null = null
|
|
@@ -203,7 +203,7 @@ function WallRegionHandlesForWall({ wall }: { wall: WallNode }) {
|
|
|
203
203
|
const frame: WallFrame = {
|
|
204
204
|
wall: effectiveWall,
|
|
205
205
|
baseElevation: getWallBaseElevationForNodes(effectiveWall, nodes),
|
|
206
|
-
wallHeight: getWallEffectiveHeightForNodes(effectiveWall, nodes),
|
|
206
|
+
wallHeight: getWallEffectiveHeightForNodes(effectiveWall, nodes, 0.5),
|
|
207
207
|
angle: Math.atan2(
|
|
208
208
|
effectiveWall.end[1] - effectiveWall.start[1],
|
|
209
209
|
effectiveWall.end[0] - effectiveWall.start[0],
|
|
@@ -426,7 +426,7 @@ function RegionEdgeHandle({
|
|
|
426
426
|
const face = region.face
|
|
427
427
|
const faceSurface = getWallFaceSurface(initialNode, face)
|
|
428
428
|
const baseAt = faceBaseSampler(initialNode.id, face)
|
|
429
|
-
const wallHeight = getWallEffectiveHeightForNodes(initialNode, nodes)
|
|
429
|
+
const wallHeight = getWallEffectiveHeightForNodes(initialNode, nodes, 0.5)
|
|
430
430
|
const faceHeight = regionFaceHeight(region, faceSurface.length, wallHeight, baseAt)
|
|
431
431
|
const bounds = resolveRegionBounds(region, faceSurface.length, faceHeight)
|
|
432
432
|
const midU = (bounds.u0 + bounds.u1) / 2
|
|
@@ -165,9 +165,10 @@ type WallTopHighlightSegment = {
|
|
|
165
165
|
angle: number
|
|
166
166
|
center: [number, number]
|
|
167
167
|
length: number
|
|
168
|
+
tCenter: number
|
|
168
169
|
}
|
|
169
170
|
|
|
170
|
-
function getWallTopY(wall: WallNode, nodes: Readonly<Record<string, AnyNode
|
|
171
|
+
function getWallTopY(wall: WallNode, nodes: Readonly<Record<string, AnyNode>>, t = 0.5): number {
|
|
171
172
|
const levelId = resolveLevelId(wall, nodes as Record<string, AnyNode>)
|
|
172
173
|
const support = spatialGridManager.getSlabSupportForWall(
|
|
173
174
|
levelId,
|
|
@@ -181,12 +182,16 @@ function getWallTopY(wall: WallNode, nodes: Readonly<Record<string, AnyNode>>) {
|
|
|
181
182
|
wall.justification,
|
|
182
183
|
)
|
|
183
184
|
return (
|
|
184
|
-
getWallTop(wall, levelId, nodes as Record<string, AnyNode>, support.elevation) +
|
|
185
|
+
getWallTop(wall, levelId, nodes as Record<string, AnyNode>, support.elevation, t) +
|
|
185
186
|
WALL_TOP_HIGHLIGHT_LIFT
|
|
186
187
|
)
|
|
187
188
|
}
|
|
188
189
|
|
|
189
|
-
function buildHighlightSegment(
|
|
190
|
+
function buildHighlightSegment(
|
|
191
|
+
start: [number, number],
|
|
192
|
+
end: [number, number],
|
|
193
|
+
tCenter = 0.5,
|
|
194
|
+
): WallTopHighlightSegment | null {
|
|
190
195
|
const dx = end[0] - start[0]
|
|
191
196
|
const dz = end[1] - start[1]
|
|
192
197
|
const length = Math.hypot(dx, dz)
|
|
@@ -196,15 +201,20 @@ function buildHighlightSegment(start: [number, number], end: [number, number]) {
|
|
|
196
201
|
angle: -Math.atan2(dz, dx),
|
|
197
202
|
center: [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2] as [number, number],
|
|
198
203
|
length,
|
|
204
|
+
tCenter,
|
|
199
205
|
}
|
|
200
206
|
}
|
|
201
207
|
|
|
202
208
|
function buildWallTopHighlightSegments(wall: WallNode): WallTopHighlightSegment[] {
|
|
203
209
|
if (!isCurvedWall(wall)) {
|
|
204
|
-
const segment = buildHighlightSegment(wall.start, wall.end)
|
|
210
|
+
const segment = buildHighlightSegment(wall.start, wall.end, 0.5)
|
|
205
211
|
return segment ? [segment] : []
|
|
206
212
|
}
|
|
207
213
|
|
|
214
|
+
const dx = wall.end[0] - wall.start[0]
|
|
215
|
+
const dz = wall.end[1] - wall.start[1]
|
|
216
|
+
const chordLenSq = dx * dx + dz * dz
|
|
217
|
+
|
|
208
218
|
const sampleCount = Math.max(
|
|
209
219
|
8,
|
|
210
220
|
Math.ceil(getWallCurveLength(wall) / CURVED_WALL_HIGHLIGHT_SEGMENT_LENGTH),
|
|
@@ -213,7 +223,16 @@ function buildWallTopHighlightSegments(wall: WallNode): WallTopHighlightSegment[
|
|
|
213
223
|
let previous = getWallCurveFrameAt(wall, 0).point
|
|
214
224
|
for (let index = 1; index <= sampleCount; index += 1) {
|
|
215
225
|
const current = getWallCurveFrameAt(wall, index / sampleCount).point
|
|
216
|
-
const
|
|
226
|
+
const midX = (previous.x + current.x) / 2
|
|
227
|
+
const midY = (previous.y + current.y) / 2
|
|
228
|
+
const tCenter =
|
|
229
|
+
chordLenSq > 1e-12
|
|
230
|
+
? Math.max(
|
|
231
|
+
0,
|
|
232
|
+
Math.min(1, ((midX - wall.start[0]) * dx + (midY - wall.start[1]) * dz) / chordLenSq),
|
|
233
|
+
)
|
|
234
|
+
: 0.5
|
|
235
|
+
const segment = buildHighlightSegment([previous.x, previous.y], [current.x, current.y], tCenter)
|
|
217
236
|
if (segment) segments.push(segment)
|
|
218
237
|
previous = current
|
|
219
238
|
}
|
|
@@ -228,38 +247,40 @@ function WallTopHighlight({
|
|
|
228
247
|
wall: WallNode
|
|
229
248
|
}) {
|
|
230
249
|
const segments = useMemo(() => buildWallTopHighlightSegments(wall), [wall])
|
|
231
|
-
const y = getWallTopY(wall, nodes)
|
|
232
250
|
const width = Math.max(getWallThickness(wall) + WALL_TOP_HIGHLIGHT_OVERHANG, 0.24)
|
|
233
251
|
const glowWidth = Math.max(getWallThickness(wall) + WALL_TOP_GLOW_OVERHANG, 0.42)
|
|
234
252
|
|
|
235
253
|
return (
|
|
236
254
|
<>
|
|
237
|
-
{segments.map((segment, index) =>
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
255
|
+
{segments.map((segment, index) => {
|
|
256
|
+
const y = getWallTopY(wall, nodes, segment.tCenter)
|
|
257
|
+
return (
|
|
258
|
+
<group key={`${wall.id}:${index}`}>
|
|
259
|
+
<mesh
|
|
260
|
+
frustumCulled={false}
|
|
261
|
+
geometry={WALL_TOP_HIGHLIGHT_GEOMETRY}
|
|
262
|
+
layers={EDITOR_LAYER}
|
|
263
|
+
material={wallTopHighlightGlowMaterial}
|
|
264
|
+
position={[segment.center[0], y - 0.003, segment.center[1]]}
|
|
265
|
+
raycast={NO_RAYCAST}
|
|
266
|
+
renderOrder={1003}
|
|
267
|
+
rotation={[0, segment.angle, 0]}
|
|
268
|
+
scale={[segment.length + WALL_TOP_END_OVERHANG, WALL_TOP_GLOW_HEIGHT, glowWidth]}
|
|
269
|
+
/>
|
|
270
|
+
<mesh
|
|
271
|
+
frustumCulled={false}
|
|
272
|
+
geometry={WALL_TOP_HIGHLIGHT_GEOMETRY}
|
|
273
|
+
layers={EDITOR_LAYER}
|
|
274
|
+
material={wallTopHighlightMaterial}
|
|
275
|
+
position={[segment.center[0], y + 0.002, segment.center[1]]}
|
|
276
|
+
raycast={NO_RAYCAST}
|
|
277
|
+
renderOrder={1004}
|
|
278
|
+
rotation={[0, segment.angle, 0]}
|
|
279
|
+
scale={[segment.length + WALL_TOP_END_OVERHANG, WALL_TOP_HIGHLIGHT_HEIGHT, width]}
|
|
280
|
+
/>
|
|
281
|
+
</group>
|
|
282
|
+
)
|
|
283
|
+
})}
|
|
263
284
|
</>
|
|
264
285
|
)
|
|
265
286
|
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
DEFAULT_ANGLE_STEP,
|
|
9
9
|
type FloorplanGeometry,
|
|
10
10
|
type FloorplanPalette,
|
|
11
|
+
formatLinearMeasurement,
|
|
11
12
|
pauseSceneHistory,
|
|
12
13
|
pauseSpaceDetection,
|
|
13
14
|
resumeSceneHistory,
|
|
@@ -23,7 +24,6 @@ import { GROUP_MOVE_DRAG_LABEL, GROUP_ROTATE_DRAG_LABEL } from '../../lib/contex
|
|
|
23
24
|
import { applyFloorplanAlignment } from '../../lib/floorplan/apply-alignment'
|
|
24
25
|
import { clientToPlan } from '../../lib/floorplan/plan-coords'
|
|
25
26
|
import { isHistoryShortcut } from '../../lib/history'
|
|
26
|
-
import { formatLinearMeasurement } from '../../lib/measurements'
|
|
27
27
|
import type { WallGroupPush } from '../../lib/room-handle-drag'
|
|
28
28
|
import { useRoomHandleDrag } from '../../lib/room-handle-drag'
|
|
29
29
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
import {
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
|
+
buildMeasurementAngleArcPoints,
|
|
6
7
|
closestMeasurementFeatureBinding,
|
|
7
8
|
collectAlignmentAnchors,
|
|
8
9
|
emitter,
|
|
10
|
+
formatAngleRadians,
|
|
11
|
+
formatAreaLabel,
|
|
12
|
+
formatLinearMeasurement,
|
|
9
13
|
formatNumber,
|
|
14
|
+
formatVolumeLabel,
|
|
10
15
|
type GeometryContext,
|
|
16
|
+
getLinearUnitLabel,
|
|
11
17
|
type MeasurementFeatureAnchor,
|
|
12
18
|
type MeasurementSnapKind,
|
|
13
19
|
measurementAngle,
|
|
@@ -16,23 +22,18 @@ import {
|
|
|
16
22
|
measurementFeatureLength,
|
|
17
23
|
measurementPerimeter,
|
|
18
24
|
measurementPrismVolume,
|
|
25
|
+
metersToLinearUnit,
|
|
19
26
|
nodeRegistry,
|
|
27
|
+
parseLinearMeasurementInput,
|
|
20
28
|
useLocale,
|
|
21
29
|
useScene,
|
|
22
30
|
} from '@pascal-app/core'
|
|
23
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
24
|
-
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
25
|
-
import { measurementPolygonLabelAnchor } from '../../lib/measurement-label'
|
|
26
31
|
import {
|
|
27
|
-
buildMeasurementAngleArcPoints,
|
|
28
|
-
formatAreaLabel,
|
|
29
|
-
formatLinearMeasurement,
|
|
30
|
-
formatVolumeLabel,
|
|
31
|
-
getLinearUnitLabel,
|
|
32
32
|
MEASUREMENT_ACTIVE_COLOR,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
} from '
|
|
33
|
+
measurementPolygonLabelAnchor,
|
|
34
|
+
useViewer,
|
|
35
|
+
} from '@pascal-app/viewer'
|
|
36
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
36
37
|
import {
|
|
37
38
|
clearSurfacePlanSnapFeedback,
|
|
38
39
|
resolveSurfacePlanPointSnap,
|
|
@@ -45,7 +46,6 @@ import {
|
|
|
45
46
|
measurementPolygonMidpoints,
|
|
46
47
|
useMeasurementDraft,
|
|
47
48
|
} from '../../store/use-measurement-draft'
|
|
48
|
-
import { formatAngleRadians } from '../tools/shared/segment-angle'
|
|
49
49
|
import { FloorplanQuickMeasureLayer } from './floorplan-quick-measure-layer'
|
|
50
50
|
import { useFloorplanRender } from './floorplan-render-context'
|
|
51
51
|
import { resolveFloorplanLabelAngle } from './renderers/floorplan-label-angle'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import type { AnyNodeId, OpenWallEnd } from '@pascal-app/core'
|
|
4
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
4
|
+
import { MEASUREMENT_DANGLING_COLOR, useViewer } from '@pascal-app/viewer'
|
|
5
5
|
import { memo, useEffect, useState } from 'react'
|
|
6
6
|
import { createPortal } from 'react-dom'
|
|
7
7
|
import {
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
useOpenWallEndsSuppressed,
|
|
11
11
|
} from '../../hooks/use-open-wall-ends'
|
|
12
12
|
import { openWallEndKey, openWallEndLabel } from '../../lib/floorplan/open-wall-ends'
|
|
13
|
-
import { MEASUREMENT_DANGLING_COLOR } from '../../lib/measurements'
|
|
14
13
|
import useEditor from '../../store/use-editor'
|
|
15
14
|
import { isViewVisible, VIEW_2D } from '../../store/view-layout'
|
|
16
15
|
import { JoinWallsPill } from '../editor/join-walls-pill'
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
collectionIdsOf,
|
|
8
8
|
createSceneApi,
|
|
9
9
|
getWallMidpointHandlePoint,
|
|
10
|
+
isNodeDuplicable,
|
|
10
11
|
type NodeQuickAction,
|
|
11
12
|
nodeRegistry,
|
|
12
13
|
runAsSingleSceneHistoryStep,
|
|
@@ -255,7 +256,7 @@ export function FloorplanRegistryActionMenu() {
|
|
|
255
256
|
const canMove =
|
|
256
257
|
!registryMoveDisabled(node) &&
|
|
257
258
|
(!!def.capabilities.movable || !!def.floorplanMoveTarget || !!def.affordanceTools?.move)
|
|
258
|
-
const canDuplicate =
|
|
259
|
+
const canDuplicate = isNodeDuplicable(node)
|
|
259
260
|
const canDelete = def.capabilities.deletable !== false
|
|
260
261
|
const canAddHole = node.type === 'slab' || node.type === 'ceiling'
|
|
261
262
|
|
|
@@ -850,14 +850,25 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
850
850
|
}
|
|
851
851
|
boxEl.setAttribute('transform', transform)
|
|
852
852
|
const oldY = originalPosition[1]
|
|
853
|
+
const position = nodeRegistry.get(movingNode.type)?.capabilities?.movable?.resolvePosition?.({
|
|
854
|
+
node: useScene.getState().nodes[movingNode.id] ?? movingNode,
|
|
855
|
+
position: [finalX, oldY, finalZ],
|
|
856
|
+
nodes: useScene.getState().nodes,
|
|
857
|
+
}) ?? [finalX, oldY, finalZ]
|
|
858
|
+
if (def?.capabilities?.movable?.resolvePosition) {
|
|
859
|
+
useLiveTransforms.getState().set(movingNode.id, {
|
|
860
|
+
position: position as [number, number, number],
|
|
861
|
+
rotation: currentRotation,
|
|
862
|
+
})
|
|
863
|
+
}
|
|
853
864
|
lastPositionValid = movableValidityConfig?.isValidPosition
|
|
854
865
|
? movableValidityConfig.isValidPosition({
|
|
855
866
|
node: {
|
|
856
867
|
...movingNode,
|
|
857
|
-
position
|
|
868
|
+
position,
|
|
858
869
|
rotation: currentRotation,
|
|
859
870
|
} as AnyNode,
|
|
860
|
-
position: [
|
|
871
|
+
position: position as [number, number, number],
|
|
861
872
|
rotation: currentRotation,
|
|
862
873
|
levelId:
|
|
863
874
|
(useViewer.getState().selection.levelId as AnyNodeId | null) ??
|
|
@@ -885,6 +896,11 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
885
896
|
(storedRotation as [number?, number?, number?])[2] ?? 0,
|
|
886
897
|
]
|
|
887
898
|
: snapped.rotation
|
|
899
|
+
const position = nodeRegistry.get(movingNode.type)?.capabilities?.movable?.resolvePosition?.({
|
|
900
|
+
node: useScene.getState().nodes[movingNode.id] ?? movingNode,
|
|
901
|
+
position: [sx, oldY, sz],
|
|
902
|
+
nodes: useScene.getState().nodes,
|
|
903
|
+
}) ?? [sx, oldY, sz]
|
|
888
904
|
const rotationPatch = 'rotation' in movingNode ? { rotation } : {}
|
|
889
905
|
const atomicPreview =
|
|
890
906
|
isFreshPlacement &&
|
|
@@ -937,7 +953,7 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
937
953
|
const committedId = commitFreshPlacementSubtree(
|
|
938
954
|
movingNode.id as AnyNodeId,
|
|
939
955
|
{
|
|
940
|
-
position
|
|
956
|
+
position,
|
|
941
957
|
...rotationPatch,
|
|
942
958
|
metadata: stripPlacementMetadataFlags((movingNode as { metadata?: unknown }).metadata),
|
|
943
959
|
visible: true,
|
|
@@ -953,7 +969,7 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
953
969
|
useScene.getState().updateNode(
|
|
954
970
|
movingNode.id as AnyNodeId,
|
|
955
971
|
{
|
|
956
|
-
position
|
|
972
|
+
position,
|
|
957
973
|
...rotationPatch,
|
|
958
974
|
} as Partial<AnyNode>,
|
|
959
975
|
)
|
|
@@ -1005,6 +1021,8 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
1005
1021
|
for (const relatedEntry of relatedEntries) {
|
|
1006
1022
|
relatedEntry.style.visibility = ''
|
|
1007
1023
|
}
|
|
1024
|
+
if (def?.capabilities?.movable?.resolvePosition)
|
|
1025
|
+
useLiveTransforms.getState().clear(movingNode.id)
|
|
1008
1026
|
boxEl.remove()
|
|
1009
1027
|
useAlignmentGuides.getState().clear()
|
|
1010
1028
|
}
|
|
@@ -19,6 +19,7 @@ export type DraftMeasurement = {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export function FloorplanDraftMeasurement({
|
|
22
|
+
lengthHidden = false,
|
|
22
23
|
measurement,
|
|
23
24
|
measurementStroke,
|
|
24
25
|
labelBackground,
|
|
@@ -26,6 +27,8 @@ export function FloorplanDraftMeasurement({
|
|
|
26
27
|
sceneRotationDeg,
|
|
27
28
|
unitsPerPixel,
|
|
28
29
|
}: {
|
|
30
|
+
/** Leave the length plate to something drawn in its place (the typed-entry HUD). */
|
|
31
|
+
lengthHidden?: boolean
|
|
29
32
|
measurement: DraftMeasurement
|
|
30
33
|
measurementStroke: string
|
|
31
34
|
labelBackground: string
|
|
@@ -68,7 +71,10 @@ export function FloorplanDraftMeasurement({
|
|
|
68
71
|
|
|
69
72
|
return (
|
|
70
73
|
<g pointerEvents="none">
|
|
71
|
-
<g
|
|
74
|
+
<g
|
|
75
|
+
display={lengthHidden ? 'none' : undefined}
|
|
76
|
+
transform={`translate(${cx} ${cy}) rotate(${labelAngleDeg})`}
|
|
77
|
+
>
|
|
72
78
|
<rect
|
|
73
79
|
fill={labelBg}
|
|
74
80
|
height={lengthPlateH}
|
|
@@ -4,17 +4,19 @@ import {
|
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
6
|
type FloorplanGeometry,
|
|
7
|
+
formatLinearMeasurement,
|
|
7
8
|
type GeometryContext,
|
|
8
9
|
nodeRegistry,
|
|
9
10
|
useScene,
|
|
10
11
|
} from '@pascal-app/core'
|
|
11
12
|
import { useViewer } from '@pascal-app/viewer'
|
|
12
13
|
import { memo, useMemo } from 'react'
|
|
13
|
-
import { formatLinearMeasurement } from '../../../lib/measurements'
|
|
14
14
|
import usePlacementPreview from '../../../store/use-placement-preview'
|
|
15
|
+
import { usePlacementTyping } from '../../../store/use-placement-typing'
|
|
15
16
|
import { useFloorplanRender, useFloorplanSceneRotation } from '../floorplan-render-context'
|
|
16
17
|
import { FloorplanDimensionRenderer } from './floorplan-dimension-renderer'
|
|
17
18
|
import { FloorplanGeometryRenderer } from './floorplan-geometry-renderer'
|
|
19
|
+
import { FloorplanPlacementTypingHud } from './floorplan-placement-typing-hud'
|
|
18
20
|
|
|
19
21
|
export interface FloorplanNodePreviewProps {
|
|
20
22
|
node: AnyNode
|
|
@@ -139,14 +141,24 @@ export const FloorplanPlacementPreviewLayer = memo(function FloorplanPlacementPr
|
|
|
139
141
|
const dimensions = usePlacementPreview((s) => s.dimensions)
|
|
140
142
|
const activeDimensionId = usePlacementPreview((s) => s.activeDimensionId)
|
|
141
143
|
const dimensionInput = usePlacementPreview((s) => s.dimensionInput)
|
|
144
|
+
const typingProjectedPosition = usePlacementTyping((s) => s.projectedPosition)
|
|
142
145
|
const unit = useViewer((s) => s.unit)
|
|
143
146
|
const metricNotation = useViewer((s) => s.metricNotation)
|
|
144
147
|
const sceneRotationDeg = useFloorplanSceneRotation()
|
|
148
|
+
const unitsPerPixel = useFloorplanRender()?.unitsPerPixel ?? 1
|
|
145
149
|
if (!node) return null
|
|
146
150
|
|
|
147
151
|
return (
|
|
148
152
|
<g data-floorplan-placement-preview>
|
|
149
153
|
<FloorplanNodePreview contextNodes={contextNodes} node={node} parentNode={parentNode} />
|
|
154
|
+
{typingProjectedPosition ? (
|
|
155
|
+
<FloorplanPlacementTypingHud
|
|
156
|
+
sceneRotationDeg={sceneRotationDeg}
|
|
157
|
+
unitsPerPixel={unitsPerPixel}
|
|
158
|
+
x={typingProjectedPosition[0]}
|
|
159
|
+
y={typingProjectedPosition[2]}
|
|
160
|
+
/>
|
|
161
|
+
) : null}
|
|
150
162
|
<g data-floorplan-placement-dimensions>
|
|
151
163
|
{dimensions
|
|
152
164
|
.filter((dimension) => dimension.renderInFloorplan !== false)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { afterEach, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
PLACEMENT_TYPING_LENGTH_ANGLE,
|
|
4
|
+
usePlacementTyping,
|
|
5
|
+
} from '../../../store/use-placement-typing'
|
|
6
|
+
import { renderCurrentMarkup } from '../../../test-utils/render-current-markup'
|
|
7
|
+
import { FloorplanPlacementTypingHud } from './floorplan-placement-typing-hud'
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
usePlacementTyping.getState().clear()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('sizes and counter-rotates the HUD from the props it is given', () => {
|
|
14
|
+
usePlacementTyping.getState().begin({ labels: PLACEMENT_TYPING_LENGTH_ANGLE, activeField: 0 })
|
|
15
|
+
|
|
16
|
+
const markup = renderCurrentMarkup(
|
|
17
|
+
<svg>
|
|
18
|
+
<FloorplanPlacementTypingHud sceneRotationDeg={30} unitsPerPixel={0.02} x={1} y={2} />
|
|
19
|
+
</svg>,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
expect(markup).toContain('transform="translate(1 2) rotate(-30)"')
|
|
23
|
+
expect(markup).toMatch(/<foreignObject[^>]*width="4.72"/)
|
|
24
|
+
expect(markup).toContain('scale(0.02)')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('renders nothing without a typing session', () => {
|
|
28
|
+
const markup = renderCurrentMarkup(
|
|
29
|
+
<svg>
|
|
30
|
+
<FloorplanPlacementTypingHud sceneRotationDeg={0} unitsPerPixel={0.02} x={0} y={0} />
|
|
31
|
+
</svg>,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
expect(markup).not.toContain('foreignObject')
|
|
35
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { usePlacementTyping } from '../../../store/use-placement-typing'
|
|
4
|
+
import { PlacementCoordinateInput } from '../../tools/shared/placement-coordinate-input'
|
|
5
|
+
|
|
6
|
+
const HUD_WIDTH_PX = 236
|
|
7
|
+
const HUD_HEIGHT_PX = 64
|
|
8
|
+
const HUD_LIFT_PX = 14
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The typed-entry HUD placed in the plan at `x`/`y` (level-local metres).
|
|
12
|
+
* HTML inside a `foreignObject` is laid out in CSS pixels equal to SVG user
|
|
13
|
+
* units, and the plan's `<g>` is zoomed and rotated, so the box is scaled by
|
|
14
|
+
* `unitsPerPixel` and counter-rotated to stay screen-sized and upright. Both
|
|
15
|
+
* come from the caller: the wall draft renders outside `FloorplanRenderProvider`.
|
|
16
|
+
* It sits just above its anchor, and only the HUD itself takes the pointer, so
|
|
17
|
+
* clicks on the ghost or the draft under it still land on the plan.
|
|
18
|
+
*/
|
|
19
|
+
export function FloorplanPlacementTypingHud({
|
|
20
|
+
placeholders,
|
|
21
|
+
sceneRotationDeg,
|
|
22
|
+
unitsPerPixel,
|
|
23
|
+
x,
|
|
24
|
+
y,
|
|
25
|
+
}: {
|
|
26
|
+
placeholders?: readonly string[]
|
|
27
|
+
sceneRotationDeg: number
|
|
28
|
+
unitsPerPixel: number
|
|
29
|
+
x: number
|
|
30
|
+
y: number
|
|
31
|
+
}) {
|
|
32
|
+
const isActive = usePlacementTyping((state) => state.isActive)
|
|
33
|
+
const scale = Math.max(unitsPerPixel, 1e-6)
|
|
34
|
+
if (!isActive) return null
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<g transform={`translate(${x} ${y}) rotate(${-sceneRotationDeg})`}>
|
|
38
|
+
<foreignObject
|
|
39
|
+
height={HUD_HEIGHT_PX * scale}
|
|
40
|
+
overflow="visible"
|
|
41
|
+
pointerEvents="none"
|
|
42
|
+
style={{ overflow: 'visible' }}
|
|
43
|
+
width={HUD_WIDTH_PX * scale}
|
|
44
|
+
x={(-HUD_WIDTH_PX / 2) * scale}
|
|
45
|
+
y={-(HUD_HEIGHT_PX + HUD_LIFT_PX) * scale}
|
|
46
|
+
>
|
|
47
|
+
<div
|
|
48
|
+
style={{
|
|
49
|
+
alignItems: 'flex-end',
|
|
50
|
+
display: 'flex',
|
|
51
|
+
height: HUD_HEIGHT_PX,
|
|
52
|
+
justifyContent: 'center',
|
|
53
|
+
overflow: 'visible',
|
|
54
|
+
pointerEvents: 'none',
|
|
55
|
+
transform: `scale(${scale})`,
|
|
56
|
+
transformOrigin: 'top left',
|
|
57
|
+
width: HUD_WIDTH_PX,
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
<PlacementCoordinateInput placeholders={placeholders} />
|
|
61
|
+
</div>
|
|
62
|
+
</foreignObject>
|
|
63
|
+
</g>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -11,11 +11,13 @@ import { type AnyNodeDefinition, emitter, nodeRegistry, registerNode } from '@pa
|
|
|
11
11
|
import { createElement } from 'react'
|
|
12
12
|
import { renderToStaticMarkup } from 'react-dom/server'
|
|
13
13
|
import { z } from 'zod'
|
|
14
|
+
import { AXIS_MOVE_DRAG_LABEL, PATH_POINT_DRAG_LABEL } from '../../../lib/contextual-help'
|
|
14
15
|
import {
|
|
15
16
|
FLOORPLAN_NODE_EXTENSION_KEY,
|
|
16
17
|
floorplanGeometryMetadata,
|
|
17
18
|
} from '../../../lib/floorplan/floorplan-extension'
|
|
18
19
|
import type { SessionWrites } from '../../../lib/session-writes'
|
|
20
|
+
import { snapContextOf } from '../../../lib/snapping-mode'
|
|
19
21
|
import {
|
|
20
22
|
buildFloorplanEntryGeometry,
|
|
21
23
|
cancelFloorplanAffordanceDrag,
|
|
@@ -276,6 +278,26 @@ describe('floorplan affordance ownership', () => {
|
|
|
276
278
|
driver: 'floorplan',
|
|
277
279
|
})
|
|
278
280
|
})
|
|
281
|
+
|
|
282
|
+
// With no scope, Select mode reads every snap mode as off, so these drags
|
|
283
|
+
// never snapped to the grid on the plan.
|
|
284
|
+
test('gives the MEP run point and segment drags a handle-drag scope in the run snap context', () => {
|
|
285
|
+
const point = floorplanAffordanceReshapeScope('move-path-point', 'duct_1', { pointIndex: 0 })
|
|
286
|
+
const segment = floorplanAffordanceReshapeScope('move-segment', 'duct_1', { segmentIndex: 0 })
|
|
287
|
+
expect(point).toEqual({ kind: 'handle-drag', nodeId: 'duct_1', handle: PATH_POINT_DRAG_LABEL })
|
|
288
|
+
expect(segment).toEqual({ kind: 'handle-drag', nodeId: 'duct_1', handle: AXIS_MOVE_DRAG_LABEL })
|
|
289
|
+
for (const scope of [point, segment]) {
|
|
290
|
+
expect(
|
|
291
|
+
snapContextOf({
|
|
292
|
+
scope: scope!,
|
|
293
|
+
mode: 'select',
|
|
294
|
+
tool: null,
|
|
295
|
+
profileOf: () => undefined,
|
|
296
|
+
profileOfNode: () => 'structural',
|
|
297
|
+
}),
|
|
298
|
+
).toBe('polygon')
|
|
299
|
+
}
|
|
300
|
+
})
|
|
279
301
|
})
|
|
280
302
|
|
|
281
303
|
function cabinetRun(id: string, children: string[] = [], parentId: string | null = 'level_test') {
|
|
@@ -59,7 +59,11 @@ import {
|
|
|
59
59
|
useRoomRecords,
|
|
60
60
|
} from '../../../hooks/use-selected-room'
|
|
61
61
|
import { useCeilingEditSession } from '../../../lib/ceiling-edit-session'
|
|
62
|
-
import {
|
|
62
|
+
import {
|
|
63
|
+
AXIS_MOVE_DRAG_LABEL,
|
|
64
|
+
PATH_POINT_DRAG_LABEL,
|
|
65
|
+
ROTATE_HANDLE_DRAG_LABEL,
|
|
66
|
+
} from '../../../lib/contextual-help'
|
|
63
67
|
import {
|
|
64
68
|
canDirectRotateNode,
|
|
65
69
|
resolveDirectManipulationNode,
|
|
@@ -321,7 +325,7 @@ export function subscribeFloorplanAffordanceToolCancel(
|
|
|
321
325
|
// edits that set a direction/shape map to a scope; resize / rotate / body-move
|
|
322
326
|
// affordances return `null` (no polygon/wall snapping chip). Keyed off the
|
|
323
327
|
// affordance name the kinds register (`move-vertex` / `move-edge` / `add-vertex`
|
|
324
|
-
// / `curve` / `move-endpoint`).
|
|
328
|
+
// / `curve` / `move-endpoint` / `move-path-point` / `move-segment`).
|
|
325
329
|
export function floorplanAffordanceReshapeScope(
|
|
326
330
|
affordance: string,
|
|
327
331
|
nodeId: string,
|
|
@@ -362,6 +366,14 @@ export function floorplanAffordanceReshapeScope(
|
|
|
362
366
|
if (affordance.includes('rotate')) {
|
|
363
367
|
return { kind: 'handle-drag', nodeId, handle: ROTATE_HANDLE_DRAG_LABEL }
|
|
364
368
|
}
|
|
369
|
+
// MEP run point and run segment drags: the same handle-drag scopes as their
|
|
370
|
+
// 3D arrows, so they snap in the kind's own context (polygon for runs).
|
|
371
|
+
if (affordance === 'move-path-point') {
|
|
372
|
+
return { kind: 'handle-drag', nodeId, handle: PATH_POINT_DRAG_LABEL }
|
|
373
|
+
}
|
|
374
|
+
if (affordance === 'move-segment') {
|
|
375
|
+
return { kind: 'handle-drag', nodeId, handle: AXIS_MOVE_DRAG_LABEL }
|
|
376
|
+
}
|
|
365
377
|
return null
|
|
366
378
|
}
|
|
367
379
|
|