mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 12:18:51 +00:00
fix: make plan visuals grid-scale invariant
Issue: #239 User-Visible: yes
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
/** Historical plan-cell size whose raw SVG constants define the visual baseline. */
|
||||
export const GRID_VISUAL_REFERENCE_CELL_CM = 5;
|
||||
|
||||
/** Canonical centimetres in one imperial grid cell (one inch). */
|
||||
export const GRID_IMPERIAL_CELL_CM = 2.54;
|
||||
|
||||
/** Scale a legacy SVG visual unit without changing physical or screen-space sizes. */
|
||||
export function gridVisualScale(cellCm: unknown): number {
|
||||
const value = typeof cellCm === 'number' ? cellCm : NaN;
|
||||
if (!Number.isFinite(value) || value <= 0) return 1;
|
||||
if (value === GRID_VISUAL_REFERENCE_CELL_CM) return 1;
|
||||
return GRID_VISUAL_REFERENCE_CELL_CM / value;
|
||||
}
|
||||
|
||||
export function gridVisualUnits(baseUnits: number, cellCm: unknown): number {
|
||||
return baseUnits * gridVisualScale(cellCm);
|
||||
}
|
||||
|
||||
/** Default only for newly-created spaces. Read compatibility keeps its own 5 cm fallback. */
|
||||
export function newSpaceCellCm(imperial: boolean): number {
|
||||
return imperial ? GRID_IMPERIAL_CELL_CM : 1;
|
||||
}
|
||||
|
||||
/** Human-sized field projection; the canonical draft remains separate and lossless. */
|
||||
export function gridCellFieldValue(cellCm: number, imperial: boolean): string {
|
||||
if (!imperial) return String(cellCm);
|
||||
const inches = cellCm / GRID_IMPERIAL_CELL_CM;
|
||||
return String(Math.round(inches * 1_000_000) / 1_000_000);
|
||||
}
|
||||
|
||||
export function gridCellFieldToCm(value: number, imperial: boolean): number {
|
||||
return imperial ? value * GRID_IMPERIAL_CELL_CM : value;
|
||||
}
|
||||
+80
-35
@@ -271,6 +271,9 @@ import {
|
||||
type OpeningDimension, type OpeningDimensionContext,
|
||||
} from './opening-dimensions';
|
||||
import { safeStoredColor } from './color';
|
||||
import {
|
||||
gridCellFieldToCm, gridCellFieldValue, gridVisualScale, gridVisualUnits, newSpaceCellCm,
|
||||
} from './grid-scale';
|
||||
import {
|
||||
applySpaceOrder, canStartTabDrag, markersNeedingPlacement, passedDragThreshold,
|
||||
reorderSpaceIds,
|
||||
@@ -1849,6 +1852,9 @@ class HouseplanCard extends LitElement {
|
||||
labelLqi: boolean;
|
||||
labelLight: boolean;
|
||||
cellCm: number; // real-world cm represented by one grid cell
|
||||
/** Projected text is separate so an untouched imperial edit stays lossless. */
|
||||
cellCmInput?: string;
|
||||
cellCmTouched?: boolean;
|
||||
busy: boolean;
|
||||
} | null = null;
|
||||
private _keyHandler = (e: KeyboardEvent) => this._onKey(e);
|
||||
@@ -5171,12 +5177,14 @@ class HouseplanCard extends LitElement {
|
||||
flipH: !!opening.flip_h,
|
||||
flipV: !!opening.flip_v,
|
||||
}));
|
||||
const wallHeight = gridVisualUnits(ISO_WALL_HEIGHT, this._cellCm);
|
||||
const floorEdgeHeight = gridVisualUnits(ISO_FLOOR_EDGE_HEIGHT, this._cellCm);
|
||||
const key = `${space.id}|${isoGeometryFingerprint({
|
||||
rooms: space.rooms, walls, openCuts, openings,
|
||||
partitions: space.partitions, roomDrafts: space.room_drafts, columns: space.wall_columns,
|
||||
cellCm: this._cellCm, gridPitch: this._gridPitch, wallKeyPitch: this._wallKeyPitch,
|
||||
camera: ISO_CAMERA, wallHeight: ISO_WALL_HEIGHT,
|
||||
floorEdgeHeight: ISO_FLOOR_EDGE_HEIGHT, algorithm: 3,
|
||||
camera: ISO_CAMERA, wallHeight,
|
||||
floorEdgeHeight, algorithm: 3,
|
||||
})}`;
|
||||
return {
|
||||
key,
|
||||
@@ -5217,7 +5225,7 @@ class HouseplanCard extends LitElement {
|
||||
flip_v: faceFlipV,
|
||||
})
|
||||
: { ox: 0, oy: 0, cm: 0, side: -1 as -1 | 1 };
|
||||
return buildIsoOpeningBasis({ ...opening, face });
|
||||
return buildIsoOpeningBasis({ ...opening, face }, wallHeight);
|
||||
});
|
||||
return { walls: united?.geom || [], floor, openings: Object.freeze(openingBases) };
|
||||
},
|
||||
@@ -5242,16 +5250,18 @@ class HouseplanCard extends LitElement {
|
||||
if (cached) return { key: source.key, ...cached };
|
||||
const flat = this._frameOf().rect;
|
||||
const structural = source.build();
|
||||
const wallHeight = gridVisualUnits(ISO_WALL_HEIGHT, this._cellCm);
|
||||
const floorEdgeHeight = gridVisualUnits(ISO_FLOOR_EDGE_HEIGHT, this._cellCm);
|
||||
const openingFrame = isoOpeningBounds(structural.openings);
|
||||
const structuralFrame = openingFrame ? unionRect(flat, openingFrame) : flat;
|
||||
const frame = projectedFrame({
|
||||
rect: structuralFrame,
|
||||
wallHeight: ISO_WALL_HEIGHT,
|
||||
openingHeight: ISO_WALL_HEIGHT,
|
||||
floorDepth: ISO_FLOOR_EDGE_HEIGHT,
|
||||
wallHeight,
|
||||
openingHeight: wallHeight,
|
||||
floorDepth: floorEdgeHeight,
|
||||
});
|
||||
const geometry = buildIsoWallGeometry(structural.walls);
|
||||
const floor = buildIsoFloorGeometry(structural.floor, ISO_FLOOR_EDGE_HEIGHT);
|
||||
const geometry = buildIsoWallGeometry(structural.walls, ISO_CAMERA, wallHeight);
|
||||
const floor = buildIsoFloorGeometry(structural.floor, floorEdgeHeight);
|
||||
const value = { geometry, floor, openings: structural.openings, frame };
|
||||
lruWrite(this._isoGeometryCache, source.key, value, 8);
|
||||
return { key: source.key, ...value };
|
||||
@@ -5293,7 +5303,10 @@ class HouseplanCard extends LitElement {
|
||||
// otherwise merely hiding volume reframes every live floor pixel.
|
||||
if (!this._spaceDisplayForRender().showBorders) {
|
||||
const flat = this._frameOf().rect;
|
||||
const frame = projectedFrame({ rect: flat, wallHeight: ISO_WALL_HEIGHT });
|
||||
const frame = projectedFrame({
|
||||
rect: flat,
|
||||
wallHeight: gridVisualUnits(ISO_WALL_HEIGHT, this._cellCm),
|
||||
});
|
||||
return [frame.x, frame.y, frame.w, frame.h];
|
||||
}
|
||||
const frame = this._isoScene()?.frame ?? this._frameOf().rect;
|
||||
@@ -11622,7 +11635,8 @@ class HouseplanCard extends LitElement {
|
||||
<path class="wallbody ${solid ? 'solid' : ''}"
|
||||
data-hp="wall" data-id="union" data-kind="union"
|
||||
d="${united.d}" fill="${solid ? 'none' : 'url(#hp-wall-hatch)'}" fill-rule=${united.fillRule}
|
||||
stroke="${stroke}" stroke-width="0.6" pointer-events="none"></path>
|
||||
stroke="${stroke}" stroke-width="${gridVisualUnits(0.6, this._cellCm)}"
|
||||
pointer-events="none"></path>
|
||||
</g>` as unknown as TemplateResult;
|
||||
}
|
||||
|
||||
@@ -11656,6 +11670,7 @@ class HouseplanCard extends LitElement {
|
||||
layers: IsoDecorationLayers,
|
||||
root: 'underlay' | 'shadows' | 'walls',
|
||||
): TemplateResult {
|
||||
const visualScale = gridVisualScale(this._cellCm);
|
||||
return svg`<defs>
|
||||
${root === 'walls' && layers.materialNuance ? svg`
|
||||
<linearGradient id="hp-iso-wall-side" x1="0" y1="0" x2="0" y2="1">
|
||||
@@ -11666,14 +11681,14 @@ class HouseplanCard extends LitElement {
|
||||
</linearGradient>` : nothing}
|
||||
${root === 'underlay' && layers.shadows ? svg`
|
||||
<filter id="hp-iso-ambient-shadow" x="-12%" y="-12%" width="124%" height="130%">
|
||||
<feGaussianBlur stdDeviation="7"></feGaussianBlur>
|
||||
<feGaussianBlur stdDeviation="${7 * visualScale}"></feGaussianBlur>
|
||||
</filter>` : nothing}
|
||||
${root === 'shadows' && layers.shadows ? svg`
|
||||
<filter id="hp-iso-contact-shadow" x="-8%" y="-20%" width="116%" height="140%">
|
||||
<feGaussianBlur stdDeviation="2.5"></feGaussianBlur>
|
||||
<feGaussianBlur stdDeviation="${2.5 * visualScale}"></feGaussianBlur>
|
||||
</filter>
|
||||
<filter id="hp-iso-leaf-shadow" x="-12%" y="-30%" width="124%" height="160%">
|
||||
<feGaussianBlur stdDeviation="2"></feGaussianBlur>
|
||||
<feGaussianBlur stdDeviation="${2 * visualScale}"></feGaussianBlur>
|
||||
</filter>` : nothing}
|
||||
</defs>` as unknown as TemplateResult;
|
||||
}
|
||||
@@ -11685,7 +11700,8 @@ class HouseplanCard extends LitElement {
|
||||
return svg`<g class="iso-underlay" data-hp="iso-underlay" aria-hidden="true" pointer-events="none">
|
||||
${this._renderIsoDefs(layers, 'underlay')}
|
||||
${layers.shadows && floor.footprintPath
|
||||
? svg`<path class="iso-ambient-shadow" d=${floor.footprintPath} transform="translate(0 8)"></path>`
|
||||
? svg`<path class="iso-ambient-shadow" d=${floor.footprintPath}
|
||||
transform="translate(0 ${gridVisualUnits(8, this._cellCm)})"></path>`
|
||||
: nothing}
|
||||
<g class="iso-floor-edge">${floor.sides.map((face) =>
|
||||
svg`<path class="iso-floor-side" d=${face.d} data-component=${face.component}
|
||||
@@ -11915,7 +11931,7 @@ class HouseplanCard extends LitElement {
|
||||
const rad = o.angle * Math.PI / 180;
|
||||
const dx = raw[0] - o.rx, dy = raw[1] - o.ry;
|
||||
const localX = dx * Math.cos(rad) + dy * Math.sin(rad);
|
||||
if (Math.abs(localX) > o.rlen / 2 + 12) return [];
|
||||
if (Math.abs(localX) > o.rlen / 2 + gridVisualUnits(12, this._cellCm)) return [];
|
||||
return [{ o, localY: -dx * Math.sin(rad) + dy * Math.cos(rad) }];
|
||||
});
|
||||
if (!roughHits.length) return null;
|
||||
@@ -14066,6 +14082,10 @@ class HouseplanCard extends LitElement {
|
||||
labelTemp: disp.labelTemp, labelHum: disp.labelHum,
|
||||
labelLqi: disp.labelLqi, labelLight: disp.labelLight,
|
||||
cellCm: Number(sp.cell_cm) > 0 ? Number(sp.cell_cm) : 5,
|
||||
cellCmInput: gridCellFieldValue(
|
||||
Number(sp.cell_cm) > 0 ? Number(sp.cell_cm) : 5, this._imperial,
|
||||
),
|
||||
cellCmTouched: false,
|
||||
busy: false,
|
||||
};
|
||||
} else {
|
||||
@@ -14085,7 +14105,9 @@ class HouseplanCard extends LitElement {
|
||||
showLqi: this._config?.show_signal ?? true,
|
||||
cardFontScale: 1,
|
||||
labelTemp: false, labelHum: false, labelLqi: false, labelLight: false,
|
||||
cellCm: 5,
|
||||
cellCm: newSpaceCellCm(this._imperial),
|
||||
cellCmInput: gridCellFieldValue(newSpaceCellCm(this._imperial), this._imperial),
|
||||
cellCmTouched: false,
|
||||
busy: false,
|
||||
};
|
||||
}
|
||||
@@ -14456,7 +14478,9 @@ class HouseplanCard extends LitElement {
|
||||
showLqi: this._config?.show_signal ?? true,
|
||||
cardFontScale: 1,
|
||||
labelTemp: false, labelHum: false, labelLqi: false, labelLight: false,
|
||||
cellCm: 5,
|
||||
cellCm: newSpaceCellCm(this._imperial),
|
||||
cellCmInput: gridCellFieldValue(newSpaceCellCm(this._imperial), this._imperial),
|
||||
cellCmTouched: false,
|
||||
busy: false,
|
||||
};
|
||||
}
|
||||
@@ -16468,7 +16492,7 @@ class HouseplanCard extends LitElement {
|
||||
|
||||
<div class="stage ${this._markup ? 'markup tool-' + this._tool + (this._tool === 'split' && !this._splitSel ? ' pickstage' : '') + (this._tool === 'boundary' ? this._boundaryStageClass : '') + (this._tool === 'wallthick' && this._wallThickHover ? ' wallhot' : '') : ''} ${this._mode === 'decor' ? 'dtool-' + this._decorTool : ''} ${space.bg ? '' : 'noplan'} mode-${this._mode}${this._bdMovable ? ' bdgrab' : ''}${this._bdDrag ? ' bdgrabbing' : ''}${dayCycle ? ` daycycle phase-${dayCycle.phase}` : ''}${this._booting ? ' hpboot' : ''}${this._bootSoft ? ' hpsettle' : ''}${this._modeTransitionBusy ? ' mode-transition' : ''}"
|
||||
?inert=${this._modeTransitionBusy}
|
||||
style="height:${modeVisual ? `${modeVisual.stageHeight}px` : this._kiosk ? '100dvh' : `calc(100dvh - ${this._hdrH}px)`}${transitionStageBg ? `;background:${transitionStageBg}` : ''};--wall-fill:${this._fillColors.wall_fill.c};--wall-fill-op:${this._fillColors.wall_fill.a};--hp-mode-architecture-opacity:${modeVisual ? modeVisual.architectureOpacity : this._mode === 'decor' ? 0.35 : 1};--hp-mode-view-weight:${modeVisual?.viewWeight ?? (this._mode === 'view' ? 1 : 0)};--hp-mode-editor-weight:${modeVisual?.editorWeight ?? (this._mode === 'view' ? 0 : 1)}${modeVisual ? `;--hp-mode-paper:${modeVisual.paperColor}` : ''}${dayCycle ? `;${dayCycleStageVars(dayCycle)}` : ''}"
|
||||
style="height:${modeVisual ? `${modeVisual.stageHeight}px` : this._kiosk ? '100dvh' : `calc(100dvh - ${this._hdrH}px)`}${transitionStageBg ? `;background:${transitionStageBg}` : ''};--hp-cell-visual-scale:${gridVisualScale(this._cellCm)};--wall-fill:${this._fillColors.wall_fill.c};--wall-fill-op:${this._fillColors.wall_fill.a};--hp-mode-architecture-opacity:${modeVisual ? modeVisual.architectureOpacity : this._mode === 'decor' ? 0.35 : 1};--hp-mode-view-weight:${modeVisual?.viewWeight ?? (this._mode === 'view' ? 1 : 0)};--hp-mode-editor-weight:${modeVisual?.editorWeight ?? (this._mode === 'view' ? 0 : 1)}${modeVisual ? `;--hp-mode-paper:${modeVisual.paperColor}` : ''}${dayCycle ? `;${dayCycleStageVars(dayCycle)}` : ''}"
|
||||
@click=${(e: MouseEvent) => this._markupClick(e)}
|
||||
@wheel=${(e: WheelEvent) => this._onWheel(e)}
|
||||
@pointerdown=${(e: PointerEvent) => { this._notePointer(e); this._stagePointerDown(e); }}
|
||||
@@ -18300,7 +18324,8 @@ class HouseplanCard extends LitElement {
|
||||
? [gd.at, gd.from[1], gd.at, pt[1] + Math.sign(pt[1] - gd.from[1]) * over]
|
||||
: [gd.from[0], gd.at, pt[0] + Math.sign(pt[0] - gd.from[0]) * over, gd.at];
|
||||
return svg`<line class="alignline" x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}"></line>
|
||||
<circle class="aligndot" cx="${gd.from[0]}" cy="${gd.from[1]}" r="${g * 0.18}"></circle>`;
|
||||
<circle class="aligndot" cx="${gd.from[0]}" cy="${gd.from[1]}"
|
||||
r="${gridVisualUnits(g * 0.18, this._cellCm)}"></circle>`;
|
||||
})}
|
||||
</g>` as unknown as TemplateResult;
|
||||
}
|
||||
@@ -18310,7 +18335,7 @@ class HouseplanCard extends LitElement {
|
||||
* about the wall stroke (2.5) × 6 to each side. */
|
||||
private _renderOpeningCenterTick(gd: { x: number; y: number; angle: number }): TemplateResult {
|
||||
const rad = ((gd.angle + 90) * Math.PI) / 180;
|
||||
const half = 2.5 * 6;
|
||||
const half = gridVisualUnits(2.5 * 6, this._cellCm);
|
||||
return svg`<line class="alignline opcentertick"
|
||||
x1="${gd.x - Math.cos(rad) * half}" y1="${gd.y - Math.sin(rad) * half}"
|
||||
x2="${gd.x + Math.cos(rad) * half}" y2="${gd.y + Math.sin(rad) * half}"></line>` as unknown as TemplateResult;
|
||||
@@ -18402,7 +18427,9 @@ class HouseplanCard extends LitElement {
|
||||
face: candidate.face,
|
||||
};
|
||||
const passageGeometry = candidate.type === 'passage'
|
||||
? passagePlacementPreviewGeometry(candidate, this._gridPitch)
|
||||
? passagePlacementPreviewGeometry(
|
||||
candidate, gridVisualUnits(this._gridPitch, this._cellCm),
|
||||
)
|
||||
: null;
|
||||
return svg`<g class="opening-preview" data-kind=${candidate.type}
|
||||
aria-hidden="true" pointer-events="none"
|
||||
@@ -18418,7 +18445,8 @@ class HouseplanCard extends LitElement {
|
||||
` : renderOpeningVisibleGeometry(visibleSpec)}
|
||||
</g>
|
||||
<circle class="opening-preview-dot opghost-dot" aria-hidden="true" pointer-events="none"
|
||||
cx=${candidate.x} cy=${candidate.y} r=${this._gridPitch * 0.18}></circle>` as unknown as TemplateResult;
|
||||
cx=${candidate.x} cy=${candidate.y}
|
||||
r=${gridVisualUnits(this._gridPitch * 0.18, this._cellCm)}></circle>` as unknown as TemplateResult;
|
||||
}
|
||||
|
||||
private _renderOpenings(disp: SpaceDisplay): TemplateResult {
|
||||
@@ -18436,7 +18464,7 @@ class HouseplanCard extends LitElement {
|
||||
aria-label=${this._t('opening.partition_orphan')}
|
||||
transform="translate(${o.rx} ${o.ry})"
|
||||
@click=${(event: MouseEvent) => { event.stopPropagation(); this._editOpening(o); }}>
|
||||
<circle r=${this._gridPitch * 0.55}></circle>
|
||||
<circle r=${gridVisualUnits(this._gridPitch * 0.55, this._cellCm)}></circle>
|
||||
<text text-anchor="middle" dominant-baseline="central">!</text>
|
||||
</g>`;
|
||||
const amt = this._openingAmt(o);
|
||||
@@ -18466,11 +18494,16 @@ class HouseplanCard extends LitElement {
|
||||
face,
|
||||
};
|
||||
const { half, outlineHalf, hitHalf } = openingVisibleMetrics(visibleSpec);
|
||||
const outlinePad = gridVisualUnits(10, this._cellCm);
|
||||
const hitPad = gridVisualUnits(12, this._cellCm);
|
||||
return svg`<g class="opening" data-hp="opening" data-id="${o.id}" data-kind="${o.type}"
|
||||
transform="translate(${o.rx} ${o.ry}) rotate(${o.angle})">
|
||||
${renderOpeningVisibleGeometry(visibleSpec)}
|
||||
<rect class="op-outline" x="${-half - 10}" y="${-outlineHalf}" width="${o.rlen + 20}" height="${outlineHalf * 2}" rx="6"></rect>
|
||||
<rect class="op-hit" x="${-half - 12}" y="${-hitHalf}" width="${o.rlen + 24}" height="${hitHalf * 2}"
|
||||
<rect class="op-outline" x="${-half - outlinePad}" y="${-outlineHalf}"
|
||||
width="${o.rlen + outlinePad * 2}" height="${outlineHalf * 2}"
|
||||
rx="${gridVisualUnits(6, this._cellCm)}"></rect>
|
||||
<rect class="op-hit" x="${-half - hitPad}" y="${-hitHalf}"
|
||||
width="${o.rlen + hitPad * 2}" height="${hitHalf * 2}"
|
||||
@click=${(e: MouseEvent) => this._opClick(e, o)}
|
||||
@pointerdown=${(e: PointerEvent) => this._opPointerDown(e, o)}
|
||||
@pointermove=${(e: PointerEvent) => this._opPointerMove(e, o)}
|
||||
@@ -18502,7 +18535,8 @@ class HouseplanCard extends LitElement {
|
||||
const gateFace = o.type === 'gate'
|
||||
? this._openingFace(o, openingWallIndex, !o.flip_v)
|
||||
: null;
|
||||
const off = gateFace ? -16 * gateFace.side : 16 * (o.flip_v ? -1 : 1);
|
||||
const lockOffset = gridVisualUnits(16, this._cellCm);
|
||||
const off = gateFace ? -lockOffset * gateFace.side : lockOffset * (o.flip_v ? -1 : 1);
|
||||
const px = o.rx + Math.cos(rad) * off;
|
||||
const py = o.ry + Math.sin(rad) * off;
|
||||
const point = this._scenePoint([px, py]);
|
||||
@@ -19081,14 +19115,16 @@ class HouseplanCard extends LitElement {
|
||||
x2="${this._cursorPt[0]}" y2="${this._cursorPt[1]}" aria-hidden="true"></line>
|
||||
${!this._activePlanSnapCandidate && !this._activePlanSnapConflicts.length
|
||||
? svg`<circle class="active-vertex" cx="${this._cursorPt[0]}" cy="${this._cursorPt[1]}"
|
||||
r="${g * 0.22}" aria-hidden="true"></circle>` : nothing}`
|
||||
r="${gridVisualUnits(g * 0.22, this._cellCm)}" aria-hidden="true"></circle>` : nothing}`
|
||||
: nothing}
|
||||
${path.map((p, i) => svg`<circle class="vertex ${i === 0 ? 'first' : ''}" cx="${p[0]}" cy="${p[1]}" r="${g * 0.22}"></circle>`)}
|
||||
${path.map((p, i) => svg`<circle class="vertex ${i === 0 ? 'first' : ''}"
|
||||
cx="${p[0]}" cy="${p[1]}" r="${gridVisualUnits(g * 0.22, this._cellCm)}"></circle>`)}
|
||||
${this._tool === 'split' && this._splitSel?.pts?.length
|
||||
? svg`${this._splitSel.pts.length > 1
|
||||
? svg`<polyline class="pathline" points="${this._splitSel.pts.map((p) => p.join(',')).join(' ')}"></polyline>`
|
||||
: nothing}
|
||||
${this._splitSel.pts.map((p, i) => svg`<circle class="vertex ${i === 0 ? 'first' : ''}" cx="${p[0]}" cy="${p[1]}" r="${g * 0.22}"></circle>`)}
|
||||
${this._splitSel.pts.map((p, i) => svg`<circle class="vertex ${i === 0 ? 'first' : ''}"
|
||||
cx="${p[0]}" cy="${p[1]}" r="${gridVisualUnits(g * 0.22, this._cellCm)}"></circle>`)}
|
||||
${this._cursorPt
|
||||
? svg`<line class="preview" x1="${this._splitSel.pts[this._splitSel.pts.length - 1][0]}" y1="${this._splitSel.pts[this._splitSel.pts.length - 1][1]}"
|
||||
x2="${this._cursorPt[0]}" y2="${this._cursorPt[1]}"></line>`
|
||||
@@ -20589,16 +20625,25 @@ class HouseplanCard extends LitElement {
|
||||
|
||||
<label>${this._t('space.scale_label')}</label>
|
||||
<div class="colorrow">
|
||||
<input class="namein tempin" type="number" min=${CELL_CM_MIN} max=${CELL_CM_MAX}
|
||||
step="0.1" .value=${String(d.cellCm)}
|
||||
<input class="namein tempin" type="number"
|
||||
min=${gridCellFieldValue(CELL_CM_MIN, this._imperial)}
|
||||
max=${gridCellFieldValue(CELL_CM_MAX, this._imperial)}
|
||||
step="0.1" .value=${d.cellCmInput ?? gridCellFieldValue(d.cellCm, this._imperial)}
|
||||
@input=${(e: Event) => {
|
||||
const n = strictNumber((e.target as HTMLInputElement).value);
|
||||
const raw = (e.target as HTMLInputElement).value;
|
||||
const n = strictNumber(raw);
|
||||
const canonical = n == null ? null : gridCellFieldToCm(n, this._imperial);
|
||||
this._spaceDialog = {
|
||||
...d, cellCm: n != null && n > 0
|
||||
? Math.max(CELL_CM_MIN, Math.min(CELL_CM_MAX, n)) : d.cellCm,
|
||||
...d,
|
||||
cellCmInput: raw,
|
||||
cellCmTouched: true,
|
||||
cellCm: canonical != null && canonical > 0
|
||||
? Math.max(CELL_CM_MIN, Math.min(CELL_CM_MAX, canonical)) : d.cellCm,
|
||||
};
|
||||
}} />
|
||||
<span class="opl">${this._t('space.scale_unit')}</span>
|
||||
<span class="opl">${this._t(
|
||||
this._imperial ? 'space.scale_unit_imperial' : 'space.scale_unit',
|
||||
)}</span>
|
||||
</div>
|
||||
|
||||
<label class="dispsection">${this._t('space.display_section')}</label>
|
||||
|
||||
@@ -372,6 +372,7 @@
|
||||
"btn.skip": "Skip",
|
||||
"space.scale_label": "Scale (grid cell size)",
|
||||
"space.scale_unit": "cm per cell",
|
||||
"space.scale_unit_imperial": "in per cell",
|
||||
"space.display_section": "Display",
|
||||
"space.show_borders": "Always show room borders",
|
||||
"space.show_names": "Show room names (drag to move)",
|
||||
|
||||
@@ -372,6 +372,7 @@
|
||||
"btn.skip": "Пропустить",
|
||||
"space.scale_label": "Масштаб (размер клетки сетки)",
|
||||
"space.scale_unit": "см на клетку",
|
||||
"space.scale_unit_imperial": "дюйм на клетку",
|
||||
"space.display_section": "Отображение",
|
||||
"space.show_borders": "Всегда отображать границы комнат",
|
||||
"space.show_names": "Отображать названия комнат (перетаскиваются)",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { svg, type TemplateResult } from 'lit';
|
||||
import type { OpeningCfg } from '../types';
|
||||
import { gridVisualScale, gridVisualUnits } from '../grid-scale';
|
||||
|
||||
export interface OpeningFaceOffset {
|
||||
ox: number;
|
||||
@@ -34,14 +35,22 @@ export function openingVisibleMetrics(spec: OpeningVisibleSpec): OpeningVisibleM
|
||||
const half = spec.length / 2;
|
||||
const jambHalf = spec.face.cm > 0
|
||||
? ((spec.face.cm / spec.cellCm) * spec.gridPitch) / 2
|
||||
: 4;
|
||||
: gridVisualUnits(4, spec.cellCm);
|
||||
const gateDepth = spec.type === 'gate' ? Math.sin((10 * Math.PI) / 180) * half : 0;
|
||||
return {
|
||||
half,
|
||||
jambHalf,
|
||||
gateDepth,
|
||||
outlineHalf: Math.max(16, jambHalf + 8, gateDepth + 8),
|
||||
hitHalf: Math.max(20, jambHalf + 10, gateDepth + 12),
|
||||
outlineHalf: Math.max(
|
||||
gridVisualUnits(16, spec.cellCm),
|
||||
jambHalf + gridVisualUnits(8, spec.cellCm),
|
||||
gateDepth + gridVisualUnits(8, spec.cellCm),
|
||||
),
|
||||
hitHalf: Math.max(
|
||||
gridVisualUnits(20, spec.cellCm),
|
||||
jambHalf + gridVisualUnits(10, spec.cellCm),
|
||||
gateDepth + gridVisualUnits(12, spec.cellCm),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -55,6 +64,8 @@ export function renderOpeningVisibleGeometry(spec: OpeningVisibleSpec): Template
|
||||
if (spec.type === 'passage') return svg``;
|
||||
const amount = Math.max(0, Math.min(1, spec.amount));
|
||||
const { half, jambHalf } = openingVisibleMetrics(spec);
|
||||
const visualScale = gridVisualScale(spec.cellCm);
|
||||
const leafHalf = 1.75 * visualScale;
|
||||
const sx = spec.flipH ? -1 : 1;
|
||||
const sy = spec.flipV ? -1 : 1;
|
||||
|
||||
@@ -75,7 +86,7 @@ export function renderOpeningVisibleGeometry(spec: OpeningVisibleSpec): Template
|
||||
const arcLen = (Math.PI / 2) * half;
|
||||
const glass = spec.face.cm > 0
|
||||
? svg`<line class="op-glass" x1="0" y1="${-jambHalf}" x2="0" y2="${jambHalf}"
|
||||
stroke="${spec.tone}" stroke-width="1.5"></line>`
|
||||
stroke="${spec.tone}" stroke-width="${1.5 * visualScale}"></line>`
|
||||
: svg``;
|
||||
body = svg`
|
||||
<g transform="translate(${swingTx} ${swingTy})">
|
||||
@@ -85,12 +96,12 @@ export function renderOpeningVisibleGeometry(spec: OpeningVisibleSpec): Template
|
||||
stroke="${spec.tone}" stroke-dasharray="${arcLen}" stroke-dashoffset="${arcLen * (1 - amount)}"></path>
|
||||
<g transform="translate(${-half} 0)">
|
||||
<g class="op-leaf" style="transform:rotate(${-90 * amount}deg)">
|
||||
<rect x="0" y="-1.5" width="${half}" height="3" fill="${spec.tone}"></rect>
|
||||
<rect x="0" y="${-1.5 * visualScale}" width="${half}" height="${3 * visualScale}" fill="${spec.tone}"></rect>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(${half} 0)">
|
||||
<g class="op-leaf" style="transform:rotate(${90 * amount}deg)">
|
||||
<rect x="${-half}" y="-1.5" width="${half}" height="3" fill="${spec.tone}"></rect>
|
||||
<rect x="${-half}" y="${-1.5 * visualScale}" width="${half}" height="${3 * visualScale}" fill="${spec.tone}"></rect>
|
||||
</g>
|
||||
</g>
|
||||
${glass}
|
||||
@@ -103,12 +114,12 @@ export function renderOpeningVisibleGeometry(spec: OpeningVisibleSpec): Template
|
||||
<g transform="translate(${swingTx} ${swingTy})">
|
||||
<g transform="translate(${-half} 0)">
|
||||
<g class="op-leaf" style="transform:rotate(${gateAngle}deg)">
|
||||
<rect x="0" y="-1.75" width="${half}" height="3.5" fill="${spec.tone}"></rect>
|
||||
<rect x="0" y="${-leafHalf}" width="${half}" height="${leafHalf * 2}" fill="${spec.tone}"></rect>
|
||||
</g>
|
||||
</g>
|
||||
<g transform="translate(${half} 0)">
|
||||
<g class="op-leaf" style="transform:rotate(${-gateAngle}deg)">
|
||||
<rect x="${-half}" y="-1.75" width="${half}" height="3.5" fill="${spec.tone}"></rect>
|
||||
<rect x="${-half}" y="${-leafHalf}" width="${half}" height="${leafHalf * 2}" fill="${spec.tone}"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</g>`;
|
||||
@@ -120,7 +131,7 @@ export function renderOpeningVisibleGeometry(spec: OpeningVisibleSpec): Template
|
||||
stroke="${spec.tone}" stroke-dasharray="${arcLen}" stroke-dashoffset="${arcLen * (1 - amount)}"></path>
|
||||
<g transform="translate(${-half} 0)">
|
||||
<g class="op-leaf" style="transform:rotate(${-90 * amount}deg)">
|
||||
<rect x="0" y="-1.75" width="${spec.length}" height="3.5" fill="${spec.tone}"></rect>
|
||||
<rect x="0" y="${-leafHalf}" width="${spec.length}" height="${leafHalf * 2}" fill="${spec.tone}"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</g>`;
|
||||
@@ -128,9 +139,9 @@ export function renderOpeningVisibleGeometry(spec: OpeningVisibleSpec): Template
|
||||
|
||||
return svg`<g transform="scale(${sx} ${sy})">
|
||||
<line x1="${-half}" y1="${-jambHalf}" x2="${-half}" y2="${jambHalf}"
|
||||
stroke="${spec.base}" stroke-width="2.5"></line>
|
||||
stroke="${spec.base}" stroke-width="${2.5 * visualScale}"></line>
|
||||
<line x1="${half}" y1="${-jambHalf}" x2="${half}" y2="${jambHalf}"
|
||||
stroke="${spec.base}" stroke-width="2.5"></line>
|
||||
stroke="${spec.base}" stroke-width="${2.5 * visualScale}"></line>
|
||||
${body}
|
||||
</g>`;
|
||||
}
|
||||
|
||||
+1
-1
@@ -884,7 +884,7 @@ class HouseplanSpaceCard extends LitElement {
|
||||
fill: url(#hp-wall-hatch);
|
||||
fill-rule: evenodd;
|
||||
stroke: var(--room-stroke, var(--hp-muted, #607d8b));
|
||||
stroke-width: 0.6;
|
||||
stroke-width: calc(0.6px * var(--hp-cell-visual-scale, 1));
|
||||
pointer-events: none;
|
||||
}
|
||||
.wallbody.solid {
|
||||
|
||||
+12
-2
@@ -7,6 +7,7 @@ import { declump, contentUrl } from './logic';
|
||||
import type { ServerConfig, SpaceModel, RoomCfg, DevItem, OpeningCfg } from './types';
|
||||
import { boxCorners, normalizeAngle } from './editors/decor/geometry';
|
||||
import { canonicalColumnAngle } from './physical-geometry';
|
||||
import { gridVisualScale } from './grid-scale';
|
||||
|
||||
export const NORM_W = 1000; // side of the render space — the canvas is square
|
||||
|
||||
@@ -154,6 +155,9 @@ export function spaceModels(cfg: ServerConfig | null): SpaceModel[] {
|
||||
return {
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
// Missing remains the historical 5 cm for stored-config compatibility.
|
||||
cellCm: Number.isFinite(Number(s.cell_cm)) && Number(s.cell_cm) > 0
|
||||
? Number(s.cell_cm) : 5,
|
||||
vb: [vb[0] * NORM_W, vb[1] * H, vb[2] * NORM_W, vb[3] * H],
|
||||
// the image's own placement — the centred default plus whatever the
|
||||
// backdrop frame has stored (plan_x/y, per-axis scale and angle)
|
||||
@@ -446,8 +450,14 @@ export function iconUnit(space: SpaceModel): number {
|
||||
// no business inflating the icons. Degenerate axes are still lifted off zero,
|
||||
// which cannot matter here (FLOOR < NORM_W).
|
||||
const b = contentFrame(items, { pad: 0 }).core;
|
||||
if (!b) return NORM_W;
|
||||
return Math.max(NORM_W, Math.min(SANE_LIMIT, Math.max(b.w, b.h)));
|
||||
// The historical NORM_W floor is a VISUAL size, not a physical distance.
|
||||
// A physically equivalent plan at a finer cell size stores proportionally
|
||||
// larger coordinates, so its floor must grow by the same factor. Without
|
||||
// this, icons and room labels shrink whenever one side of the plan is below
|
||||
// the old 1000-unit threshold (issue #239).
|
||||
const visualFloor = NORM_W * gridVisualScale(space.cellCm ?? 5);
|
||||
if (!b) return visualFloor;
|
||||
return Math.max(visualFloor, Math.min(SANE_LIMIT, Math.max(b.w, b.h)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+3
-2
@@ -44,6 +44,7 @@ import { valueBadgeTitle } from './device-value-badge';
|
||||
import { contentFingerprint } from './visual-continuity';
|
||||
import type { VirtualLightSnapshot } from './virtual-light-state';
|
||||
import { renderOpeningTunnelFills } from './render/opening-tunnels';
|
||||
import { gridVisualScale, gridVisualUnits } from './grid-scale';
|
||||
import {
|
||||
spaceModels, defaultPositions, markerPos, labelPos, spaceFrame, iconCqw, NORM_W,
|
||||
GRID_STEP_N, GRID_PITCH, staticPassageOpenings,
|
||||
@@ -507,7 +508,7 @@ export function renderSpaceStatic(o: StaticRenderOpts): TemplateResult | null {
|
||||
return html`
|
||||
<div class="hp-static-stage${dayCycle ? ` daycycle phase-${dayCycle.phase}` : ''}"
|
||||
?inert=${!!o.inert}
|
||||
style="aspect-ratio:${vb[2]}/${vb[3]}${stageBg ? ';background:' + stageBg : ''};--wall-fill:${colors.wall_fill.c};--wall-fill-op:${colors.wall_fill.a}${dayCycle ? `;${dayCycleStageVars(dayCycle)}` : ''}">
|
||||
style="aspect-ratio:${vb[2]}/${vb[3]}${stageBg ? ';background:' + stageBg : ''};--hp-cell-visual-scale:${gridVisualScale(cellCm)};--wall-fill:${colors.wall_fill.c};--wall-fill-op:${colors.wall_fill.a}${dayCycle ? `;${dayCycleStageVars(dayCycle)}` : ''}">
|
||||
${renderDayCycleEnvironment(dayCycle)}
|
||||
<svg viewBox="${vb[0]} ${vb[1]} ${vb[2]} ${vb[3]}" preserveAspectRatio="xMidYMid meet">
|
||||
${wallUnion ? svg`<defs>
|
||||
@@ -545,7 +546,7 @@ export function renderSpaceStatic(o: StaticRenderOpts): TemplateResult | null {
|
||||
stroke="none" pointer-events="none"></path>
|
||||
<path class="wallbody ${solidWall ? 'solid' : ''}" data-hp="wall" data-id="union" data-kind="union"
|
||||
d="${wallUnion.d}" fill="${solidWall ? 'none' : 'url(#hp-wall-hatch)'}" fill-rule=${wallUnion.fillRule}
|
||||
stroke="${wallStroke}" stroke-width="0.6" pointer-events="none"></path>
|
||||
stroke="${wallStroke}" stroke-width="${gridVisualUnits(0.6, cellCm)}" pointer-events="none"></path>
|
||||
</g>`
|
||||
: nothing}
|
||||
${hostedOpeningSymbols}
|
||||
|
||||
+39
-28
@@ -637,7 +637,7 @@ export const cardStyles = css`
|
||||
.room.overlay {
|
||||
fill: transparent;
|
||||
stroke: transparent;
|
||||
stroke-width: 2;
|
||||
stroke-width: calc(2px * var(--hp-cell-visual-scale, 1));
|
||||
}
|
||||
:host([data-pointer-hover]) .stage.mode-view .room.overlay:not(.styled):hover {
|
||||
stroke: var(--hp-accent);
|
||||
@@ -646,7 +646,7 @@ export const cardStyles = css`
|
||||
.room.yard {
|
||||
fill: rgba(75, 140, 90, 0.14);
|
||||
stroke: #4b8c5a;
|
||||
stroke-width: 2;
|
||||
stroke-width: calc(2px * var(--hp-cell-visual-scale, 1));
|
||||
}
|
||||
:host([data-pointer-hover]) .stage.mode-view .room.yard:not(.styled):hover {
|
||||
stroke: var(--hp-accent);
|
||||
@@ -655,7 +655,7 @@ export const cardStyles = css`
|
||||
.room.styled {
|
||||
stroke: var(--room-stroke, transparent);
|
||||
stroke-opacity: var(--room-stroke-op, 0);
|
||||
stroke-width: 2.5;
|
||||
stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1));
|
||||
fill: var(--room-fill, transparent);
|
||||
fill-opacity: var(--room-fill-op, 0);
|
||||
}
|
||||
@@ -705,14 +705,14 @@ export const cardStyles = css`
|
||||
transition: transform 0.6s ease;
|
||||
}
|
||||
.op-arc {
|
||||
stroke-width: 1.5;
|
||||
stroke-width: calc(1.5px * var(--hp-cell-visual-scale, 1));
|
||||
transition: stroke-dashoffset 0.6s ease;
|
||||
}
|
||||
/* hover affordance: a rounded outline hugging the wall strip + a grab cursor */
|
||||
.op-outline {
|
||||
fill: none;
|
||||
stroke: var(--hp-accent);
|
||||
stroke-width: 1.5;
|
||||
stroke-width: calc(1.5px * var(--hp-cell-visual-scale, 1));
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
pointer-events: none;
|
||||
@@ -743,12 +743,12 @@ export const cardStyles = css`
|
||||
.stage.markup .opening.orphan circle {
|
||||
fill: var(--hp-bg);
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
stroke-width: calc(2px * var(--hp-cell-visual-scale, 1));
|
||||
}
|
||||
.stage.markup .opening.orphan text {
|
||||
fill: currentColor;
|
||||
font-weight: 800;
|
||||
font-size: 12px;
|
||||
font-size: calc(12px * var(--hp-cell-visual-scale, 1));
|
||||
pointer-events: none;
|
||||
}
|
||||
/* HP-1550-04: in the resize tool the wall handles own the hit test — the
|
||||
@@ -1176,8 +1176,10 @@ export const cardStyles = css`
|
||||
}
|
||||
.alignline {
|
||||
stroke: var(--hp-accent);
|
||||
stroke-width: 1.2;
|
||||
stroke-dasharray: 4 4;
|
||||
stroke-width: calc(1.2px * var(--hp-cell-visual-scale, 1));
|
||||
stroke-dasharray:
|
||||
calc(4px * var(--hp-cell-visual-scale, 1))
|
||||
calc(4px * var(--hp-cell-visual-scale, 1));
|
||||
pointer-events: none;
|
||||
opacity: 0.9;
|
||||
}
|
||||
@@ -1301,7 +1303,9 @@ export const cardStyles = css`
|
||||
filter: drop-shadow(0 0 3px var(--hp-accent));
|
||||
}
|
||||
.decorlayer .ddraft {
|
||||
stroke-dasharray: 6 5;
|
||||
stroke-dasharray:
|
||||
calc(6px * var(--hp-cell-visual-scale, 1))
|
||||
calc(5px * var(--hp-cell-visual-scale, 1));
|
||||
pointer-events: none;
|
||||
}
|
||||
.decorlayer text {
|
||||
@@ -1479,7 +1483,7 @@ export const cardStyles = css`
|
||||
}
|
||||
.passage-preview-boundary {
|
||||
stroke: var(--hp-open, #ff9800);
|
||||
stroke-width: 2.5;
|
||||
stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1));
|
||||
pointer-events: none;
|
||||
}
|
||||
.opening-preview .op-leaf,
|
||||
@@ -1531,7 +1535,7 @@ export const cardStyles = css`
|
||||
fill: url(#hp-wall-hatch);
|
||||
fill-rule: evenodd;
|
||||
stroke: var(--room-stroke, var(--hp-muted));
|
||||
stroke-width: 0.6;
|
||||
stroke-width: calc(0.6px * var(--hp-cell-visual-scale, 1));
|
||||
pointer-events: none;
|
||||
}
|
||||
/* Thin-on-screen: hatch collapses into noise — fill alone, stroke from fill path's sibling. */
|
||||
@@ -1542,7 +1546,7 @@ export const cardStyles = css`
|
||||
fill: var(--accent-color, #03a9f4);
|
||||
fill-opacity: 0.38;
|
||||
stroke: var(--accent-color, #03a9f4);
|
||||
stroke-width: 2.5;
|
||||
stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1));
|
||||
stroke-linejoin: round;
|
||||
stroke-linecap: round;
|
||||
pointer-events: none;
|
||||
@@ -1585,8 +1589,10 @@ export const cardStyles = css`
|
||||
.stage.markup.tool-boundary.boundary-invalid { cursor: not-allowed; }
|
||||
.openwall {
|
||||
stroke: var(--ow-stroke, var(--hp-muted));
|
||||
stroke-width: 2.5;
|
||||
stroke-dasharray: 7 7;
|
||||
stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1));
|
||||
stroke-dasharray:
|
||||
calc(7px * var(--hp-cell-visual-scale, 1))
|
||||
calc(7px * var(--hp-cell-visual-scale, 1));
|
||||
stroke-linecap: butt;
|
||||
pointer-events: none;
|
||||
opacity: 0.9;
|
||||
@@ -1603,7 +1609,7 @@ export const cardStyles = css`
|
||||
(owner 2026-08-04 — no teeth anywhere on a room border). */
|
||||
.room-outline {
|
||||
fill: none;
|
||||
stroke-width: 2.5;
|
||||
stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1));
|
||||
stroke-linejoin: round;
|
||||
stroke-linecap: round;
|
||||
pointer-events: none;
|
||||
@@ -1627,7 +1633,7 @@ export const cardStyles = css`
|
||||
fill: none;
|
||||
stroke: var(--hp-accent);
|
||||
stroke-opacity: 0.28;
|
||||
stroke-width: 8;
|
||||
stroke-width: calc(8px * var(--hp-cell-visual-scale, 1));
|
||||
stroke-linejoin: round;
|
||||
stroke-linecap: round;
|
||||
pointer-events: none;
|
||||
@@ -1635,15 +1641,17 @@ export const cardStyles = css`
|
||||
.room-hover-outline {
|
||||
fill: none;
|
||||
stroke: var(--hp-accent);
|
||||
stroke-width: 3;
|
||||
stroke-width: calc(3px * var(--hp-cell-visual-scale, 1));
|
||||
stroke-linejoin: round;
|
||||
stroke-linecap: round;
|
||||
pointer-events: none;
|
||||
}
|
||||
.openwall-preview {
|
||||
stroke: #ffc14d;
|
||||
stroke-width: 5;
|
||||
stroke-dasharray: 7 7;
|
||||
stroke-width: calc(5px * var(--hp-cell-visual-scale, 1));
|
||||
stroke-dasharray:
|
||||
calc(7px * var(--hp-cell-visual-scale, 1))
|
||||
calc(7px * var(--hp-cell-visual-scale, 1));
|
||||
stroke-linecap: round;
|
||||
pointer-events: none;
|
||||
opacity: 0.95;
|
||||
@@ -1655,7 +1663,7 @@ export const cardStyles = css`
|
||||
fill: rgba(255, 193, 77, 0.28);
|
||||
fill-rule: evenodd;
|
||||
stroke: #ffc14d;
|
||||
stroke-width: 2.5;
|
||||
stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1));
|
||||
stroke-dasharray: none;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
@@ -1764,7 +1772,7 @@ export const cardStyles = css`
|
||||
/* AFTER .outlined: same specificity — source order decides (gotcha x4) */
|
||||
.room.picked {
|
||||
stroke: #ffc14d;
|
||||
stroke-width: 3;
|
||||
stroke-width: calc(3px * var(--hp-cell-visual-scale, 1));
|
||||
fill: rgba(255, 193, 77, 0.25);
|
||||
}
|
||||
/* Owner 2026-08-04: the grid is a HINT, not content — at full strength the
|
||||
@@ -1787,21 +1795,23 @@ export const cardStyles = css`
|
||||
so the round cap IS the corner (matches the finished .room border) */
|
||||
.seg {
|
||||
stroke: var(--hp-accent);
|
||||
stroke-width: 2.5;
|
||||
stroke-width: calc(2.5px * var(--hp-cell-visual-scale, 1));
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.pathline {
|
||||
stroke: #ffc14d;
|
||||
stroke-width: 3;
|
||||
stroke-width: calc(3px * var(--hp-cell-visual-scale, 1));
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.preview {
|
||||
stroke: #ffc14d;
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 6 5;
|
||||
stroke-width: calc(2px * var(--hp-cell-visual-scale, 1));
|
||||
stroke-dasharray:
|
||||
calc(6px * var(--hp-cell-visual-scale, 1))
|
||||
calc(5px * var(--hp-cell-visual-scale, 1));
|
||||
opacity: 0.7;
|
||||
}
|
||||
.active-axis {
|
||||
@@ -1838,14 +1848,14 @@ export const cardStyles = css`
|
||||
fill-opacity: 0.55;
|
||||
fill-rule: evenodd;
|
||||
stroke: var(--accent-color, #03a9f4);
|
||||
stroke-width: 0.5;
|
||||
stroke-width: calc(0.5px * var(--hp-cell-visual-scale, 1));
|
||||
stroke-opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
.vertex {
|
||||
fill: #ffc14d;
|
||||
stroke: #4a2800;
|
||||
stroke-width: 1;
|
||||
stroke-width: calc(1px * var(--hp-cell-visual-scale, 1));
|
||||
}
|
||||
.vertex.first {
|
||||
fill: #4bd28f;
|
||||
@@ -1858,6 +1868,7 @@ export const cardStyles = css`
|
||||
.plan-snap-line {
|
||||
fill: none;
|
||||
stroke: color-mix(in srgb, var(--hp-accent) 82%, white 18%);
|
||||
/* Explicit non-scaling-stroke in the SVG keeps this one screen pixel. */
|
||||
stroke-width: 1;
|
||||
stroke-linecap: round;
|
||||
opacity: 0.92;
|
||||
|
||||
@@ -65,6 +65,8 @@ export type WallColumnCfg =
|
||||
export interface SpaceModel {
|
||||
id: string;
|
||||
title: string;
|
||||
/** Canonical centimetres represented by one stored grid cell. */
|
||||
cellCm?: number;
|
||||
vb: number[]; // render units
|
||||
bg: { href: string; x: number; y: number; w: number; h: number; angle?: number } | null;
|
||||
rooms: RoomCfg[]; // render units
|
||||
|
||||
Reference in New Issue
Block a user