@pascal-app/core 1.1.0 → 1.1.2

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.
Files changed (133) hide show
  1. package/dist/agent-operations/__fixtures__/cases.d.ts +3 -2
  2. package/dist/agent-operations/__fixtures__/cases.d.ts.map +1 -1
  3. package/dist/agent-operations/__fixtures__/place-items-cases.d.ts.map +1 -1
  4. package/dist/agent-operations/__fixtures__/place-items-cases.js +28 -0
  5. package/dist/agent-operations/__fixtures__/roof-bearing-cases.d.ts +2618 -2618
  6. package/dist/agent-operations/asset-catalog.d.ts +21 -11
  7. package/dist/agent-operations/asset-catalog.d.ts.map +1 -1
  8. package/dist/agent-operations/asset-catalog.js +28 -10
  9. package/dist/agent-operations/asset-views.d.ts +8 -3
  10. package/dist/agent-operations/asset-views.d.ts.map +1 -1
  11. package/dist/agent-operations/asset-views.js +19 -3
  12. package/dist/agent-operations/batch.d.ts +4 -2
  13. package/dist/agent-operations/batch.d.ts.map +1 -1
  14. package/dist/agent-operations/batch.js +9 -6
  15. package/dist/agent-operations/catalog-asset.d.ts +15 -0
  16. package/dist/agent-operations/catalog-asset.d.ts.map +1 -0
  17. package/dist/agent-operations/catalog-asset.js +4 -0
  18. package/dist/agent-operations/circulation-spaces.d.ts +585 -585
  19. package/dist/agent-operations/design-brief.d.ts.map +1 -1
  20. package/dist/agent-operations/design-brief.js +2 -0
  21. package/dist/agent-operations/furnish-room.d.ts.map +1 -1
  22. package/dist/agent-operations/furnish-room.js +2 -1
  23. package/dist/agent-operations/index.d.ts +5716 -7
  24. package/dist/agent-operations/index.d.ts.map +1 -1
  25. package/dist/agent-operations/index.js +17 -3
  26. package/dist/agent-operations/level-reads.d.ts.map +1 -1
  27. package/dist/agent-operations/level-reads.js +4 -1
  28. package/dist/agent-operations/place-in-room.d.ts.map +1 -1
  29. package/dist/agent-operations/place-in-room.js +2 -1
  30. package/dist/agent-operations/place-items.d.ts.map +1 -1
  31. package/dist/agent-operations/place-items.js +2 -1
  32. package/dist/agent-operations/scene-queries.d.ts +3818 -3818
  33. package/dist/agent-operations/scene-view.d.ts.map +1 -1
  34. package/dist/agent-operations/scene-view.js +38 -13
  35. package/dist/agent-operations/search-assets.d.ts.map +1 -1
  36. package/dist/agent-operations/search-assets.js +6 -0
  37. package/dist/agent-operations/types.d.ts +10 -2
  38. package/dist/agent-operations/types.d.ts.map +1 -1
  39. package/dist/agent-operations/usage-checks.js +18 -3
  40. package/dist/agent-operations/verify-scene.d.ts +7 -3
  41. package/dist/agent-operations/verify-scene.d.ts.map +1 -1
  42. package/dist/agent-operations/verify-scene.js +15 -7
  43. package/dist/agent-tools/assets.js +1 -1
  44. package/dist/building/__fixtures__/wall-opening-cases.d.ts +30 -30
  45. package/dist/building/level-duplication.d.ts +3409 -3409
  46. package/dist/building/level-duplication.d.ts.map +1 -1
  47. package/dist/building/level-duplication.js +3 -1
  48. package/dist/building/wall-openings.d.ts +75 -75
  49. package/dist/commands/structure/shared.d.ts +3407 -3407
  50. package/dist/commands/structure/zone-wall-merge.d.ts +652 -652
  51. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +3818 -3818
  52. package/dist/index.d.ts +2 -0
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +2 -0
  55. package/dist/lib/__fixtures__/mezzanine.d.ts +2003 -2003
  56. package/dist/lib/absorb-wall-separators.d.ts +1032 -1032
  57. package/dist/lib/asset-storage.d.ts.map +1 -1
  58. package/dist/lib/asset-storage.js +3 -0
  59. package/dist/lib/floor-foundation-datum.d.ts.map +1 -1
  60. package/dist/lib/floor-foundation-datum.js +5 -1
  61. package/dist/lib/floorplan/context.d.ts +95 -0
  62. package/dist/lib/floorplan/context.d.ts.map +1 -0
  63. package/dist/lib/floorplan/context.js +155 -0
  64. package/dist/lib/floorplan/index.d.ts +6 -0
  65. package/dist/lib/floorplan/index.d.ts.map +1 -0
  66. package/dist/lib/floorplan/index.js +5 -0
  67. package/dist/lib/floorplan/marks.d.ts +71 -0
  68. package/dist/lib/floorplan/marks.d.ts.map +1 -0
  69. package/dist/lib/floorplan/marks.js +157 -0
  70. package/dist/lib/floorplan/plan-math.d.ts +18 -0
  71. package/dist/lib/floorplan/plan-math.d.ts.map +1 -0
  72. package/dist/lib/floorplan/plan-math.js +82 -0
  73. package/dist/lib/floorplan/svg-paths.d.ts +17 -0
  74. package/dist/lib/floorplan/svg-paths.d.ts.map +1 -0
  75. package/dist/lib/floorplan/svg-paths.js +92 -0
  76. package/dist/lib/floorplan/wall-affordances.d.ts +31 -0
  77. package/dist/lib/floorplan/wall-affordances.d.ts.map +1 -0
  78. package/dist/lib/floorplan/wall-affordances.js +24 -0
  79. package/dist/lib/steps.d.ts +8 -0
  80. package/dist/lib/steps.d.ts.map +1 -0
  81. package/dist/lib/steps.js +7 -0
  82. package/dist/registry/__fixtures__/roof-planes.d.ts +69 -0
  83. package/dist/registry/__fixtures__/roof-planes.d.ts.map +1 -0
  84. package/dist/registry/__fixtures__/roof-planes.js +59 -0
  85. package/dist/registry/index.d.ts +2 -1
  86. package/dist/registry/index.d.ts.map +1 -1
  87. package/dist/registry/index.js +1 -0
  88. package/dist/registry/roofs.d.ts +34 -0
  89. package/dist/registry/roofs.d.ts.map +1 -0
  90. package/dist/registry/roofs.js +61 -0
  91. package/dist/registry/types.d.ts +30 -0
  92. package/dist/registry/types.d.ts.map +1 -1
  93. package/dist/registry/validate-relations.d.ts +5 -1
  94. package/dist/registry/validate-relations.d.ts.map +1 -1
  95. package/dist/registry/validate-relations.js +6 -2
  96. package/dist/schema/asset-url.d.ts +8 -0
  97. package/dist/schema/asset-url.d.ts.map +1 -1
  98. package/dist/schema/asset-url.js +17 -0
  99. package/dist/schema/nodes/roof.d.ts +1 -1
  100. package/dist/schema/nodes/roof.d.ts.map +1 -1
  101. package/dist/schema/nodes/roof.js +13 -8
  102. package/dist/schema/types.d.ts +1 -1
  103. package/dist/services/roof-level.d.ts.map +1 -1
  104. package/dist/services/roof-level.js +12 -2
  105. package/dist/services/roof-move.d.ts +4 -2
  106. package/dist/services/roof-move.d.ts.map +1 -1
  107. package/dist/services/roof-move.js +21 -9
  108. package/dist/spatial/usage-field/findings.d.ts +10 -1
  109. package/dist/spatial/usage-field/findings.d.ts.map +1 -1
  110. package/dist/spatial/usage-field/findings.js +56 -12
  111. package/dist/spatial/usage-field/measure.d.ts +3 -1
  112. package/dist/spatial/usage-field/measure.d.ts.map +1 -1
  113. package/dist/spatial/usage-field/measure.js +7 -2
  114. package/dist/spatial/usage-field/path.d.ts.map +1 -1
  115. package/dist/spatial/usage-field/path.js +28 -18
  116. package/dist/store/actions/node-actions.d.ts +2380 -2380
  117. package/dist/store/scene-annotations.d.ts +1279 -1279
  118. package/dist/systems/document-reconcile.d.ts.map +1 -1
  119. package/dist/systems/document-reconcile.js +1 -2
  120. package/dist/systems/roof/roof-elevation.d.ts +11 -6
  121. package/dist/systems/roof/roof-elevation.d.ts.map +1 -1
  122. package/dist/systems/roof/roof-elevation.js +24 -11
  123. package/dist/systems/slab/__fixtures__/floor-step.d.ts +2381 -2381
  124. package/dist/systems/slab/__fixtures__/raised-room.d.ts +3818 -3818
  125. package/dist/systems/stair/stair-opening-preview.d.ts +900 -900
  126. package/dist/utils/ensure-scene-openings.d.ts +4815 -4815
  127. package/dist/utils/floor-plate-migration.d.ts +1032 -1032
  128. package/dist/utils/owned-floor-opening-migration.d.ts +1032 -1032
  129. package/dist/utils/reconcile-structure-on-load.d.ts +2380 -2380
  130. package/dist/utils/scene-bounds.d.ts +32 -0
  131. package/dist/utils/scene-bounds.d.ts.map +1 -0
  132. package/dist/utils/scene-bounds.js +201 -0
  133. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"asset-storage.d.ts","sourceRoot":"","sources":["../../src/lib/asset-storage.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,YAAY,gBAAgB,CAAA;AAQzC;;GAEG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAI3D;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAoCtE"}
1
+ {"version":3,"file":"asset-storage.d.ts","sourceRoot":"","sources":["../../src/lib/asset-storage.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,YAAY,gBAAgB,CAAA;AAQzC;;GAEG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAI3D;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAqCtE"}
@@ -1,5 +1,6 @@
1
1
  import { get, set } from 'idb-keyval';
2
2
  import { customAlphabet } from 'nanoid';
3
+ import { isSchemeRelativeUrl } from '../schema/asset-url.js';
3
4
  import { ARTIFACT_URL_PREFIX, resolveArtifactUrl } from './artifact-store.js';
4
5
  export const ASSET_PREFIX = 'asset_data:';
5
6
  // Cache for active object URLs to prevent leaks and flickering
@@ -21,6 +22,8 @@ export async function saveAsset(file) {
21
22
  export async function loadAssetUrl(url) {
22
23
  if (!url)
23
24
  return null;
25
+ if (isSchemeRelativeUrl(url))
26
+ return null;
24
27
  // If it's already a blob or http URL, return as is
25
28
  if (url.startsWith('blob:') || url.startsWith('http')) {
26
29
  return url;
@@ -1 +1 @@
1
- {"version":3,"file":"floor-foundation-datum.d.ts","sourceRoot":"","sources":["../../src/lib/floor-foundation-datum.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAclD,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,KAAK,EAAE,QAAQ,GACd,MAAM,CAER;AAED,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,KAAK,EAAE,QAAQ,GACd,MAAM,CA+BR;AAED,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,KAAK,EAAE,QAAQ,GACd,WAAW,GAAG,gBAAgB,CA+BhC;AAED,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,KAAK,EAAE,QAAQ,GACd,OAAO,CAMT;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,KAAK,EAAE,QAAQ,GACd,OAAO,CAET;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,KAAK,EAAE,QAAQ,GACd;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAA;CAAE,CAK7E;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,KAAK,EAAE,QAAQ,GACd,MAAM,CAER;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,GAAG,MAAM,CAM/F;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,KAAK,EAAE,QAAQ,EACf,SAAS,GAAE,MAA6C,GACvD;IACD,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;IAC5B,MAAM,CAAC,EAAE,aAAa,CAAA;CACvB,GAAG,IAAI,CAYP;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,IAAI,EAAE,OAAO,EACb,YAAY,EAAE,MAAM,GACnB,MAAM,CAUR;AAID,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,QAAQ,EACd,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACvC,OAAO,CAgBT"}
1
+ {"version":3,"file":"floor-foundation-datum.d.ts","sourceRoot":"","sources":["../../src/lib/floor-foundation-datum.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAclD,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,KAAK,EAAE,QAAQ,GACd,MAAM,CAER;AAED,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,KAAK,EAAE,QAAQ,GACd,MAAM,CA+BR;AAED,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,KAAK,EAAE,QAAQ,GACd,WAAW,GAAG,gBAAgB,CA+BhC;AAED,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,KAAK,EAAE,QAAQ,GACd,OAAO,CAMT;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,KAAK,EAAE,QAAQ,GACd,OAAO,CAET;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,KAAK,EAAE,QAAQ,GACd;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAA;CAAE,CAK7E;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,KAAK,EAAE,QAAQ,GACd,MAAM,CAER;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,GAAG,MAAM,CAM/F;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,KAAK,EAAE,QAAQ,EACf,SAAS,GAAE,MAA6C,GACvD;IACD,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAA;IAC5B,MAAM,CAAC,EAAE,aAAa,CAAA;CACvB,GAAG,IAAI,CAYP;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,IAAI,EAAE,OAAO,EACb,YAAY,EAAE,MAAM,GACnB,MAAM,CAUR;AAID,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,QAAQ,EACd,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACvC,OAAO,CAgBT"}
@@ -1,3 +1,4 @@
1
+ import { roofFaceShapes } from '../registry/roofs.js';
1
2
  import { DEFAULT_SLAB_ELEVATION, MIN_SLAB_THICKNESS } from '../schema/nodes/slab.js';
2
3
  import { wallOverlapsSlabFootprint } from '../systems/slab/slab-support.js';
3
4
  import { getWallArcData } from '../systems/wall/wall-curve.js';
@@ -199,8 +200,11 @@ function computeFootprintSupport(base, node, nodes) {
199
200
  const segments = node.children
200
201
  .map((id) => nodes[id])
201
202
  .filter((segment) => segment?.type === 'roof-segment');
202
- if (!segments.length)
203
+ const faces = roofFaceShapes(node, nodes);
204
+ if (!segments.length && !faces.length)
203
205
  return containsPoint([{ outer: base.polygon, holes: base.holes ?? [] }], [node.position[0], node.position[2]]);
206
+ if (faces.some((face) => polygonsOverlap(base, face.outline)))
207
+ return true;
204
208
  return segments.some((segment) => {
205
209
  if (segment.type !== 'roof-segment')
206
210
  return false;
@@ -0,0 +1,95 @@
1
+ import type { FloorplanGeometry, FloorplanPalette, GeometryContext } from '../../registry/types.js';
2
+ import type { AnyNode } from '../../schema/index.js';
3
+ /** The `def.floorplan` contract every plan host shares: context extensions and geometry metadata. */
4
+ export declare const FLOORPLAN_GEOMETRY_METADATA_KEY = "pascal:editor/floorplan";
5
+ export declare const FLOORPLAN_CONTEXT_EXTENSION_KEY = "pascal:editor/floorplan";
6
+ export type FloorplanRenderPurpose = 'edit' | 'document';
7
+ export type FloorplanMetricNotation = 'meters' | 'millimeters';
8
+ /**
9
+ * Display → Measure walls: which finished faces wall dimensions measure to —
10
+ * the interior faces (ID, the room sizes), the exterior faces (OD, the
11
+ * outside size) or both. Read by a selected wall's temporary dimensions in 2D
12
+ * and 3D and by the Expert plan's automatic dimension strings.
13
+ */
14
+ export type FloorplanWallDimensionFaces = 'interior' | 'exterior' | 'both';
15
+ export declare const DEFAULT_FLOORPLAN_WALL_DIMENSION_FACES: FloorplanWallDimensionFaces;
16
+ export declare function normalizeFloorplanWallDimensionFaces(value: unknown): FloorplanWallDimensionFaces;
17
+ export type FloorplanAnnotationRole = 'automatic-dimension' | 'contextual-dimension' | 'manual-dimension' | 'measurement' | 'opening-mark' | 'structural-grid' | 'column-center' | 'room-label'
18
+ /** A room's finish / ceiling-height lines — detail under the name, off in the clean plan. */
19
+ | 'room-detail'
20
+ /** The roof plan proper: outline, ridges, hips, slope arrows. Off over a floor plan unless asked. */
21
+ | 'roof-plan' | 'roof-pitch' | 'stair-annotation';
22
+ export type FloorplanSchedule = {
23
+ id: string;
24
+ title: string;
25
+ columns: ReadonlyArray<{
26
+ key: string;
27
+ label: string;
28
+ weight?: number;
29
+ }>;
30
+ rows: ReadonlyArray<{
31
+ id: string;
32
+ cells: Readonly<Record<string, string>>;
33
+ }>;
34
+ issues?: readonly string[];
35
+ };
36
+ type FloorplanGeometryMetadata = {
37
+ annotationRole?: FloorplanAnnotationRole;
38
+ annotationObstacle?: 'bounds' | 'outline';
39
+ renderPass?: 'overlay';
40
+ /**
41
+ * Annotation text only: the size to print at, in paper points, instead of
42
+ * the size its role implies. Lets a sheet set a room tag's name larger than
43
+ * its number without inventing a role per size.
44
+ */
45
+ textSizePt?: number;
46
+ };
47
+ type FloorplanContextExtension = {
48
+ automaticDimensions: boolean;
49
+ purpose: FloorplanRenderPurpose;
50
+ /**
51
+ * Sheet drafting (the Sheets plan viewports): kinds draw the permit-set
52
+ * convention — fixtures as labelled linework, furniture as light outlines,
53
+ * no raster sprites. Only `collectSheetGeometry` sets it; the editor and the
54
+ * read-only viewer never do, so their plans keep today's look.
55
+ */
56
+ drafting: boolean;
57
+ /**
58
+ * The editor canvas draws room labels as screen pills over the plan
59
+ * (`RoomLabels2D`), so zones skip their drawn tags and wash there. Exports,
60
+ * sheets and the read-only viewer keep the drawn tags.
61
+ */
62
+ roomLabelOverlay: boolean;
63
+ metricNotation: FloorplanMetricNotation;
64
+ wallDimensionFaces: FloorplanWallDimensionFaces;
65
+ };
66
+ export declare function floorplanGeometryMetadata(values: FloorplanGeometryMetadata): Readonly<Record<string, unknown>>;
67
+ export declare function withFloorplanGeometryMetadata<T extends FloorplanGeometry | null>(geometry: T, values: FloorplanGeometryMetadata): T;
68
+ export declare function readFloorplanGeometryMetadata(geometry: unknown): FloorplanGeometryMetadata;
69
+ export declare function isFloorplanAnnotationObstacleGeometry(geometry: FloorplanGeometry): boolean;
70
+ export declare function floorplanAnnotationObstacleMode(geometry: FloorplanGeometry): 'bounds' | 'outline' | '' | undefined;
71
+ export declare function createFloorplanContextExtensions(values: Partial<FloorplanContextExtension>): Readonly<Record<string, unknown>>;
72
+ export declare function readFloorplanContext(ctx: GeometryContext): FloorplanContextExtension;
73
+ export declare function readFloorplanMetricNotationOverride(ctx: GeometryContext): FloorplanMetricNotation | undefined;
74
+ export type FloorplanViewState = {
75
+ automaticDimensions?: boolean;
76
+ selected: boolean;
77
+ unit: 'metric' | 'imperial';
78
+ metricNotation?: 'meters' | 'millimeters';
79
+ purpose?: 'edit' | 'document';
80
+ /** Sheet drafting conventions (see `FloorplanContextExtension.drafting`). */
81
+ drafting?: boolean;
82
+ /** Room labels drawn as screen pills (see `FloorplanContextExtension.roomLabelOverlay`). */
83
+ roomLabelOverlay?: boolean;
84
+ wallDimensionFaces?: FloorplanWallDimensionFaces;
85
+ highlighted: boolean;
86
+ hovered: boolean;
87
+ moving: boolean;
88
+ focusedUnitId?: string;
89
+ focusedUnitMemberIds?: readonly string[];
90
+ palette: FloorplanPalette | undefined;
91
+ };
92
+ export declare function buildFloorplanContext(node: AnyNode, nodes: Record<string, AnyNode>, viewState: FloorplanViewState, levelData?: unknown): GeometryContext;
93
+ export declare function floorplanLayerRank(type: string): number;
94
+ export {};
95
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../../src/lib/floorplan/context.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,155 @@
1
+ /** The `def.floorplan` contract every plan host shares: context extensions and geometry metadata. */
2
+ export const FLOORPLAN_GEOMETRY_METADATA_KEY = 'pascal:editor/floorplan';
3
+ export const FLOORPLAN_CONTEXT_EXTENSION_KEY = 'pascal:editor/floorplan';
4
+ export const DEFAULT_FLOORPLAN_WALL_DIMENSION_FACES = 'both';
5
+ export function normalizeFloorplanWallDimensionFaces(value) {
6
+ return value === 'interior' || value === 'exterior'
7
+ ? value
8
+ : DEFAULT_FLOORPLAN_WALL_DIMENSION_FACES;
9
+ }
10
+ export function floorplanGeometryMetadata(values) {
11
+ return { [FLOORPLAN_GEOMETRY_METADATA_KEY]: values };
12
+ }
13
+ export function withFloorplanGeometryMetadata(geometry, values) {
14
+ if (!geometry)
15
+ return geometry;
16
+ const existing = readFloorplanGeometryMetadata(geometry);
17
+ return {
18
+ ...geometry,
19
+ metadata: floorplanGeometryMetadata({ ...existing, ...values }),
20
+ };
21
+ }
22
+ export function readFloorplanGeometryMetadata(geometry) {
23
+ const metadata = geometry?.metadata;
24
+ const value = metadata?.[FLOORPLAN_GEOMETRY_METADATA_KEY];
25
+ return value && typeof value === 'object' && !Array.isArray(value)
26
+ ? value
27
+ : {};
28
+ }
29
+ export function isFloorplanAnnotationObstacleGeometry(geometry) {
30
+ if (floorplanAnnotationObstacleMode(geometry))
31
+ return true;
32
+ if (geometry.kind !== 'group')
33
+ return false;
34
+ const hasPlate = geometry.children.some((child) => child.kind === 'rect' || child.kind === 'circle');
35
+ const hasUprightText = geometry.children.some((child) => child.kind === 'text' && child.upright);
36
+ return hasPlate && hasUprightText;
37
+ }
38
+ export function floorplanAnnotationObstacleMode(geometry) {
39
+ const metadata = readFloorplanGeometryMetadata(geometry);
40
+ if (metadata.annotationObstacle)
41
+ return metadata.annotationObstacle;
42
+ switch (metadata.annotationRole) {
43
+ case 'room-label':
44
+ return geometry.kind === 'text' ? 'bounds' : undefined;
45
+ case 'column-center':
46
+ return geometry.kind === 'line' || geometry.kind === 'text' ? 'bounds' : undefined;
47
+ case 'stair-annotation':
48
+ return geometry.kind === 'polyline' || geometry.kind === 'line' ? 'outline' : 'bounds';
49
+ default:
50
+ return undefined;
51
+ }
52
+ }
53
+ export function createFloorplanContextExtensions(values) {
54
+ return {
55
+ [FLOORPLAN_CONTEXT_EXTENSION_KEY]: {
56
+ automaticDimensions: values.automaticDimensions !== false,
57
+ purpose: values.purpose === 'document' ? 'document' : 'edit',
58
+ drafting: values.drafting === true,
59
+ roomLabelOverlay: values.roomLabelOverlay === true,
60
+ metricNotation: values.metricNotation === 'millimeters' ? 'millimeters' : 'meters',
61
+ wallDimensionFaces: normalizeFloorplanWallDimensionFaces(values.wallDimensionFaces),
62
+ },
63
+ };
64
+ }
65
+ export function readFloorplanContext(ctx) {
66
+ const value = ctx.extensions?.[FLOORPLAN_CONTEXT_EXTENSION_KEY];
67
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
68
+ const extension = value;
69
+ return {
70
+ automaticDimensions: extension.automaticDimensions !== false,
71
+ purpose: extension.purpose === 'document' ? 'document' : 'edit',
72
+ drafting: extension.drafting === true,
73
+ roomLabelOverlay: extension.roomLabelOverlay === true,
74
+ metricNotation: extension.metricNotation === 'millimeters' ? 'millimeters' : 'meters',
75
+ wallDimensionFaces: normalizeFloorplanWallDimensionFaces(extension.wallDimensionFaces),
76
+ };
77
+ }
78
+ return {
79
+ automaticDimensions: true,
80
+ purpose: 'edit',
81
+ drafting: false,
82
+ roomLabelOverlay: false,
83
+ metricNotation: 'meters',
84
+ wallDimensionFaces: DEFAULT_FLOORPLAN_WALL_DIMENSION_FACES,
85
+ };
86
+ }
87
+ export function readFloorplanMetricNotationOverride(ctx) {
88
+ const value = ctx.extensions?.[FLOORPLAN_CONTEXT_EXTENSION_KEY];
89
+ if (!value || typeof value !== 'object' || Array.isArray(value))
90
+ return undefined;
91
+ const metricNotation = value.metricNotation;
92
+ return metricNotation === 'meters' || metricNotation === 'millimeters'
93
+ ? metricNotation
94
+ : undefined;
95
+ }
96
+ export function buildFloorplanContext(node, nodes, viewState, levelData) {
97
+ const resolve = (id) => nodes[id];
98
+ const childIds = node.children;
99
+ const children = Array.isArray(childIds)
100
+ ? childIds.map((id) => nodes[id]).filter((child) => child !== undefined)
101
+ : [];
102
+ const parentId = node.parentId;
103
+ const parent = parentId ? (nodes[parentId] ?? null) : null;
104
+ let siblings = [];
105
+ if (parent) {
106
+ const parentChildIds = parent.children;
107
+ siblings = Array.isArray(parentChildIds)
108
+ ? parentChildIds
109
+ .filter((id) => id !== node.id)
110
+ .map((id) => nodes[id])
111
+ .filter((sibling) => sibling?.type === node.type)
112
+ : Object.values(nodes).filter((candidate) => candidate.id !== node.id &&
113
+ candidate.type === node.type &&
114
+ candidate.parentId === node.parentId);
115
+ }
116
+ return {
117
+ resolve,
118
+ children,
119
+ siblings,
120
+ parent,
121
+ levelData,
122
+ sceneNodes: nodes,
123
+ extensions: createFloorplanContextExtensions({
124
+ automaticDimensions: viewState.automaticDimensions,
125
+ metricNotation: viewState.metricNotation ?? 'meters',
126
+ purpose: viewState.purpose ?? 'edit',
127
+ drafting: viewState.drafting === true,
128
+ roomLabelOverlay: viewState.roomLabelOverlay === true,
129
+ wallDimensionFaces: viewState.wallDimensionFaces,
130
+ }),
131
+ viewState: viewState.palette
132
+ ? {
133
+ selected: viewState.selected,
134
+ unit: viewState.unit,
135
+ highlighted: viewState.highlighted,
136
+ hovered: viewState.hovered,
137
+ moving: viewState.moving,
138
+ focusedUnitId: viewState.focusedUnitId,
139
+ focusedUnitMemberIds: viewState.focusedUnitMemberIds,
140
+ palette: viewState.palette,
141
+ }
142
+ : undefined,
143
+ };
144
+ }
145
+ export function floorplanLayerRank(type) {
146
+ switch (type) {
147
+ case 'zone':
148
+ return 0;
149
+ case 'slab':
150
+ case 'ceiling':
151
+ return 1;
152
+ default:
153
+ return 2;
154
+ }
155
+ }
@@ -0,0 +1,6 @@
1
+ export * from './context.js';
2
+ export * from './marks.js';
3
+ export * from './plan-math.js';
4
+ export * from './svg-paths.js';
5
+ export * from './wall-affordances.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/floorplan/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,SAAS,CAAA;AACvB,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,oBAAoB,CAAA"}
@@ -0,0 +1,5 @@
1
+ export * from './context.js';
2
+ export * from './marks.js';
3
+ export * from './plan-math.js';
4
+ export * from './svg-paths.js';
5
+ export * from './wall-affordances.js';
@@ -0,0 +1,71 @@
1
+ import type { AnyNode, AnyNodeId, DoorNode, WindowNode } from '../../schema/index.js';
2
+ /**
3
+ * Deterministic door / window marks per level — WS3.
4
+ *
5
+ * `packages/core/src/schema/nodes/door.ts:75-79` promises "the deterministic
6
+ * level fallback (101, 102, ...)"; this is the implementation.
7
+ *
8
+ * Numbering
9
+ * ---------
10
+ * base = (level.level + 1) * 100 — level 0 → 100, level 1 → 200
11
+ * doors = `D${base + n}` → D101, D102 …
12
+ * windows= `W${base + n}` → W101, W102 …
13
+ *
14
+ * Order
15
+ * -----
16
+ * Openings are visited **clockwise** around the level, starting from the
17
+ * north-west-most exterior wall, exterior walls first and interior walls
18
+ * after. Clockwise is the ascending-angle sweep of the wall midpoint about
19
+ * the level centroid, anchored on the north-west diagonal (plan axes are
20
+ * x east, z south — z grows down the screen — so a screen-clockwise sweep
21
+ * is increasing atan2(z, x)). Openings on the same wall are ordered along
22
+ * the wall direction.
23
+ *
24
+ * Stability
25
+ * ---------
26
+ * An explicit `mark` on the node always wins and is never reassigned.
27
+ * `resolveMarks` also reports, in `assignments`, the marks it invented for
28
+ * nodes that had none — the caller is expected to PERSIST those back onto
29
+ * `node.mark` the first time they are resolved in an edit session
30
+ * (`persistResolvedMarks`). That is what makes numbering stable under
31
+ * insertion: once written, an existing opening keeps its number forever and
32
+ * a newly inserted opening only ever takes the next free number. Without
33
+ * the write-back, inserting a door in the middle of a wall would silently
34
+ * renumber every door after it.
35
+ */
36
+ export type OpeningMarkKind = 'door' | 'window';
37
+ export type MarkResolution = {
38
+ /** Every door and window on the level → its mark. */
39
+ marks: ReadonlyMap<string, string>;
40
+ /** Only the marks that were invented (node.mark was empty). */
41
+ assignments: ReadonlyMap<string, string>;
42
+ /** Duplicate explicit marks and other reportable defects. */
43
+ issues: readonly string[];
44
+ };
45
+ export type MarkSceneInput = {
46
+ nodes: Readonly<Record<string, AnyNode>>;
47
+ };
48
+ /**
49
+ * Resolve marks for every door and window under `levelId`.
50
+ *
51
+ * `scene` may be a `{ nodes }` snapshot or the raw node map.
52
+ */
53
+ export declare function resolveMarks(scene: MarkSceneInput | Readonly<Record<string, AnyNode>>, levelId: AnyNodeId): Map<string, string>;
54
+ export declare function resolveMarkDetail(scene: MarkSceneInput | Readonly<Record<string, AnyNode>>, levelId: AnyNodeId): MarkResolution;
55
+ /**
56
+ * Write invented marks back onto the nodes so numbering never drifts.
57
+ *
58
+ * Call once per level per edit session (the caller decides when — this
59
+ * module stays pure). `update` is typically `sceneApi.update`.
60
+ */
61
+ export declare function persistResolvedMarks(resolution: MarkResolution, update: (id: AnyNodeId, data: Record<string, unknown>) => void): number;
62
+ type Opening = (DoorNode | WindowNode) & {
63
+ type: 'door' | 'window';
64
+ };
65
+ /**
66
+ * Every door and window under the level, clockwise from the NW-most
67
+ * exterior wall, exterior walls first.
68
+ */
69
+ export declare function orderedOpenings(nodes: Readonly<Record<string, AnyNode>>, levelId: AnyNodeId): Opening[];
70
+ export {};
71
+ //# sourceMappingURL=marks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"marks.d.ts","sourceRoot":"","sources":["../../../src/lib/floorplan/marks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAuB,UAAU,EAAE,MAAM,cAAc,CAAA;AAEjG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,QAAQ,CAAA;AAE/C,MAAM,MAAM,cAAc,GAAG;IAC3B,qDAAqD;IACrD,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAClC,+DAA+D;IAC/D,WAAW,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACxC,6DAA6D;IAC7D,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CACzC,CAAA;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,cAAc,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACzD,OAAO,EAAE,SAAS,GACjB,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAErB;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,cAAc,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACzD,OAAO,EAAE,SAAS,GACjB,cAAc,CAkDhB;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,cAAc,EAC1B,MAAM,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,GAC7D,MAAM,CAOR;AAID,KAAK,OAAO,GAAG,CAAC,QAAQ,GAAG,UAAU,CAAC,GAAG;IAAE,IAAI,EAAE,MAAM,GAAG,QAAQ,CAAA;CAAE,CAAA;AAEpE;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,OAAO,EAAE,SAAS,GACjB,OAAO,EAAE,CAuCX"}
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Resolve marks for every door and window under `levelId`.
3
+ *
4
+ * `scene` may be a `{ nodes }` snapshot or the raw node map.
5
+ */
6
+ export function resolveMarks(scene, levelId) {
7
+ return new Map(resolveMarkDetail(scene, levelId).marks);
8
+ }
9
+ export function resolveMarkDetail(scene, levelId) {
10
+ const nodes = normalizeNodes(scene);
11
+ const level = nodes[levelId];
12
+ const ordinal = level && level.type === 'level' ? (level.level ?? 0) : 0;
13
+ const base = (Math.max(0, ordinal) + 1) * 100;
14
+ const openings = orderedOpenings(nodes, levelId);
15
+ const marks = new Map();
16
+ const assignments = new Map();
17
+ const issues = [];
18
+ for (const kind of ['door', 'window']) {
19
+ const prefix = kind === 'door' ? 'D' : 'W';
20
+ const list = openings.filter((opening) => opening.type === kind);
21
+ const used = new Set();
22
+ const explicitOwners = new Map();
23
+ for (const opening of list) {
24
+ const explicit = opening.mark?.trim();
25
+ if (!explicit)
26
+ continue;
27
+ marks.set(opening.id, explicit);
28
+ const normalized = explicit.toLocaleUpperCase();
29
+ used.add(normalized);
30
+ const owners = explicitOwners.get(normalized);
31
+ if (owners)
32
+ owners.push(opening.id);
33
+ else
34
+ explicitOwners.set(normalized, [opening.id]);
35
+ }
36
+ let sequence = 1;
37
+ for (const opening of list) {
38
+ if (marks.has(opening.id))
39
+ continue;
40
+ let candidate = `${prefix}${base + sequence}`;
41
+ while (used.has(candidate.toLocaleUpperCase())) {
42
+ sequence++;
43
+ candidate = `${prefix}${base + sequence}`;
44
+ }
45
+ marks.set(opening.id, candidate);
46
+ assignments.set(opening.id, candidate);
47
+ used.add(candidate.toLocaleUpperCase());
48
+ sequence++;
49
+ }
50
+ for (const [mark, owners] of explicitOwners) {
51
+ if (owners.length > 1) {
52
+ issues.push(`Duplicate ${kind} mark ${mark} (${owners.length} instances)`);
53
+ }
54
+ }
55
+ }
56
+ return { marks, assignments, issues };
57
+ }
58
+ /**
59
+ * Write invented marks back onto the nodes so numbering never drifts.
60
+ *
61
+ * Call once per level per edit session (the caller decides when — this
62
+ * module stays pure). `update` is typically `sceneApi.update`.
63
+ */
64
+ export function persistResolvedMarks(resolution, update) {
65
+ let written = 0;
66
+ for (const [id, mark] of resolution.assignments) {
67
+ update(id, { mark });
68
+ written++;
69
+ }
70
+ return written;
71
+ }
72
+ /**
73
+ * Every door and window under the level, clockwise from the NW-most
74
+ * exterior wall, exterior walls first.
75
+ */
76
+ export function orderedOpenings(nodes, levelId) {
77
+ const walls = [];
78
+ const orphanOpenings = [];
79
+ const visit = (id) => {
80
+ const node = nodes[id];
81
+ if (!node)
82
+ return;
83
+ if (node.visible === false)
84
+ return;
85
+ if (node.type === 'wall')
86
+ walls.push(node);
87
+ else if (node.type === 'door' || node.type === 'window') {
88
+ const opening = node;
89
+ if (!opening.wallId || !nodes[opening.wallId])
90
+ orphanOpenings.push(opening);
91
+ }
92
+ for (const childId of node.children ?? [])
93
+ visit(childId);
94
+ };
95
+ visit(levelId);
96
+ const centroid = wallCentroid(walls);
97
+ const ordered = [...walls].sort((left, right) => {
98
+ const leftExterior = isExteriorWall(left) ? 0 : 1;
99
+ const rightExterior = isExteriorWall(right) ? 0 : 1;
100
+ if (leftExterior !== rightExterior)
101
+ return leftExterior - rightExterior;
102
+ return clockwiseKey(left, centroid) - clockwiseKey(right, centroid);
103
+ });
104
+ const result = [];
105
+ for (const wall of ordered) {
106
+ const wallOpenings = [];
107
+ for (const childId of wall.children ?? []) {
108
+ const child = nodes[childId];
109
+ if (child && child.visible !== false && (child.type === 'door' || child.type === 'window')) {
110
+ wallOpenings.push(child);
111
+ }
112
+ }
113
+ wallOpenings.sort((left, right) => left.position[0] - right.position[0]);
114
+ result.push(...wallOpenings);
115
+ }
116
+ result.push(...orphanOpenings);
117
+ return result;
118
+ }
119
+ function wallCentroid(walls) {
120
+ if (walls.length === 0)
121
+ return [0, 0];
122
+ let x = 0;
123
+ let z = 0;
124
+ for (const wall of walls) {
125
+ x += (wall.start[0] + wall.end[0]) / 2;
126
+ z += (wall.start[1] + wall.end[1]) / 2;
127
+ }
128
+ return [x / walls.length, z / walls.length];
129
+ }
130
+ /**
131
+ * Sweep key: 0 at the north-west direction, increasing clockwise on screen.
132
+ *
133
+ * Plan axes are x east, z south — z grows DOWN the screen — so a clockwise
134
+ * screen sweep (up → right → down → left) is the INCREASING atan2(z, x)
135
+ * direction. The north-west diagonal sits at atan2(-1, -1) = -3π/4, so
136
+ * anchoring there and wrapping into [0, 2π) yields north, east, south, west.
137
+ */
138
+ const NORTH_WEST_ANGLE = (-3 * Math.PI) / 4;
139
+ function clockwiseKey(wall, centroid) {
140
+ const midX = (wall.start[0] + wall.end[0]) / 2 - centroid[0];
141
+ const midZ = (wall.start[1] + wall.end[1]) / 2 - centroid[1];
142
+ let sweep = Math.atan2(midZ, midX) - NORTH_WEST_ANGLE;
143
+ while (sweep < 0)
144
+ sweep += Math.PI * 2;
145
+ while (sweep >= Math.PI * 2)
146
+ sweep -= Math.PI * 2;
147
+ return sweep;
148
+ }
149
+ function isExteriorWall(wall) {
150
+ return wall.frontSide === 'exterior' || wall.backSide === 'exterior';
151
+ }
152
+ function normalizeNodes(scene) {
153
+ const candidate = scene;
154
+ return candidate && typeof candidate === 'object' && 'nodes' in candidate
155
+ ? candidate.nodes
156
+ : scene;
157
+ }
@@ -0,0 +1,18 @@
1
+ import { type WallJustification } from '../../systems/wall/wall-frame.js';
2
+ import type { Point2D } from '../../systems/wall/wall-mitering.js';
3
+ export declare const FLOORPLAN_VIEW_ROTATION_DEG = 0;
4
+ export declare function clampPlanValue(value: number, min: number, max: number): number;
5
+ export declare function rotatePlanVector(x: number, y: number, rotation: number): [number, number];
6
+ export declare function worldToFloorplanLocalPoint(worldX: number, worldZ: number, buildingPosition: readonly [number, number, number], buildingRotationY: number): Point2D;
7
+ export declare function floorplanLocalToWorldPoint(point: Point2D | [number, number], buildingPosition: readonly [number, number, number], buildingRotationY: number): {
8
+ x: number;
9
+ z: number;
10
+ };
11
+ export declare function interpolatePlanPoint(start: Point2D, end: Point2D, t: number): Point2D;
12
+ export declare function getPlanPointDistance(start: Point2D, end: Point2D): number;
13
+ export declare function movePlanPointTowards(start: Point2D, end: Point2D, distance: number): Point2D;
14
+ export declare function getThickPlanLinePolygon(line: {
15
+ start: Point2D;
16
+ end: Point2D;
17
+ }, thickness: number, justification?: WallJustification): Point2D[];
18
+ //# sourceMappingURL=plan-math.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan-math.d.ts","sourceRoot":"","sources":["../../../src/lib/floorplan/plan-math.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,iBAAiB,EAAE,MAAM,+BAA+B,CAAA;AAC1F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAA;AAY/D,eAAO,MAAM,2BAA2B,IAAI,CAAA;AAE5C,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,UAErE;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAIzF;AAQD,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,gBAAgB,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EACnD,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAUT;AAGD,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,OAAO,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EACjC,gBAAgB,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EACnD,iBAAiB,EAAE,MAAM,GACxB;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAU1B;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAKrF;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,GAAG,MAAM,CAEzE;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAO5F;AAED,wBAAgB,uBAAuB,CACrC,IAAI,EAAE;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,OAAO,CAAA;CAAE,EACtC,SAAS,EAAE,MAAM,EACjB,aAAa,CAAC,EAAE,iBAAiB,GAChC,OAAO,EAAE,CAqBX"}