@pascal-app/editor 0.7.0 → 0.8.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 +6 -6
- package/src/components/editor/custom-camera-controls.tsx +2 -1
- package/src/components/editor/editor-layout-v2.tsx +4 -3
- package/src/components/editor/first-person/build-collider-world.ts +5 -7
- package/src/components/editor/first-person/bvh-ecctrl.tsx +119 -54
- package/src/components/editor/first-person-controls.tsx +11 -11
- package/src/components/editor/floating-action-menu.tsx +0 -0
- package/src/components/editor/floorplan-panel.tsx +44 -37
- package/src/components/editor/index.tsx +68 -53
- package/src/components/editor/selection-manager.tsx +2 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +465 -0
- package/src/components/editor/thumbnail-generator.tsx +18 -61
- package/src/components/editor/use-floorplan-background-placement.ts +3 -3
- package/src/components/editor/wall-measurement-label.tsx +0 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +5 -5
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +10 -12
- package/src/components/systems/roof/roof-edit-system.tsx +1 -1
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
- package/src/components/systems/zone/zone-system.tsx +0 -0
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +9 -2
- package/src/components/tools/fence/curve-fence-tool.tsx +4 -5
- package/src/components/tools/fence/fence-tool.tsx +2 -2
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +11 -8
- package/src/components/tools/fence/move-fence-tool.tsx +13 -9
- package/src/components/tools/item/move-tool.tsx +3 -6
- package/src/components/tools/item/placement-math.ts +2 -4
- package/src/components/tools/item/placement-strategies.ts +11 -10
- package/src/components/tools/item/use-draft-node.ts +0 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +9 -111
- package/src/components/tools/roof/move-roof-tool.tsx +7 -2
- package/src/components/tools/select/box-select-tool.tsx +12 -17
- package/src/components/tools/shared/segment-angle.ts +1 -1
- package/src/components/tools/tool-manager.tsx +12 -12
- package/src/components/tools/wall/curve-wall-tool.tsx +8 -6
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +11 -8
- package/src/components/tools/wall/move-wall-tool.tsx +6 -4
- package/src/components/tools/wall/wall-drafting.ts +0 -0
- package/src/components/tools/wall/wall-tool.tsx +3 -3
- package/src/components/tools/zone/zone-tool.tsx +20 -5
- package/src/components/ui/action-menu/camera-actions.tsx +0 -0
- package/src/components/ui/action-menu/control-modes.tsx +7 -1
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +35 -86
- package/src/components/ui/action-menu/view-toggles.tsx +19 -31
- package/src/components/ui/command-palette/editor-commands.tsx +6 -4
- package/src/components/ui/command-palette/index.tsx +4 -255
- package/src/components/ui/controls/material-picker.tsx +8 -5
- package/src/components/ui/floating-level-selector.tsx +1 -1
- package/src/components/ui/helpers/helper-manager.tsx +5 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1742 -315
- package/src/components/ui/item-catalog/item-catalog.tsx +88 -46
- package/src/components/ui/level-duplicate-dialog.tsx +3 -5
- package/src/components/ui/panels/ceiling-panel.tsx +2 -3
- package/src/components/ui/panels/column-panel.tsx +62 -18
- package/src/components/ui/panels/door-panel.tsx +272 -265
- package/src/components/ui/panels/fence-panel.tsx +0 -5
- package/src/components/ui/panels/paint-panel.tsx +66 -41
- package/src/components/ui/panels/panel-manager.tsx +3 -32
- package/src/components/ui/panels/reference-panel.tsx +28 -13
- package/src/components/ui/panels/roof-panel.tsx +52 -2
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -0
- package/src/components/ui/panels/slab-panel.tsx +0 -0
- package/src/components/ui/panels/spawn-panel.tsx +10 -4
- package/src/components/ui/panels/stair-panel.tsx +66 -14
- package/src/components/ui/panels/wall-panel.tsx +97 -1
- package/src/components/ui/panels/window-panel.tsx +13 -5
- package/src/components/ui/primitives/number-input.tsx +1 -1
- package/src/components/ui/primitives/sidebar.tsx +0 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
- package/src/components/ui/sidebar/icon-rail.tsx +0 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +330 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +0 -0
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +0 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +4 -6
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +0 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -7
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +1 -1
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/viewer-overlay.tsx +0 -0
- package/src/components/viewer-zone-system.tsx +0 -0
- package/src/hooks/use-auto-save.ts +14 -0
- package/src/hooks/use-keyboard.ts +10 -0
- package/src/index.tsx +8 -1
- package/src/lib/level-duplication.test.ts +0 -2
- package/src/lib/level-duplication.ts +1 -1
- package/src/lib/material-paint.ts +1 -1
- package/src/lib/roof-duplication.ts +1 -1
- package/src/lib/scene-bounds.ts +1 -1
- package/src/lib/scene.ts +0 -0
- package/src/lib/sfx-bus.ts +2 -0
- package/src/lib/sfx-player.ts +5 -5
- package/src/lib/stair-duplication.ts +2 -2
- package/src/store/use-editor.tsx +27 -59
- package/tsconfig.json +2 -1
- package/src/components/feedback-dialog.tsx +0 -265
- package/src/components/pascal-radio.tsx +0 -280
- package/src/components/preview-button.tsx +0 -16
- package/src/components/ui/viewer-toolbar.tsx +0 -436
|
@@ -4092,7 +4092,6 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({
|
|
|
4092
4092
|
d={path}
|
|
4093
4093
|
fill={isDeleteHovered ? palette.deleteFill : palette.slabFill}
|
|
4094
4094
|
fillRule="evenodd"
|
|
4095
|
-
opacity={slabFillOpacity}
|
|
4096
4095
|
onClick={
|
|
4097
4096
|
canSelectSlabs
|
|
4098
4097
|
? (event) => {
|
|
@@ -4111,9 +4110,10 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({
|
|
|
4111
4110
|
}
|
|
4112
4111
|
onPointerEnter={canSelectSlabs ? () => onSlabHoverChange(slab.id) : undefined}
|
|
4113
4112
|
onPointerLeave={canSelectSlabs ? () => onSlabHoverChange(null) : undefined}
|
|
4113
|
+
opacity={slabFillOpacity}
|
|
4114
4114
|
pointerEvents={canSelectSlabs ? undefined : 'none'}
|
|
4115
|
-
style={canSelectSlabs ? { cursor: EDITOR_CURSOR } : undefined}
|
|
4116
4115
|
stroke="none"
|
|
4116
|
+
style={canSelectSlabs ? { cursor: EDITOR_CURSOR } : undefined}
|
|
4117
4117
|
/>
|
|
4118
4118
|
{isSelected && !isDeleteHovered ? (
|
|
4119
4119
|
<path
|
|
@@ -4168,7 +4168,6 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({
|
|
|
4168
4168
|
d={path}
|
|
4169
4169
|
fill={isDeleteHovered ? palette.deleteFill : palette.ceilingFill}
|
|
4170
4170
|
fillRule="evenodd"
|
|
4171
|
-
opacity={ceilingFillOpacity}
|
|
4172
4171
|
onClick={
|
|
4173
4172
|
canSelectCeilings
|
|
4174
4173
|
? (event) => {
|
|
@@ -4189,9 +4188,10 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({
|
|
|
4189
4188
|
canSelectCeilings ? () => onCeilingHoverChange(ceiling.id) : undefined
|
|
4190
4189
|
}
|
|
4191
4190
|
onPointerLeave={canSelectCeilings ? () => onCeilingHoverChange(null) : undefined}
|
|
4191
|
+
opacity={ceilingFillOpacity}
|
|
4192
4192
|
pointerEvents={canSelectCeilings ? undefined : 'none'}
|
|
4193
|
-
style={canSelectCeilings ? { cursor: EDITOR_CURSOR } : undefined}
|
|
4194
4193
|
stroke="none"
|
|
4194
|
+
style={canSelectCeilings ? { cursor: EDITOR_CURSOR } : undefined}
|
|
4195
4195
|
/>
|
|
4196
4196
|
{isSelected && !isDeleteHovered ? (
|
|
4197
4197
|
<path
|
|
@@ -4298,8 +4298,8 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({
|
|
|
4298
4298
|
<polygon
|
|
4299
4299
|
fill={`url(#${wallSelectionHatchId})`}
|
|
4300
4300
|
opacity={1}
|
|
4301
|
-
points={points}
|
|
4302
4301
|
pointerEvents="none"
|
|
4302
|
+
points={points}
|
|
4303
4303
|
/>
|
|
4304
4304
|
) : null}
|
|
4305
4305
|
</g>
|
|
@@ -4427,8 +4427,8 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({
|
|
|
4427
4427
|
{canSelectGeometry && (
|
|
4428
4428
|
<polygon
|
|
4429
4429
|
fill="transparent"
|
|
4430
|
-
points={points}
|
|
4431
4430
|
pointerEvents="all"
|
|
4431
|
+
points={points}
|
|
4432
4432
|
stroke="transparent"
|
|
4433
4433
|
strokeWidth={FLOORPLAN_OPENING_HIT_STROKE_WIDTH}
|
|
4434
4434
|
vectorEffect="non-scaling-stroke"
|
|
@@ -4538,8 +4538,8 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({
|
|
|
4538
4538
|
{canSelectGeometry && (
|
|
4539
4539
|
<polygon
|
|
4540
4540
|
fill="transparent"
|
|
4541
|
-
points={points}
|
|
4542
4541
|
pointerEvents="all"
|
|
4542
|
+
points={points}
|
|
4543
4543
|
stroke="transparent"
|
|
4544
4544
|
strokeWidth={FLOORPLAN_OPENING_HIT_STROKE_WIDTH}
|
|
4545
4545
|
vectorEffect="non-scaling-stroke"
|
|
@@ -5218,8 +5218,8 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({
|
|
|
5218
5218
|
{canSelectGeometry && (
|
|
5219
5219
|
<polygon
|
|
5220
5220
|
fill="transparent"
|
|
5221
|
-
points={points}
|
|
5222
5221
|
pointerEvents="all"
|
|
5222
|
+
points={points}
|
|
5223
5223
|
stroke="transparent"
|
|
5224
5224
|
strokeWidth={FLOORPLAN_OPENING_HIT_STROKE_WIDTH}
|
|
5225
5225
|
vectorEffect="non-scaling-stroke"
|
|
@@ -5290,8 +5290,8 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({
|
|
|
5290
5290
|
d={foldingPath}
|
|
5291
5291
|
fill="none"
|
|
5292
5292
|
stroke={doorStroke}
|
|
5293
|
-
strokeLinejoin="round"
|
|
5294
5293
|
strokeLinecap="round"
|
|
5294
|
+
strokeLinejoin="round"
|
|
5295
5295
|
strokeWidth={isSelected || isSelectionHighlighted ? '1.8' : '1.25'}
|
|
5296
5296
|
vectorEffect="non-scaling-stroke"
|
|
5297
5297
|
/>
|
|
@@ -5963,6 +5963,12 @@ function FloorplanItemImage({
|
|
|
5963
5963
|
}) {
|
|
5964
5964
|
const resolvedUrl = useResolvedAssetUrl(url)
|
|
5965
5965
|
if (!resolvedUrl) return null
|
|
5966
|
+
// The PNG is captured with the modal's top-down camera (default up = +Y),
|
|
5967
|
+
// so its pixel-right is world +X and pixel-up is world -Z. The plan SVG
|
|
5968
|
+
// negates both axes (`toSvgX(v) = -v`, `toSvgY(v) = -v`), which together
|
|
5969
|
+
// are a 180° rotation — so the captured image lands upside-down /
|
|
5970
|
+
// mirrored when overlaid as-is. Bake that 180° into the image transform
|
|
5971
|
+
// here; the panel / modal previews use the PNG directly and stay correct.
|
|
5966
5972
|
const rotationDeg = (-rotation * 180) / Math.PI + 180
|
|
5967
5973
|
return (
|
|
5968
5974
|
<g
|
|
@@ -6168,6 +6174,7 @@ const FloorplanNodeLayer = memo(function FloorplanNodeLayer({
|
|
|
6168
6174
|
}
|
|
6169
6175
|
points={points}
|
|
6170
6176
|
stroke={stroke}
|
|
6177
|
+
strokeOpacity={1}
|
|
6171
6178
|
strokeWidth={
|
|
6172
6179
|
isSelectionActive ? FLOORPLAN_SELECTED_WALL_STROKE_WIDTH : FLOORPLAN_WALL_STROKE_WIDTH
|
|
6173
6180
|
}
|
|
@@ -6217,8 +6224,8 @@ const FloorplanNodeLayer = memo(function FloorplanNodeLayer({
|
|
|
6217
6224
|
<polygon
|
|
6218
6225
|
fill={`url(#${wallSelectionHatchId})`}
|
|
6219
6226
|
opacity={1}
|
|
6220
|
-
points={points}
|
|
6221
6227
|
pointerEvents="none"
|
|
6228
|
+
points={points}
|
|
6222
6229
|
/>
|
|
6223
6230
|
) : null}
|
|
6224
6231
|
{itemDimensionMeasurements.length > 0 ? (
|
|
@@ -6308,8 +6315,8 @@ const FloorplanNodeLayer = memo(function FloorplanNodeLayer({
|
|
|
6308
6315
|
<polygon
|
|
6309
6316
|
fill={fill}
|
|
6310
6317
|
fillOpacity={isDeleteHovered ? 0.82 : 0.92}
|
|
6311
|
-
points={FLOORPLAN_SPAWN_ARROW_POINTS}
|
|
6312
6318
|
pointerEvents="none"
|
|
6319
|
+
points={FLOORPLAN_SPAWN_ARROW_POINTS}
|
|
6313
6320
|
stroke={stroke}
|
|
6314
6321
|
strokeLinejoin="round"
|
|
6315
6322
|
strokeWidth={0.055}
|
|
@@ -7224,6 +7231,11 @@ const FloorplanPolygonHandleLayer = memo(function FloorplanPolygonHandleLayer({
|
|
|
7224
7231
|
y2={endSvg.y}
|
|
7225
7232
|
/>
|
|
7226
7233
|
<line
|
|
7234
|
+
onPointerDown={
|
|
7235
|
+
onEdgePointerDown
|
|
7236
|
+
? (event) => onEdgePointerDown(nodeId, edgeIndex, event)
|
|
7237
|
+
: undefined
|
|
7238
|
+
}
|
|
7227
7239
|
pointerEvents="stroke"
|
|
7228
7240
|
stroke="transparent"
|
|
7229
7241
|
strokeLinecap="round"
|
|
@@ -7234,11 +7246,6 @@ const FloorplanPolygonHandleLayer = memo(function FloorplanPolygonHandleLayer({
|
|
|
7234
7246
|
x2={endSvg.x}
|
|
7235
7247
|
y1={startSvg.y}
|
|
7236
7248
|
y2={endSvg.y}
|
|
7237
|
-
onPointerDown={
|
|
7238
|
-
onEdgePointerDown
|
|
7239
|
-
? (event) => onEdgePointerDown(nodeId, edgeIndex, event)
|
|
7240
|
-
: undefined
|
|
7241
|
-
}
|
|
7242
7249
|
/>
|
|
7243
7250
|
</g>
|
|
7244
7251
|
)
|
|
@@ -9838,7 +9845,7 @@ export function FloorplanPanel() {
|
|
|
9838
9845
|
zoneVertexDragState != null ||
|
|
9839
9846
|
isPolygonDraftBuildActive
|
|
9840
9847
|
|
|
9841
|
-
if (!hasUserAdjustedViewportRef.current
|
|
9848
|
+
if (!(hasUserAdjustedViewportRef.current || transientFloorplanFit)) {
|
|
9842
9849
|
setViewport((current) =>
|
|
9843
9850
|
floorplanViewportEquals(current, fittedViewport) ? current : fittedViewport,
|
|
9844
9851
|
)
|
|
@@ -10970,6 +10977,14 @@ export function FloorplanPanel() {
|
|
|
10970
10977
|
}
|
|
10971
10978
|
}, [isItemPlacementPreviewActive, scheduleMovingFloorplanNodeRefresh])
|
|
10972
10979
|
|
|
10980
|
+
useEffect(() => {
|
|
10981
|
+
if (!hasPendingItemMeshFootprints) {
|
|
10982
|
+
return
|
|
10983
|
+
}
|
|
10984
|
+
|
|
10985
|
+
scheduleMovingFloorplanNodeRefresh()
|
|
10986
|
+
}, [hasPendingItemMeshFootprints, scheduleMovingFloorplanNodeRefresh])
|
|
10987
|
+
|
|
10973
10988
|
// Subscribe to the live-transforms store so rotation/position changes that
|
|
10974
10989
|
// *don't* go through pointer events still refresh the floorplan — e.g. R/T
|
|
10975
10990
|
// keyboard rotation during placement updates `useLiveTransforms` but emits
|
|
@@ -10985,14 +11000,6 @@ export function FloorplanPanel() {
|
|
|
10985
11000
|
return unsubscribe
|
|
10986
11001
|
}, [isItemPlacementPreviewActive, scheduleMovingFloorplanNodeRefresh])
|
|
10987
11002
|
|
|
10988
|
-
useEffect(() => {
|
|
10989
|
-
if (!hasPendingItemMeshFootprints) {
|
|
10990
|
-
return
|
|
10991
|
-
}
|
|
10992
|
-
|
|
10993
|
-
scheduleMovingFloorplanNodeRefresh()
|
|
10994
|
-
}, [hasPendingItemMeshFootprints, scheduleMovingFloorplanNodeRefresh])
|
|
10995
|
-
|
|
10996
11003
|
useEffect(() => {
|
|
10997
11004
|
if (!(movingNode?.type === 'door' || movingNode?.type === 'window')) {
|
|
10998
11005
|
return
|
|
@@ -16132,17 +16139,13 @@ export function FloorplanPanel() {
|
|
|
16132
16139
|
onDuplicate: handleSelectedItemDuplicate,
|
|
16133
16140
|
onMove: handleSelectedItemMove,
|
|
16134
16141
|
}}
|
|
16142
|
+
offsetY={FLOORPLAN_ACTION_MENU_OFFSET_Y}
|
|
16135
16143
|
opening={{
|
|
16136
16144
|
position: selectedOpeningActionMenuPosition,
|
|
16137
16145
|
onDelete: handleSelectedOpeningDelete,
|
|
16138
16146
|
onDuplicate: handleSelectedOpeningDuplicate,
|
|
16139
16147
|
onMove: handleSelectedOpeningMove,
|
|
16140
16148
|
}}
|
|
16141
|
-
spawn={{
|
|
16142
|
-
position: selectedSpawnActionMenuPosition,
|
|
16143
|
-
onDelete: handleSelectedSpawnDelete,
|
|
16144
|
-
onMove: handleSelectedSpawnMove,
|
|
16145
|
-
}}
|
|
16146
16149
|
roof={{
|
|
16147
16150
|
position: selectedRoofActionMenuPosition,
|
|
16148
16151
|
onDelete: handleSelectedRoofDelete,
|
|
@@ -16157,6 +16160,11 @@ export function FloorplanPanel() {
|
|
|
16157
16160
|
: handleSelectedSlabDelete,
|
|
16158
16161
|
onMove: selectedSlabEditingHole ? handleSelectedSlabHoleMove : handleSelectedSlabMove,
|
|
16159
16162
|
}}
|
|
16163
|
+
spawn={{
|
|
16164
|
+
position: selectedSpawnActionMenuPosition,
|
|
16165
|
+
onDelete: handleSelectedSpawnDelete,
|
|
16166
|
+
onMove: handleSelectedSpawnMove,
|
|
16167
|
+
}}
|
|
16160
16168
|
stair={{
|
|
16161
16169
|
position: selectedStairActionMenuPosition,
|
|
16162
16170
|
onDelete: handleSelectedStairDelete,
|
|
@@ -16168,7 +16176,6 @@ export function FloorplanPanel() {
|
|
|
16168
16176
|
onDelete: handleSelectedWallDelete,
|
|
16169
16177
|
onMove: handleSelectedWallMove,
|
|
16170
16178
|
}}
|
|
16171
|
-
offsetY={FLOORPLAN_ACTION_MENU_OFFSET_Y}
|
|
16172
16179
|
/>
|
|
16173
16180
|
|
|
16174
16181
|
{referenceScaleDraft && (
|
|
@@ -16201,7 +16208,7 @@ export function FloorplanPanel() {
|
|
|
16201
16208
|
</div>
|
|
16202
16209
|
|
|
16203
16210
|
<div className="mb-3 rounded-xl border border-border/70 bg-white/5 px-3 py-2">
|
|
16204
|
-
<div className="text-muted-foreground
|
|
16211
|
+
<div className="text-[11px] text-muted-foreground uppercase tracking-wide">
|
|
16205
16212
|
Drawn line
|
|
16206
16213
|
</div>
|
|
16207
16214
|
<div className="mt-1 font-medium text-sm">
|
|
@@ -16363,8 +16370,8 @@ export function FloorplanPanel() {
|
|
|
16363
16370
|
<FloorplanGuideLayer
|
|
16364
16371
|
activeGuideInteractionGuideId={activeGuideInteractionGuideId}
|
|
16365
16372
|
activeGuideInteractionMode={activeGuideInteractionMode}
|
|
16366
|
-
guideUi={guideUi}
|
|
16367
16373
|
guides={displayGuides}
|
|
16374
|
+
guideUi={guideUi}
|
|
16368
16375
|
isInteractive={canInteractWithGuides}
|
|
16369
16376
|
onGuideSelect={handleGuideSelect}
|
|
16370
16377
|
onGuideTranslateStart={handleGuideTranslateStart}
|
|
@@ -16385,6 +16392,8 @@ export function FloorplanPanel() {
|
|
|
16385
16392
|
hoveredSlabId={hoveredSlabId}
|
|
16386
16393
|
hoveredWallId={hoveredWallId}
|
|
16387
16394
|
isDeleteMode={isDeleteMode}
|
|
16395
|
+
isGuideTraceVisible={isGuideTraceVisible}
|
|
16396
|
+
metersPerUnit={calibratedMetersPerUnit}
|
|
16388
16397
|
onCeilingDoubleClick={handleCeilingDoubleClick}
|
|
16389
16398
|
onCeilingHoverChange={handleCeilingHoverChange}
|
|
16390
16399
|
onCeilingSelect={handleCeilingSelect}
|
|
@@ -16401,11 +16410,9 @@ export function FloorplanPanel() {
|
|
|
16401
16410
|
openingsPolygons={openingsPolygons}
|
|
16402
16411
|
palette={palette}
|
|
16403
16412
|
selectedIdSet={selectedIdSet}
|
|
16404
|
-
slabSelectionHatchId={slabSelectionHatchId}
|
|
16405
16413
|
slabPolygons={displaySlabPolygons}
|
|
16414
|
+
slabSelectionHatchId={slabSelectionHatchId}
|
|
16406
16415
|
unit={unit}
|
|
16407
|
-
metersPerUnit={calibratedMetersPerUnit}
|
|
16408
|
-
isGuideTraceVisible={isGuideTraceVisible}
|
|
16409
16416
|
wallPolygons={displayWallPolygons}
|
|
16410
16417
|
wallSelectionHatchId={wallSelectionHatchId}
|
|
16411
16418
|
/>
|
|
@@ -16483,8 +16490,8 @@ export function FloorplanPanel() {
|
|
|
16483
16490
|
|
|
16484
16491
|
<FloorplanReferenceScaleLayer
|
|
16485
16492
|
draft={referenceScaleDraft}
|
|
16486
|
-
guideUi={guideUi}
|
|
16487
16493
|
guides={displayGuides}
|
|
16494
|
+
guideUi={guideUi}
|
|
16488
16495
|
palette={palette}
|
|
16489
16496
|
unit={unit}
|
|
16490
16497
|
unitsPerPixel={floorplanUnitsPerPixel}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import { ViewerOverlay } from '../../components/viewer-overlay'
|
|
21
21
|
import { ViewerZoneSystem } from '../../components/viewer-zone-system'
|
|
22
22
|
import { type PresetsAdapter, PresetsProvider } from '../../contexts/presets-context'
|
|
23
|
+
import { useAutoFrame } from '../../hooks/use-auto-frame'
|
|
23
24
|
import { type SaveStatus, useAutoSave } from '../../hooks/use-auto-save'
|
|
24
25
|
import { useKeyboard } from '../../hooks/use-keyboard'
|
|
25
26
|
import {
|
|
@@ -64,6 +65,7 @@ import { Grid } from './grid'
|
|
|
64
65
|
import { PresetThumbnailGenerator } from './preset-thumbnail-generator'
|
|
65
66
|
import { SelectionManager } from './selection-manager'
|
|
66
67
|
import { SiteEdgeLabels } from './site-edge-labels'
|
|
68
|
+
import { SnapshotCaptureOverlay } from './snapshot-capture-overlay'
|
|
67
69
|
import { type SnapshotCameraData, ThumbnailGenerator } from './thumbnail-generator'
|
|
68
70
|
import { WallMeasurementLabel } from './wall-measurement-label'
|
|
69
71
|
|
|
@@ -76,12 +78,12 @@ const PAINT_CURSOR_BADGE_DISABLED_COLOR = '#94a3b8'
|
|
|
76
78
|
const PAINT_CURSOR_BADGE_OFFSET_X = 14
|
|
77
79
|
const PAINT_CURSOR_BADGE_OFFSET_Y = 14
|
|
78
80
|
const EDITOR_HOVER_STYLES: HoverStyles = {
|
|
79
|
-
default: { visibleColor:
|
|
80
|
-
delete: { visibleColor:
|
|
81
|
-
'paint-ready': { visibleColor:
|
|
81
|
+
default: { visibleColor: 0x00_aa_ff, hiddenColor: 0xf3_ff_47, strength: 5, pulse: true },
|
|
82
|
+
delete: { visibleColor: 0xef_44_44, hiddenColor: 0x99_1b_1b, strength: 6, pulse: false },
|
|
83
|
+
'paint-ready': { visibleColor: 0xf5_9e_0b, hiddenColor: 0xfd_e0_68, strength: 5, pulse: true },
|
|
82
84
|
'paint-disabled': {
|
|
83
|
-
visibleColor:
|
|
84
|
-
hiddenColor:
|
|
85
|
+
visibleColor: 0x94_a3_b8,
|
|
86
|
+
hiddenColor: 0x47_55_69,
|
|
85
87
|
strength: 4,
|
|
86
88
|
pulse: false,
|
|
87
89
|
},
|
|
@@ -462,7 +464,7 @@ function ViewerCanvasControlsHint({
|
|
|
462
464
|
<div className="pointer-events-none absolute top-14 left-1/2 z-40 max-w-[calc(100%-2rem)] -translate-x-1/2">
|
|
463
465
|
<section
|
|
464
466
|
aria-label="Camera controls hint"
|
|
465
|
-
className="pointer-events-auto flex items-start gap-3 rounded-2xl border border-border/35 bg-background/90 px-3.5 py-2.5 shadow-
|
|
467
|
+
className="pointer-events-auto flex items-start gap-3 rounded-2xl border border-border/35 bg-background/90 px-3.5 py-2.5 shadow-elevation-4 backdrop-blur-xl"
|
|
466
468
|
>
|
|
467
469
|
<div className="grid min-w-0 flex-1 grid-cols-3 items-start divide-x divide-border/18">
|
|
468
470
|
{hints.map((hint) => (
|
|
@@ -753,7 +755,7 @@ function PaintCursorLayer({
|
|
|
753
755
|
(activePaintMaterial.material !== undefined ||
|
|
754
756
|
activePaintMaterial.materialPreset !== undefined),
|
|
755
757
|
)
|
|
756
|
-
const label =
|
|
758
|
+
const label = hasMaterial ? `Paint ${activePaintTarget}` : 'Choose material'
|
|
757
759
|
const icon = 'mdi:format-color-fill'
|
|
758
760
|
|
|
759
761
|
useLayoutEffect(() => {
|
|
@@ -786,16 +788,16 @@ function PaintCursorLayer({
|
|
|
786
788
|
const ViewerCanvas = memo(function ViewerCanvas({
|
|
787
789
|
isVersionPreviewMode,
|
|
788
790
|
isLoading,
|
|
791
|
+
isFirstPersonMode,
|
|
789
792
|
hasLoadedInitialScene,
|
|
790
793
|
showLoader,
|
|
791
|
-
isFirstPersonMode,
|
|
792
794
|
onThumbnailCapture,
|
|
793
795
|
}: {
|
|
794
796
|
isVersionPreviewMode: boolean
|
|
795
797
|
isLoading: boolean
|
|
798
|
+
isFirstPersonMode: boolean
|
|
796
799
|
hasLoadedInitialScene: boolean
|
|
797
800
|
showLoader: boolean
|
|
798
|
-
isFirstPersonMode: boolean
|
|
799
801
|
onThumbnailCapture?: (blob: Blob, cameraData: SnapshotCameraData) => void
|
|
800
802
|
}) {
|
|
801
803
|
const viewMode = useEditor((s) => s.viewMode)
|
|
@@ -837,7 +839,7 @@ const ViewerCanvas = memo(function ViewerCanvas({
|
|
|
837
839
|
window.removeEventListener('pointermove', handlePointerMove)
|
|
838
840
|
window.removeEventListener('pointerup', handlePointerUp)
|
|
839
841
|
}
|
|
840
|
-
}, [
|
|
842
|
+
}, [])
|
|
841
843
|
|
|
842
844
|
useEffect(() => {
|
|
843
845
|
setIsCameraControlsHintVisible(!readCameraControlsHintDismissed())
|
|
@@ -940,7 +942,9 @@ export default function Editor({
|
|
|
940
942
|
commandPaletteEmptyAction,
|
|
941
943
|
}: EditorProps) {
|
|
942
944
|
const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
|
|
945
|
+
|
|
943
946
|
useKeyboard({ isVersionPreviewMode, disabled: isFirstPersonMode })
|
|
947
|
+
|
|
944
948
|
const { isLoadingSceneRef } = useAutoSave({
|
|
945
949
|
onSave,
|
|
946
950
|
onDirty,
|
|
@@ -951,8 +955,7 @@ export default function Editor({
|
|
|
951
955
|
const [isSceneLoading, setIsSceneLoading] = useState(false)
|
|
952
956
|
const [hasLoadedInitialScene, setHasLoadedInitialScene] = useState(false)
|
|
953
957
|
const isPreviewMode = useEditor((s) => s.isPreviewMode)
|
|
954
|
-
const
|
|
955
|
-
const wasFirstPersonModeRef = useRef(isFirstPersonMode)
|
|
958
|
+
const isCaptureMode = useEditor((s) => s.isCaptureMode)
|
|
956
959
|
|
|
957
960
|
const sidebarWidth = useSidebarStore((s) => s.width)
|
|
958
961
|
const isSidebarCollapsed = useSidebarStore((s) => s.isCollapsed)
|
|
@@ -970,39 +973,6 @@ export default function Editor({
|
|
|
970
973
|
}
|
|
971
974
|
}, [projectId])
|
|
972
975
|
|
|
973
|
-
useEffect(() => {
|
|
974
|
-
const wasFirstPersonMode = wasFirstPersonModeRef.current
|
|
975
|
-
wasFirstPersonModeRef.current = isFirstPersonMode
|
|
976
|
-
|
|
977
|
-
if (isFirstPersonMode && !wasFirstPersonMode) {
|
|
978
|
-
const viewer = useViewer.getState()
|
|
979
|
-
firstPersonPreviousLevelRef.current = viewer.selection.levelId
|
|
980
|
-
viewer.setCameraMode('perspective')
|
|
981
|
-
viewer.setWallMode('up')
|
|
982
|
-
viewer.setWalkthroughMode(true)
|
|
983
|
-
viewer.setSelection({ selectedIds: [], zoneId: null })
|
|
984
|
-
return
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
if (!(wasFirstPersonMode && !isFirstPersonMode)) return
|
|
988
|
-
|
|
989
|
-
const viewer = useViewer.getState()
|
|
990
|
-
const previousLevelId = firstPersonPreviousLevelRef.current
|
|
991
|
-
firstPersonPreviousLevelRef.current = null
|
|
992
|
-
viewer.setWalkthroughMode(false)
|
|
993
|
-
|
|
994
|
-
if (!previousLevelId) return
|
|
995
|
-
|
|
996
|
-
const previousLevelNode = useScene.getState().nodes[previousLevelId]
|
|
997
|
-
if (previousLevelNode?.type === 'level') {
|
|
998
|
-
viewer.setSelection({
|
|
999
|
-
levelId: previousLevelId,
|
|
1000
|
-
zoneId: null,
|
|
1001
|
-
selectedIds: [],
|
|
1002
|
-
})
|
|
1003
|
-
}
|
|
1004
|
-
}, [isFirstPersonMode])
|
|
1005
|
-
|
|
1006
976
|
// Load scene on mount (or when onLoad identity changes, e.g. project switch)
|
|
1007
977
|
useEffect(() => {
|
|
1008
978
|
let cancelled = false
|
|
@@ -1064,6 +1034,42 @@ export default function Editor({
|
|
|
1064
1034
|
|
|
1065
1035
|
const showLoader = isLoading || isSceneLoading
|
|
1066
1036
|
|
|
1037
|
+
const firstPersonPreviousLevelRef = useRef(useViewer.getState().selection.levelId)
|
|
1038
|
+
const wasFirstPersonModeRef = useRef(isFirstPersonMode)
|
|
1039
|
+
|
|
1040
|
+
useEffect(() => {
|
|
1041
|
+
const wasFirstPersonMode = wasFirstPersonModeRef.current
|
|
1042
|
+
wasFirstPersonModeRef.current = isFirstPersonMode
|
|
1043
|
+
|
|
1044
|
+
if (isFirstPersonMode && !wasFirstPersonMode) {
|
|
1045
|
+
const viewer = useViewer.getState()
|
|
1046
|
+
firstPersonPreviousLevelRef.current = viewer.selection.levelId
|
|
1047
|
+
viewer.setCameraMode('perspective')
|
|
1048
|
+
viewer.setWallMode('up')
|
|
1049
|
+
viewer.setWalkthroughMode(true)
|
|
1050
|
+
viewer.setSelection({ selectedIds: [], zoneId: null })
|
|
1051
|
+
return
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
if (!(wasFirstPersonMode && !isFirstPersonMode)) return
|
|
1055
|
+
|
|
1056
|
+
const viewer = useViewer.getState()
|
|
1057
|
+
const previousLevelId = firstPersonPreviousLevelRef.current
|
|
1058
|
+
firstPersonPreviousLevelRef.current = null
|
|
1059
|
+
viewer.setWalkthroughMode(false)
|
|
1060
|
+
|
|
1061
|
+
if (!previousLevelId) return
|
|
1062
|
+
|
|
1063
|
+
const previousLevelNode = useScene.getState().nodes[previousLevelId]
|
|
1064
|
+
if (previousLevelNode?.type === 'level') {
|
|
1065
|
+
viewer.setSelection({
|
|
1066
|
+
levelId: previousLevelId,
|
|
1067
|
+
zoneId: null,
|
|
1068
|
+
selectedIds: [],
|
|
1069
|
+
})
|
|
1070
|
+
}
|
|
1071
|
+
}, [isFirstPersonMode])
|
|
1072
|
+
|
|
1067
1073
|
const previewViewerContent = (
|
|
1068
1074
|
<Viewer hoverStyles={EDITOR_HOVER_STYLES} selectionManager="default">
|
|
1069
1075
|
<ExportManager />
|
|
@@ -1135,21 +1141,24 @@ export default function Editor({
|
|
|
1135
1141
|
navbarSlot={navbarSlot}
|
|
1136
1142
|
overlays={
|
|
1137
1143
|
<>
|
|
1138
|
-
<FloatingLevelSelector />
|
|
1139
|
-
{!isVersionPreviewMode && (
|
|
1144
|
+
{!isCaptureMode && <FloatingLevelSelector />}
|
|
1145
|
+
{!(isVersionPreviewMode || isCaptureMode) && (
|
|
1140
1146
|
<div className="pointer-events-auto">
|
|
1141
1147
|
<ActionMenu />
|
|
1142
1148
|
</div>
|
|
1143
1149
|
)}
|
|
1144
|
-
{!isVersionPreviewMode && (
|
|
1150
|
+
{!(isVersionPreviewMode || isCaptureMode) && (
|
|
1145
1151
|
<div className="pointer-events-auto">
|
|
1146
1152
|
<PanelManager />
|
|
1147
1153
|
</div>
|
|
1148
1154
|
)}
|
|
1149
|
-
|
|
1150
|
-
<
|
|
1151
|
-
|
|
1155
|
+
{!isCaptureMode && (
|
|
1156
|
+
<div className="pointer-events-auto">
|
|
1157
|
+
<HelperManager />
|
|
1158
|
+
</div>
|
|
1159
|
+
)}
|
|
1152
1160
|
{viewerBanner}
|
|
1161
|
+
{projectId ? <SnapshotCaptureOverlay projectId={projectId} /> : null}
|
|
1153
1162
|
</>
|
|
1154
1163
|
}
|
|
1155
1164
|
renderTabContent={renderTabContent}
|
|
@@ -1159,14 +1168,14 @@ export default function Editor({
|
|
|
1159
1168
|
viewerToolbarLeft={viewerToolbarLeft}
|
|
1160
1169
|
viewerToolbarRight={viewerToolbarRight}
|
|
1161
1170
|
/>
|
|
1171
|
+
<EditorCommands />
|
|
1172
|
+
<CommandPalette emptyAction={commandPaletteEmptyAction} />
|
|
1162
1173
|
{/* First-person overlay — rendered on top of normal layout */}
|
|
1163
1174
|
{isFirstPersonMode && (
|
|
1164
1175
|
<div className="pointer-events-none fixed inset-0 z-50">
|
|
1165
1176
|
<FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
|
|
1166
1177
|
</div>
|
|
1167
1178
|
)}
|
|
1168
|
-
<EditorCommands />
|
|
1169
|
-
<CommandPalette emptyAction={commandPaletteEmptyAction} />
|
|
1170
1179
|
</>
|
|
1171
1180
|
)}
|
|
1172
1181
|
</PresetsProvider>
|
|
@@ -1222,6 +1231,12 @@ export default function Editor({
|
|
|
1222
1231
|
<HelperManager />
|
|
1223
1232
|
</div>
|
|
1224
1233
|
</ViewerOverlays>
|
|
1234
|
+
{/* First-person overlay — rendered on top of normal layout */}
|
|
1235
|
+
{isFirstPersonMode && (
|
|
1236
|
+
<div className="pointer-events-none fixed inset-0 z-50">
|
|
1237
|
+
<FirstPersonOverlay onExit={() => useEditor.getState().setFirstPersonMode(false)} />
|
|
1238
|
+
</div>
|
|
1239
|
+
)}
|
|
1225
1240
|
</>
|
|
1226
1241
|
)}
|
|
1227
1242
|
</div>
|
|
@@ -341,7 +341,7 @@ function applySingleSurfacePaintPreview(
|
|
|
341
341
|
if (node.type === 'ceiling') {
|
|
342
342
|
const root = getRegisteredMesh(node.id)
|
|
343
343
|
const overlay = root?.getObjectByName('ceiling-grid') as Mesh | undefined
|
|
344
|
-
if (!root
|
|
344
|
+
if (!(root && overlay)) return null
|
|
345
345
|
|
|
346
346
|
const previewColor =
|
|
347
347
|
getMaterialPresetByRef(material.materialPreset)?.mapProperties.color ??
|
|
@@ -999,7 +999,6 @@ export const SelectionManager = () => {
|
|
|
999
999
|
'roof-segment',
|
|
1000
1000
|
'stair',
|
|
1001
1001
|
'stair-segment',
|
|
1002
|
-
'spawn',
|
|
1003
1002
|
'window',
|
|
1004
1003
|
'door',
|
|
1005
1004
|
'zone',
|
|
@@ -1392,6 +1391,7 @@ export const SelectionManager = () => {
|
|
|
1392
1391
|
'roof-segment',
|
|
1393
1392
|
'stair',
|
|
1394
1393
|
'stair-segment',
|
|
1394
|
+
'spawn',
|
|
1395
1395
|
'window',
|
|
1396
1396
|
'door',
|
|
1397
1397
|
'zone',
|