Wawa e618175bba Reset core/viewer/editor/mcp packages and apps/editor from private-editor
Wholesale swap of packages/{core,viewer,editor,mcp} and apps/editor with the
versions from the private editor repo, which is the production source of truth.

Setup changes:
- packages/{core,viewer,editor} versions held at 0.7.0 baseline (matching
  the most recent published release) so a bump=minor publishes 0.8.0
- packages/mcp held at 0.1.1 (never published; first publish will go through
  the new release.yml flow)
- peerDependencies and devDependencies for inter-package @pascal-app/*
  references pinned to ^0.7.0 instead of '*' / 'workspace:*' so they are
  valid for npm consumers
- Root package.json: TypeScript bumped to 6.0.2, added overrides for
  @types/react, @types/react-dom, @types/three to prevent JSX namespace
  fragmentation across the workspace
- release.yml extended to also publish editor and mcp; 'both' option renamed
  to 'all'; added a sync step that updates inter-package peerDeps/devDeps to
  match the new versions on every bump (so viewer/editor/mcp tarballs always
  reference the version of core they were built against)
- Root scripts gained release:editor and release:mcp shortcuts

Verification:
- bun install --frozen-lockfile is consistent
- packages/{core,viewer,mcp} build cleanly, dist/index.d.ts emitted
- packages/editor check-types reports 21 pre-existing errors, identical to
  what private-editor currently reports

Open PRs against editor-v2 will need rebasing/conflict resolution.
2026-05-09 20:52:26 +00:00

157 lines
4.0 KiB
TypeScript

import {
type FenceNode,
getWallCurveFrameAt,
getWallCurveLength,
isCurvedWall,
type WallNode,
} from '@pascal-app/core'
export type PlanPoint = [number, number]
export type SegmentAngleLike = Pick<WallNode | FenceNode, 'start' | 'end' | 'curveOffset'>
export type SegmentAngleReference = {
vector: PlanPoint
orientation: 'directed' | 'axis'
}
const POINT_MATCH_TOLERANCE = 1e-5
const SEGMENT_POINT_TOLERANCE = 0.15
const CURVE_TANGENT_SAMPLE_SPACING = 0.08
function distanceSquared(a: PlanPoint, b: PlanPoint) {
const dx = a[0] - b[0]
const dz = a[1] - b[1]
return dx * dx + dz * dz
}
function pointsMatch(a: PlanPoint, b: PlanPoint, tolerance = POINT_MATCH_TOLERANCE) {
return distanceSquared(a, b) <= tolerance * tolerance
}
function getProjectedPointOnSegment(point: PlanPoint, segment: SegmentAngleLike): PlanPoint | null {
const [x1, z1] = segment.start
const [x2, z2] = segment.end
const dx = x2 - x1
const dz = z2 - z1
const lengthSquared = dx * dx + dz * dz
if (lengthSquared < 1e-9) {
return null
}
const t = ((point[0] - x1) * dx + (point[1] - z1) * dz) / lengthSquared
if (t <= 0 || t >= 1) {
return null
}
return [x1 + dx * t, z1 + dz * t]
}
function getCurveTangentAtPoint(point: PlanPoint, segment: SegmentAngleLike): PlanPoint | null {
const curveLength = getWallCurveLength(segment)
const sampleCount = Math.max(24, Math.ceil(curveLength / CURVE_TANGENT_SAMPLE_SPACING))
let best: { distance: number; tangent: PlanPoint } | null = null
for (let index = 0; index <= sampleCount; index += 1) {
const frame = getWallCurveFrameAt(segment, index / sampleCount)
const candidate: PlanPoint = [frame.point.x, frame.point.y]
const distance = distanceSquared(point, candidate)
if (best && distance >= best.distance) {
continue
}
best = {
distance,
tangent: [frame.tangent.x, frame.tangent.y],
}
}
if (!best || best.distance > SEGMENT_POINT_TOLERANCE * SEGMENT_POINT_TOLERANCE) {
return null
}
return best.tangent
}
export function formatAngleRadians(angle: number) {
return `${Math.round((angle * 180) / Math.PI)}°`
}
export function getAngleBetweenVectors(first: PlanPoint, second: PlanPoint): number | null {
const firstLength = Math.hypot(first[0], first[1])
const secondLength = Math.hypot(second[0], second[1])
if (firstLength < 1e-6 || secondLength < 1e-6) return null
const dot = first[0] * second[0] + first[1] * second[1]
const cosine = Math.min(1, Math.max(-1, dot / (firstLength * secondLength)))
return Math.acos(cosine)
}
export function getAngleToSegmentReference(
vector: PlanPoint,
reference: SegmentAngleReference,
): number | null {
const angle = getAngleBetweenVectors(vector, reference.vector)
if (angle === null || reference.orientation === 'directed') {
return angle
}
const reverseAngle = getAngleBetweenVectors(vector, [-reference.vector[0], -reference.vector[1]])
if (reverseAngle === null) {
return angle
}
return Math.min(angle, reverseAngle)
}
export function getSegmentAngleReferenceAtPoint(
point: PlanPoint,
segment: SegmentAngleLike,
): SegmentAngleReference | null {
if (pointsMatch(point, segment.start)) {
const frame = getWallCurveFrameAt(segment, 0)
return {
vector: [frame.tangent.x, frame.tangent.y],
orientation: 'directed',
}
}
if (pointsMatch(point, segment.end)) {
const frame = getWallCurveFrameAt(segment, 1)
return {
vector: [-frame.tangent.x, -frame.tangent.y],
orientation: 'directed',
}
}
if (isCurvedWall(segment)) {
const tangent = getCurveTangentAtPoint(point, segment)
return tangent
? {
vector: tangent,
orientation: 'axis',
}
: null
}
const projected = getProjectedPointOnSegment(point, segment)
if (!(projected && pointsMatch(point, projected, SEGMENT_POINT_TOLERANCE))) {
return null
}
return {
vector: [segment.end[0] - segment.start[0], segment.end[1] - segment.start[1]],
orientation: 'axis',
}
}