Fix room surfaces, wall openings, and paint scope (#498)
* chore(core): point material catalog at KTX2 tiers for wood/flooring/roofing finishes All 48 remaining webp/jpg/png finish entries now reference _512.ktx2 maps and 256px _thumb.webp previews, matching the fabric/leather/concrete/metal convention. flipY set to false on the converted entries — compressed textures can't be flipped at upload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: stabilize room surfaces and wall openings * style(core): format KTX2 material catalog --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
4fca38a3ef
commit
a524da1574
@@ -2,7 +2,7 @@ import { describe, expect, test } from 'bun:test'
|
||||
import { pointInPolygon2D, SlabNode } from '@pascal-app/core'
|
||||
import { slabDefinition } from '../definition'
|
||||
|
||||
function getHeightHandlePosition(slab: SlabNode) {
|
||||
function getHeightHandle(slab: SlabNode) {
|
||||
const handles =
|
||||
typeof slabDefinition.handles === 'function'
|
||||
? slabDefinition.handles(slab)
|
||||
@@ -13,7 +13,11 @@ function getHeightHandlePosition(slab: SlabNode) {
|
||||
if (!(heightHandle && heightHandle.kind === 'linear-resize')) {
|
||||
throw new Error('Missing slab height handle')
|
||||
}
|
||||
return heightHandle.placement.position(slab, {} as never)
|
||||
return heightHandle
|
||||
}
|
||||
|
||||
function getHeightHandlePosition(slab: SlabNode) {
|
||||
return getHeightHandle(slab).placement.position(slab, {} as never)
|
||||
}
|
||||
|
||||
describe('slabDefinition handles', () => {
|
||||
@@ -40,4 +44,20 @@ describe('slabDefinition handles', () => {
|
||||
expect(pointInPolygon2D([x, z], slab.polygon, { includeBoundary: false })).toBe(true)
|
||||
expect(pointInPolygon2D([x, z], slab.holes[0]!, { includeBoundary: true })).toBe(false)
|
||||
})
|
||||
|
||||
test('allows the elevation arrow to cross zero into a recessed slab', () => {
|
||||
const slab = SlabNode.parse({
|
||||
elevation: 0.05,
|
||||
polygon: [
|
||||
[0, 0],
|
||||
[2, 0],
|
||||
[2, 2],
|
||||
[0, 2],
|
||||
],
|
||||
})
|
||||
const heightHandle = getHeightHandle(slab)
|
||||
|
||||
expect(heightHandle.min).toBe(-1)
|
||||
expect(heightHandle.apply(slab, -0.15, {} as never)).toEqual({ elevation: -0.15 })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -25,7 +25,7 @@ const MODIFIERS = { shiftKey: false, altKey: false, ctrlKey: false, metaKey: fal
|
||||
* Level + one wall (centerline z=0, t=0.1) + one manual slab whose bottom
|
||||
* edge starts 0.5m away from the wall.
|
||||
*/
|
||||
function seedScene() {
|
||||
function seedScene(autoFromWalls = false) {
|
||||
const levelId = 'level_slab-move-edge' as AnyNodeId
|
||||
const wall = WallNode.parse({
|
||||
start: [0, 0],
|
||||
@@ -40,7 +40,7 @@ function seedScene() {
|
||||
[4, 3],
|
||||
[0, 3],
|
||||
],
|
||||
autoFromWalls: false,
|
||||
autoFromWalls,
|
||||
parentId: levelId,
|
||||
})
|
||||
const level = {
|
||||
@@ -115,4 +115,22 @@ describe('slabMoveEdgeAffordance', () => {
|
||||
expect(updated.polygon[0]![1]).toBeCloseTo(1.5, 5)
|
||||
expect(updated.polygon[1]![1]).toBeCloseTo(1.5, 5)
|
||||
})
|
||||
|
||||
test('editing an auto-generated outer boundary makes the slab manual', () => {
|
||||
const { slab } = seedScene(true)
|
||||
const nodes = useScene.getState().nodes
|
||||
|
||||
const session = slabMoveEdgeAffordance.start({
|
||||
node: nodes[slab.id] as SlabNodeType,
|
||||
payload: { edgeIndex: 0 },
|
||||
nodes,
|
||||
initialPlanPoint: [2, 0.5],
|
||||
gridSnapStep: 0.1,
|
||||
} as never)
|
||||
|
||||
session.apply({ planPoint: [2, 1.5], modifiers: MODIFIERS })
|
||||
|
||||
const updated = useScene.getState().nodes[slab.id] as SlabNodeType
|
||||
expect(updated.autoFromWalls).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -45,7 +45,7 @@ export const SlabBoundaryEditor: React.FC<{ slabId: SlabNode['id'] }> = ({ slabI
|
||||
const handlePolygonChange = useCallback(
|
||||
(newPolygon: Array<[number, number]>) => {
|
||||
clearSlabSnapFeedback()
|
||||
updateNode(slabId, { polygon: newPolygon })
|
||||
updateNode(slabId, { polygon: newPolygon, autoFromWalls: false })
|
||||
setSelection({ selectedIds: [slabId] })
|
||||
},
|
||||
[slabId, updateNode, setSelection],
|
||||
|
||||
@@ -18,7 +18,7 @@ import { SlabNode } from './schema'
|
||||
import { slabSlots } from './slots'
|
||||
|
||||
const HEIGHT_HANDLE_OFFSET = 0.22
|
||||
const MIN_SLAB_ELEVATION = 0.02
|
||||
const MIN_SLAB_ELEVATION = -1
|
||||
|
||||
function polygonVertexAverage(polygon: SlabNodeType['polygon']): [number, number] {
|
||||
if (polygon.length === 0) return [0, 0]
|
||||
@@ -89,10 +89,10 @@ function slabHandleAnchor(slab: SlabNodeType): [number, number] {
|
||||
}
|
||||
|
||||
// Slab height arrow — vertical chevron on solid slab surface near the
|
||||
// polygon center. Drags elevation (the extrusion thickness) with
|
||||
// `anchor: 'min'` so the bottom stays at world Y=0 and the top follows
|
||||
// the pointer. Same registry-handle pipeline as the column height arrow,
|
||||
// so live override + commit-on-release come for free.
|
||||
// polygon center. Drags elevation through zero: positive values extrude
|
||||
// upward from ground while negative values create a recessed floor whose
|
||||
// depth follows the pointer. Same registry-handle pipeline as the column
|
||||
// height arrow, so live override + commit-on-release come for free.
|
||||
function slabHeightHandle(): HandleDescriptor<SlabNodeType> {
|
||||
return {
|
||||
kind: 'linear-resize',
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
* Simpler model, no UX downside in practice.
|
||||
*/
|
||||
const slabSnapOptions = {
|
||||
boundaryCommitData: { autoFromWalls: false },
|
||||
resolvePlanPoint({
|
||||
node,
|
||||
nodes,
|
||||
|
||||
@@ -235,6 +235,7 @@ export const MoveSlabTool: React.FC<{ node: SlabNode }> = ({ node }) => {
|
||||
useScene.getState().updateNode(slabId, {
|
||||
polygon: translatePolygon(originalPolygon, deltaX, deltaZ),
|
||||
holes: originalHoles.map((h) => translatePolygon(h, deltaX, deltaZ)),
|
||||
autoFromWalls: false,
|
||||
})
|
||||
useScene.getState().markDirty(slabId as AnyNodeId)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export const slabParametrics: ParametricDescriptor<SlabNode> = {
|
||||
groups: [
|
||||
{
|
||||
label: 'Elevation',
|
||||
fields: [{ key: 'elevation', kind: 'number', unit: 'm', min: 0.02, max: 1, step: 0.01 }],
|
||||
fields: [{ key: 'elevation', kind: 'number', unit: 'm', min: -1, max: 1, step: 0.01 }],
|
||||
},
|
||||
],
|
||||
customPanel: () => import('./panel'),
|
||||
|
||||
Reference in New Issue
Block a user