editor: tool-defaults seeding + drawTool capability (fence presets) (#346)
* feat(editor): tool-defaults seeding + drawTool capability; fence consumes it Adds a generic, transient `useEditor.toolDefaults` slice keyed by tool, set via `setToolDefaults(tool, params)`. A draw tool's create path merges its entry when minting a node and clears it on deactivation, so a host app can prime the next-drawn node's parameters — placing a saved preset of a drawn kind, or a future "small / medium / large" dimension picker for wall / slab / ceiling. Marks the kind with `capabilities.drawTool` (helper `isDrawnViaTool`) so host apps know to route placement through `setToolDefaults(type) + setTool(type)` instead of cloning a finished instance. Wires fence end-to-end: it declares `drawTool: true`, its create path merges `toolDefaults.fence`, and the draft preview (bar geometry, cursor, HUD label heights) reflects the seeded height/thickness so the ghost matches what will be built. The tool clears its own defaults on unmount. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(editor): restore dropped useEditor import in fence tool The toolDefaults-seeding commit lost the `useEditor` import (formatter stripped it), shipping a runtime ReferenceError when FenceTool mounts. Re-add it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
a144502c04
commit
b8d94a6436
@@ -17,6 +17,8 @@ export {
|
|||||||
discoverPlugins,
|
discoverPlugins,
|
||||||
getHostRefFields,
|
getHostRefFields,
|
||||||
getSelectableKinds,
|
getSelectableKinds,
|
||||||
|
isDrawnViaTool,
|
||||||
|
isDrawnViaToolKind,
|
||||||
isPresettable,
|
isPresettable,
|
||||||
isPresettableKind,
|
isPresettableKind,
|
||||||
isRegistryMovable,
|
isRegistryMovable,
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { beforeEach, describe, expect, test } from 'bun:test'
|
|||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import {
|
import {
|
||||||
getHostRefFields,
|
getHostRefFields,
|
||||||
|
isDrawnViaTool,
|
||||||
|
isDrawnViaToolKind,
|
||||||
isPresettable,
|
isPresettable,
|
||||||
isPresettableKind,
|
isPresettableKind,
|
||||||
loadPlugin,
|
loadPlugin,
|
||||||
@@ -124,6 +126,28 @@ describe('getHostRefFields', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('isDrawnViaTool', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
nodeRegistry._reset()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('true when capability set', () => {
|
||||||
|
const def = makeDefinition('fence', { capabilities: { drawTool: true } })
|
||||||
|
expect(isDrawnViaTool(def)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('false when unset or not exactly true', () => {
|
||||||
|
expect(isDrawnViaTool(makeDefinition('column'))).toBe(false)
|
||||||
|
expect(isDrawnViaTool(makeDefinition('off', { capabilities: { drawTool: false } }))).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isDrawnViaToolKind looks up the registry', () => {
|
||||||
|
registerNode(makeDefinition('fence', { capabilities: { drawTool: true } }))
|
||||||
|
expect(isDrawnViaToolKind('fence')).toBe(true)
|
||||||
|
expect(isDrawnViaToolKind('unknown')).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('loadPlugin', () => {
|
describe('loadPlugin', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
nodeRegistry._reset()
|
nodeRegistry._reset()
|
||||||
|
|||||||
@@ -174,6 +174,22 @@ export function getHostRefFields(def: AnyNodeDefinition): ReadonlyArray<string>
|
|||||||
return def.capabilities.hostRefFields ?? []
|
return def.capabilities.hostRefFields ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether instances of this kind are created by drawing with a build tool
|
||||||
|
* (tool id === node `type`) rather than dropping a finished instance. Read
|
||||||
|
* by host apps to route preset placement of such kinds through
|
||||||
|
* `setToolDefaults(type, params)` + `setTool(type)` — see
|
||||||
|
* `def.capabilities.drawTool` docs.
|
||||||
|
*/
|
||||||
|
export function isDrawnViaTool(def: AnyNodeDefinition): boolean {
|
||||||
|
return def.capabilities.drawTool === true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDrawnViaToolKind(kind: string): boolean {
|
||||||
|
const def = nodeRegistry.get(kind)
|
||||||
|
return def ? isDrawnViaTool(def) : false
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadPlugin(plugin: Plugin): Promise<void> {
|
export async function loadPlugin(plugin: Plugin): Promise<void> {
|
||||||
if (plugin.apiVersion !== HOST_API_VERSION) {
|
if (plugin.apiVersion !== HOST_API_VERSION) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -1008,6 +1008,20 @@ export type Capabilities = {
|
|||||||
* are non-leaf scene containers.
|
* are non-leaf scene containers.
|
||||||
*/
|
*/
|
||||||
presettable?: boolean
|
presettable?: boolean
|
||||||
|
/**
|
||||||
|
* Instances of this kind are created by operating a build tool and
|
||||||
|
* drawing on the grid (clicking points), rather than dropping a
|
||||||
|
* finished instance. The tool id equals the node `type`. Host apps may
|
||||||
|
* seed the tool's starting parameters via
|
||||||
|
* `useEditor.setToolDefaults(type, params)` before activating it — the
|
||||||
|
* tool's create path merges those defaults when minting the node and
|
||||||
|
* clears its own entry on deactivation. Used so placing a saved preset
|
||||||
|
* of a drawn kind contributes its build parameters (a fence's
|
||||||
|
* height / style / post spacing) while the user draws the fresh span,
|
||||||
|
* and so a future "small / medium / large" picker can prime the same
|
||||||
|
* tool. Read via the `isDrawnViaTool(def)` helper. Default `false`.
|
||||||
|
*/
|
||||||
|
drawTool?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import {
|
|||||||
} from '@pascal-app/core'
|
} from '@pascal-app/core'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { sfxEmitter } from '../../../lib/sfx-bus'
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
||||||
|
import useEditor from '../../../store/use-editor'
|
||||||
import {
|
import {
|
||||||
findWallSnapTarget,
|
findWallSnapTarget,
|
||||||
getWallAngleSnapStep,
|
|
||||||
getSegmentGridStep,
|
getSegmentGridStep,
|
||||||
|
getWallAngleSnapStep,
|
||||||
isSegmentLongEnough,
|
isSegmentLongEnough,
|
||||||
snapPointTo45Degrees,
|
snapPointTo45Degrees,
|
||||||
snapPointToGrid,
|
snapPointToGrid,
|
||||||
@@ -158,7 +159,12 @@ export function createFenceOnCurrentLevel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fenceCount = Object.values(nodes).filter((node) => node.type === 'fence').length
|
const fenceCount = Object.values(nodes).filter((node) => node.type === 'fence').length
|
||||||
|
// Build parameters seeded by a placed preset (height, style, post
|
||||||
|
// spacing, …) merge in first; `name`/`start`/`end` always win. The
|
||||||
|
// schema parse validates and drops anything unexpected.
|
||||||
|
const defaults = useEditor.getState().toolDefaults.fence ?? {}
|
||||||
const fence = FenceNode.parse({
|
const fence = FenceNode.parse({
|
||||||
|
...defaults,
|
||||||
name: `Fence ${fenceCount + 1}`,
|
name: `Fence ${fenceCount + 1}`,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ export type {
|
|||||||
MovingFenceEndpoint,
|
MovingFenceEndpoint,
|
||||||
MovingWallEndpoint,
|
MovingWallEndpoint,
|
||||||
SplitOrientation,
|
SplitOrientation,
|
||||||
|
ToolDefaults,
|
||||||
ViewMode,
|
ViewMode,
|
||||||
} from './store/use-editor'
|
} from './store/use-editor'
|
||||||
export { default as useEditor } from './store/use-editor'
|
export { default as useEditor } from './store/use-editor'
|
||||||
|
|||||||
@@ -115,6 +115,14 @@ export type GridSnapStep = 0.5 | 0.25 | 0.1 | 0.05
|
|||||||
// Combined tool type
|
// Combined tool type
|
||||||
export type Tool = SiteTool | StructureTool | FurnishTool
|
export type Tool = SiteTool | StructureTool | FurnishTool
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starting parameters seeded into a draw tool before it mints a node.
|
||||||
|
* A loose param bag — the tool's create path validates it through the
|
||||||
|
* kind's schema (`FenceNode.parse({ ...defaults, start, end })`), which
|
||||||
|
* is the real type gate, so unknown keys are simply ignored.
|
||||||
|
*/
|
||||||
|
export type ToolDefaults = Record<string, unknown>
|
||||||
|
|
||||||
export type MovingWallEndpoint = {
|
export type MovingWallEndpoint = {
|
||||||
wall: WallNode
|
wall: WallNode
|
||||||
endpoint: 'start' | 'end'
|
endpoint: 'start' | 'end'
|
||||||
@@ -156,6 +164,15 @@ type EditorState = {
|
|||||||
setMode: (mode: Mode) => void
|
setMode: (mode: Mode) => void
|
||||||
tool: Tool | null
|
tool: Tool | null
|
||||||
setTool: (tool: Tool | null) => void
|
setTool: (tool: Tool | null) => void
|
||||||
|
/**
|
||||||
|
* Per-tool starting parameters for the next node a draw tool mints.
|
||||||
|
* Transient (not persisted): host apps seed an entry just before
|
||||||
|
* activating the tool (placing a drawn preset, or a future dimension
|
||||||
|
* picker), the tool's create path merges it, and the tool clears its
|
||||||
|
* own entry on deactivation so a later manual draw isn't poisoned.
|
||||||
|
*/
|
||||||
|
toolDefaults: Partial<Record<Tool, ToolDefaults>>
|
||||||
|
setToolDefaults: (tool: Tool, defaults: ToolDefaults | null) => void
|
||||||
structureLayer: StructureLayer
|
structureLayer: StructureLayer
|
||||||
setStructureLayer: (layer: StructureLayer) => void
|
setStructureLayer: (layer: StructureLayer) => void
|
||||||
catalogCategory: CatalogCategory | null
|
catalogCategory: CatalogCategory | null
|
||||||
@@ -610,6 +627,17 @@ const useEditor = create<EditorState>()(
|
|||||||
},
|
},
|
||||||
tool: DEFAULT_PERSISTED_EDITOR_UI_STATE.tool,
|
tool: DEFAULT_PERSISTED_EDITOR_UI_STATE.tool,
|
||||||
setTool: (tool) => set({ tool }),
|
setTool: (tool) => set({ tool }),
|
||||||
|
toolDefaults: {},
|
||||||
|
setToolDefaults: (tool, defaults) =>
|
||||||
|
set((state) => {
|
||||||
|
const next = { ...state.toolDefaults }
|
||||||
|
if (defaults === null) {
|
||||||
|
delete next[tool]
|
||||||
|
} else {
|
||||||
|
next[tool] = defaults
|
||||||
|
}
|
||||||
|
return { toolDefaults: next }
|
||||||
|
}),
|
||||||
structureLayer: DEFAULT_PERSISTED_EDITOR_UI_STATE.structureLayer,
|
structureLayer: DEFAULT_PERSISTED_EDITOR_UI_STATE.structureLayer,
|
||||||
setStructureLayer: (layer) => {
|
setStructureLayer: (layer) => {
|
||||||
const { mode } = get()
|
const { mode } = get()
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import {
|
import type { FenceNode as FenceNodeType, HandleDescriptor, NodeDefinition } from '@pascal-app/core'
|
||||||
type FenceNode as FenceNodeType,
|
|
||||||
type HandleDescriptor,
|
|
||||||
type NodeDefinition,
|
|
||||||
} from '@pascal-app/core'
|
|
||||||
import { buildFenceFloorplan } from './floorplan'
|
import { buildFenceFloorplan } from './floorplan'
|
||||||
import { fenceCurveAffordance, fenceMoveEndpointAffordance } from './floorplan-affordances'
|
import { fenceCurveAffordance, fenceMoveEndpointAffordance } from './floorplan-affordances'
|
||||||
import { fenceFloorplanMoveTarget } from './floorplan-move'
|
import { fenceFloorplanMoveTarget } from './floorplan-move'
|
||||||
@@ -164,6 +160,10 @@ export const fenceDefinition: NodeDefinition<typeof FenceNode> = {
|
|||||||
surfaces: { sides: { faces: 'all' } },
|
surfaces: { sides: { faces: 'all' } },
|
||||||
duplicable: true,
|
duplicable: true,
|
||||||
deletable: true,
|
deletable: true,
|
||||||
|
// Placed by drawing the span with the two-click tool; a saved preset
|
||||||
|
// seeds its build parameters via `toolDefaults.fence` (see `tool.tsx`
|
||||||
|
// and `createFenceOnCurrentLevel`).
|
||||||
|
drawTool: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
relations: {
|
relations: {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
type SegmentAngleReference,
|
type SegmentAngleReference,
|
||||||
snapFenceDraftPoint,
|
snapFenceDraftPoint,
|
||||||
triggerSFX,
|
triggerSFX,
|
||||||
|
useEditor,
|
||||||
WALL_FINE_GRID_STEP,
|
WALL_FINE_GRID_STEP,
|
||||||
} from '@pascal-app/editor'
|
} from '@pascal-app/editor'
|
||||||
import { getSceneTheme, useViewer } from '@pascal-app/viewer'
|
import { getSceneTheme, useViewer } from '@pascal-app/viewer'
|
||||||
@@ -34,9 +35,11 @@ import { BoxGeometry, BufferGeometry, DoubleSide, type Group, type Mesh, Vector3
|
|||||||
|
|
||||||
const FENCE_PREVIEW_HEIGHT = 1.8
|
const FENCE_PREVIEW_HEIGHT = 1.8
|
||||||
const FENCE_PREVIEW_THICKNESS = 0.08
|
const FENCE_PREVIEW_THICKNESS = 0.08
|
||||||
const DRAFT_LABEL_Y = FENCE_PREVIEW_HEIGHT + 0.22
|
// HUD label heights are measured from the top of the preview bar, so they
|
||||||
const DRAFT_ANGLE_LABEL_Y = FENCE_PREVIEW_HEIGHT + 0.08
|
// track whatever height a seeded preset draws at (`previewHeight`).
|
||||||
const DRAFT_ANGLE_ARC_Y = FENCE_PREVIEW_HEIGHT + 0.012
|
const DRAFT_LABEL_Y_OFFSET = 0.22
|
||||||
|
const DRAFT_ANGLE_LABEL_Y_OFFSET = 0.08
|
||||||
|
const DRAFT_ANGLE_ARC_Y_OFFSET = 0.012
|
||||||
const DRAFT_ANGLE_ARC_MIN_RADIUS = 0.32
|
const DRAFT_ANGLE_ARC_MIN_RADIUS = 0.32
|
||||||
const DRAFT_ANGLE_ARC_MAX_RADIUS = 0.72
|
const DRAFT_ANGLE_ARC_MAX_RADIUS = 0.72
|
||||||
const DRAFT_ANGLE_ARC_SEGMENTS = 24
|
const DRAFT_ANGLE_ARC_SEGMENTS = 24
|
||||||
@@ -135,12 +138,16 @@ function toMiterWall(segment: SegmentLike): WallNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildDraftFenceSegment(start: FencePlanPoint, end: FencePlanPoint): SegmentLike {
|
function buildDraftFenceSegment(
|
||||||
|
start: FencePlanPoint,
|
||||||
|
end: FencePlanPoint,
|
||||||
|
thickness: number,
|
||||||
|
): SegmentLike {
|
||||||
return {
|
return {
|
||||||
id: 'fence_draft',
|
id: 'fence_draft',
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
thickness: FENCE_PREVIEW_THICKNESS,
|
thickness,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,10 +276,12 @@ function getDraftAngleLabels(
|
|||||||
end: FencePlanPoint,
|
end: FencePlanPoint,
|
||||||
segments: SegmentLike[],
|
segments: SegmentLike[],
|
||||||
baseY: number,
|
baseY: number,
|
||||||
|
previewHeight: number,
|
||||||
|
previewThickness: number,
|
||||||
): DraftAngleLabel[] {
|
): DraftAngleLabel[] {
|
||||||
const draftFromStart: FencePlanPoint = [end[0] - start[0], end[1] - start[1]]
|
const draftFromStart: FencePlanPoint = [end[0] - start[0], end[1] - start[1]]
|
||||||
const draftFromEnd: FencePlanPoint = [start[0] - end[0], start[1] - end[1]]
|
const draftFromEnd: FencePlanPoint = [start[0] - end[0], start[1] - end[1]]
|
||||||
const draftSegment = buildDraftFenceSegment(start, end)
|
const draftSegment = buildDraftFenceSegment(start, end, previewThickness)
|
||||||
const miterData = calculateLevelMiters([...segments, draftSegment].map(toMiterWall))
|
const miterData = calculateLevelMiters([...segments, draftSegment].map(toMiterWall))
|
||||||
const endpoints = [
|
const endpoints = [
|
||||||
{ id: 'start', point: start, draftVector: draftFromStart },
|
{ id: 'start', point: start, draftVector: draftFromStart },
|
||||||
@@ -322,7 +331,7 @@ function getDraftAngleLabels(
|
|||||||
label: formatAngleRadians(angle),
|
label: formatAngleRadians(angle),
|
||||||
position: [
|
position: [
|
||||||
arcCenter[0] + Math.cos(arc.midAngle) * (radius + 0.16),
|
arcCenter[0] + Math.cos(arc.midAngle) * (radius + 0.16),
|
||||||
baseY + DRAFT_ANGLE_LABEL_Y,
|
baseY + previewHeight + DRAFT_ANGLE_LABEL_Y_OFFSET,
|
||||||
arcCenter[1] + Math.sin(arc.midAngle) * (radius + 0.16),
|
arcCenter[1] + Math.sin(arc.midAngle) * (radius + 0.16),
|
||||||
],
|
],
|
||||||
arc: {
|
arc: {
|
||||||
@@ -330,7 +339,7 @@ function getDraftAngleLabels(
|
|||||||
radius,
|
radius,
|
||||||
startAngle: arc.startAngle,
|
startAngle: arc.startAngle,
|
||||||
endAngle: arc.endAngle,
|
endAngle: arc.endAngle,
|
||||||
y: baseY + DRAFT_ANGLE_ARC_Y,
|
y: baseY + previewHeight + DRAFT_ANGLE_ARC_Y_OFFSET,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -343,6 +352,8 @@ function getDraftMeasurementState(
|
|||||||
segments: SegmentLike[],
|
segments: SegmentLike[],
|
||||||
unit: 'metric' | 'imperial',
|
unit: 'metric' | 'imperial',
|
||||||
baseY: number,
|
baseY: number,
|
||||||
|
previewHeight: number,
|
||||||
|
previewThickness: number,
|
||||||
): DraftMeasurementState {
|
): DraftMeasurementState {
|
||||||
const dx = end[0] - start[0]
|
const dx = end[0] - start[0]
|
||||||
const dz = end[1] - start[1]
|
const dz = end[1] - start[1]
|
||||||
@@ -350,8 +361,12 @@ function getDraftMeasurementState(
|
|||||||
if (length < 0.01) return null
|
if (length < 0.01) return null
|
||||||
return {
|
return {
|
||||||
lengthLabel: formatMeasurement(length, unit),
|
lengthLabel: formatMeasurement(length, unit),
|
||||||
lengthPosition: [(start[0] + end[0]) / 2, baseY + DRAFT_LABEL_Y, (start[1] + end[1]) / 2],
|
lengthPosition: [
|
||||||
angleLabels: getDraftAngleLabels(start, end, segments, baseY),
|
(start[0] + end[0]) / 2,
|
||||||
|
baseY + previewHeight + DRAFT_LABEL_Y_OFFSET,
|
||||||
|
(start[1] + end[1]) / 2,
|
||||||
|
],
|
||||||
|
angleLabels: getDraftAngleLabels(start, end, segments, baseY, previewHeight, previewThickness),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +389,13 @@ function getReferenceSegments(walls: WallNode[], fences: FenceNode[]): SegmentLi
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFencePreview(mesh: Mesh, start: Vector3, end: Vector3) {
|
function updateFencePreview(
|
||||||
|
mesh: Mesh,
|
||||||
|
start: Vector3,
|
||||||
|
end: Vector3,
|
||||||
|
previewHeight: number,
|
||||||
|
previewThickness: number,
|
||||||
|
) {
|
||||||
const direction = new Vector3(end.x - start.x, 0, end.z - start.z)
|
const direction = new Vector3(end.x - start.x, 0, end.z - start.z)
|
||||||
const length = direction.length()
|
const length = direction.length()
|
||||||
if (length < 0.01) {
|
if (length < 0.01) {
|
||||||
@@ -383,14 +404,10 @@ function updateFencePreview(mesh: Mesh, start: Vector3, end: Vector3) {
|
|||||||
}
|
}
|
||||||
mesh.visible = true
|
mesh.visible = true
|
||||||
direction.normalize()
|
direction.normalize()
|
||||||
const geometry = new BoxGeometry(length, FENCE_PREVIEW_HEIGHT, FENCE_PREVIEW_THICKNESS)
|
const geometry = new BoxGeometry(length, previewHeight, previewThickness)
|
||||||
const angle = Math.atan2(direction.z, direction.x)
|
const angle = Math.atan2(direction.z, direction.x)
|
||||||
|
|
||||||
mesh.position.set(
|
mesh.position.set((start.x + end.x) / 2, start.y + previewHeight / 2, (start.z + end.z) / 2)
|
||||||
(start.x + end.x) / 2,
|
|
||||||
start.y + FENCE_PREVIEW_HEIGHT / 2,
|
|
||||||
(start.z + end.z) / 2,
|
|
||||||
)
|
|
||||||
mesh.rotation.y = -angle
|
mesh.rotation.y = -angle
|
||||||
|
|
||||||
if (mesh.geometry) {
|
if (mesh.geometry) {
|
||||||
@@ -415,6 +432,19 @@ function getCurrentLevelElements(): { walls: WallNode[]; fences: FenceNode[] } {
|
|||||||
export const FenceTool: React.FC = () => {
|
export const FenceTool: React.FC = () => {
|
||||||
const unit = useViewer((state) => state.unit)
|
const unit = useViewer((state) => state.unit)
|
||||||
const isDark = useViewer((state) => getSceneTheme(state.sceneTheme).appearance === 'dark')
|
const isDark = useViewer((state) => getSceneTheme(state.sceneTheme).appearance === 'dark')
|
||||||
|
// A placed preset seeds `toolDefaults.fence` before the tool mounts, so
|
||||||
|
// the draft preview is drawn at the preset's height / thickness rather
|
||||||
|
// than the generic fallbacks. Read through refs so the live event
|
||||||
|
// handlers below see the latest values without re-subscribing.
|
||||||
|
const fenceDefaults = useEditor((s) => s.toolDefaults.fence)
|
||||||
|
const previewHeight =
|
||||||
|
typeof fenceDefaults?.height === 'number' ? fenceDefaults.height : FENCE_PREVIEW_HEIGHT
|
||||||
|
const previewThickness =
|
||||||
|
typeof fenceDefaults?.thickness === 'number' ? fenceDefaults.thickness : FENCE_PREVIEW_THICKNESS
|
||||||
|
const previewHeightRef = useRef(previewHeight)
|
||||||
|
previewHeightRef.current = previewHeight
|
||||||
|
const previewThicknessRef = useRef(previewThickness)
|
||||||
|
previewThicknessRef.current = previewThickness
|
||||||
const cursorRef = useRef<Group>(null)
|
const cursorRef = useRef<Group>(null)
|
||||||
const previewRef = useRef<Mesh>(null!)
|
const previewRef = useRef<Mesh>(null!)
|
||||||
const startingPoint = useRef(new Vector3(0, 0, 0))
|
const startingPoint = useRef(new Vector3(0, 0, 0))
|
||||||
@@ -425,6 +455,11 @@ export const FenceTool: React.FC = () => {
|
|||||||
const measurementColor = isDark ? '#ffffff' : '#111111'
|
const measurementColor = isDark ? '#ffffff' : '#111111'
|
||||||
const measurementShadowColor = isDark ? '#111111' : '#ffffff'
|
const measurementShadowColor = isDark ? '#111111' : '#ffffff'
|
||||||
|
|
||||||
|
// Scope seeded defaults to this tool session: clear on deactivation so a
|
||||||
|
// later manual fence draw isn't drawn with a stale preset's parameters.
|
||||||
|
// Unmount-only (empty deps) — the [unit] effect below must not clear it.
|
||||||
|
useEffect(() => () => useEditor.getState().setToolDefaults('fence', null), [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let previousFenceEnd: FencePlanPoint | null = null
|
let previousFenceEnd: FencePlanPoint | null = null
|
||||||
|
|
||||||
@@ -459,7 +494,13 @@ export const FenceTool: React.FC = () => {
|
|||||||
triggerSFX('sfx:grid-snap')
|
triggerSFX('sfx:grid-snap')
|
||||||
}
|
}
|
||||||
previousFenceEnd = currentFenceEnd
|
previousFenceEnd = currentFenceEnd
|
||||||
updateFencePreview(previewRef.current, startingPoint.current, endingPoint.current)
|
updateFencePreview(
|
||||||
|
previewRef.current,
|
||||||
|
startingPoint.current,
|
||||||
|
endingPoint.current,
|
||||||
|
previewHeightRef.current,
|
||||||
|
previewThicknessRef.current,
|
||||||
|
)
|
||||||
setDraftMeasurement(
|
setDraftMeasurement(
|
||||||
getDraftMeasurementState(
|
getDraftMeasurementState(
|
||||||
[startingPoint.current.x, startingPoint.current.z],
|
[startingPoint.current.x, startingPoint.current.z],
|
||||||
@@ -467,6 +508,8 @@ export const FenceTool: React.FC = () => {
|
|||||||
getReferenceSegments(walls, fences),
|
getReferenceSegments(walls, fences),
|
||||||
unit,
|
unit,
|
||||||
startingPoint.current.y,
|
startingPoint.current.y,
|
||||||
|
previewHeightRef.current,
|
||||||
|
previewThicknessRef.current,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -556,7 +599,7 @@ export const FenceTool: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<group>
|
<group>
|
||||||
<CursorSphere height={FENCE_PREVIEW_HEIGHT} ref={cursorRef} />
|
<CursorSphere height={previewHeight} ref={cursorRef} />
|
||||||
<mesh layers={EDITOR_LAYER} ref={previewRef} renderOrder={1} visible={false}>
|
<mesh layers={EDITOR_LAYER} ref={previewRef} renderOrder={1} visible={false}>
|
||||||
<shapeGeometry />
|
<shapeGeometry />
|
||||||
<meshBasicMaterial
|
<meshBasicMaterial
|
||||||
|
|||||||
Reference in New Issue
Block a user