mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
+1
-1
@@ -75,7 +75,7 @@ export function renderDeviceFace(
|
||||
const legacyMetrics = legacySupplementalMetrics(presentation);
|
||||
return html`
|
||||
${pulse.kind !== 'none' && pulse.reducedMotionIndicator !== 'dot'
|
||||
? html`<span class="device-pulse ${pulse.kind} ${pulse.reason} reason-${pulse.reason} ${gen2 ? 'gen2' : ''}"
|
||||
? html`<span class="device-pulse activity-ring ${pulse.kind} ${pulse.reason} reason-${pulse.reason} ${gen2 ? 'gen2' : ''}"
|
||||
aria-hidden="true"><i></i><i></i><i></i></span>`
|
||||
: nothing}
|
||||
${pulse.reducedMotionIndicator === 'dot'
|
||||
|
||||
@@ -267,10 +267,10 @@ export function resolvePresentationSources(
|
||||
// More info. On a mixed device, however, an unrelated cover capability must
|
||||
// not hijack a primary/owned light unless the legacy explicit cover action
|
||||
// (or the primary entity itself) says that the marker represents the cover.
|
||||
const resolvedDeviceRole = resolvedDeviceStateEntities(hass, d.entities);
|
||||
const coverOwnsFace = !!cover && (
|
||||
d.tapAction === 'cover'
|
||||
|| !!d.primary?.startsWith('cover.')
|
||||
|| lights.length === 0
|
||||
|| resolvedDeviceRole.some((eid) => eid.startsWith('cover.'))
|
||||
);
|
||||
if (coverOwnsFace) {
|
||||
sourceKind = 'cover';
|
||||
@@ -564,8 +564,12 @@ export function resolveDevicePresentation(
|
||||
? (d.marker?.use_climate_temp === true ? climateTempFor(hass, d.entities)
|
||||
: (d.icon === 'mdi:thermometer' || d.icon === 'mdi:air-filter') ? tempFor(hass, d.entities) : null)
|
||||
: null;
|
||||
// Legacy inline humidity belongs only to a humidity-led marker. A random
|
||||
// diagnostic humidity sibling must not turn a composite device into a
|
||||
// humidity marker; users can still select that sibling explicitly through
|
||||
// the configurable value badge.
|
||||
const hum = !staticIcon && !effectiveHidden && options.showTemperature
|
||||
&& d.entities.some((eid) => isHumEntity(hass, eid))
|
||||
&& !!d.primary && isHumEntity(hass, d.primary)
|
||||
? humFor(hass, d.entities) : null;
|
||||
const lqi = !staticIcon && !effectiveHidden && options.showSignal && !d.virtual ? lqiFor(hass, d.entities) : null;
|
||||
const markerStateGraph = options.lightSources || (d.marker?.value_badge?.source?.kind === 'derived_marker_state'
|
||||
|
||||
@@ -353,13 +353,16 @@ function resolveEntity(
|
||||
}
|
||||
|
||||
function ownRoleCandidates(device: DevItem, registryHass: any): string[] {
|
||||
const leading = device.marker?.is_light === true || device.marker?.light_entity
|
||||
? forcedLightEntityOf(device)
|
||||
: null;
|
||||
if (device.bindingKind === 'entity' && device.bindingRef) {
|
||||
return [...new Set([leading, device.bindingRef].filter((eid): eid is string => !!eid))];
|
||||
// Entity bindings are exact by contract: a controllable sibling from the
|
||||
// same HA device must never replace the entity selected by the user.
|
||||
return [device.bindingRef];
|
||||
}
|
||||
const candidates = device.entities.length ? device.entities : device.allEntities || [];
|
||||
const leading = (device.marker?.light_entity
|
||||
|| (device.primary && isControllable(device.primary)))
|
||||
? forcedLightEntityOf(device)
|
||||
: null;
|
||||
return [...new Set([
|
||||
leading,
|
||||
...resolvedDeviceStateEntities(registryHass, candidates),
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/** Pure content gate shared by the component and executable unit tests. */
|
||||
export function helpHasContent(text: unknown, ariaLabel: unknown): boolean {
|
||||
return typeof text === 'string' && text.trim().length > 0
|
||||
&& typeof ariaLabel === 'string' && ariaLabel.trim().length > 0;
|
||||
}
|
||||
|
||||
/** Scrolling the floating help itself must not dismiss it. */
|
||||
export function helpScrollShouldDismiss(
|
||||
pathIsInsideFloatingSurface: boolean,
|
||||
targetIsDialogOrContainsHelp: boolean,
|
||||
): boolean {
|
||||
return !pathIsInsideFloatingSurface && targetIsDialogOrContainsHelp;
|
||||
}
|
||||
+61
-31
@@ -191,12 +191,24 @@ import {
|
||||
type OpeningFaceOffset, type OpeningVisibleSpec,
|
||||
} from './render/opening-symbol';
|
||||
import {
|
||||
openingPlacementPreset, resolveOpeningPlacement, sameOpeningPlacementInput,
|
||||
openingDefaultLengthCm, openingPlacementPreset, resolveOpeningPlacement, sameOpeningPlacementInput,
|
||||
type OpeningPlacementCore, type OpeningPlacementPreset, type OpeningPlacementType,
|
||||
} from './opening-placement';
|
||||
import { safeStoredColor } from './color';
|
||||
|
||||
const CARD_VERSION = '1.62.0-beta.8';
|
||||
const CARD_VERSION = '1.62.0-beta.9';
|
||||
const DISPLAY_LABEL_KEYS: Record<DeviceDisplayMode, I18nKey> = {
|
||||
badge: 'display.badge',
|
||||
icon_ripple: 'display.icon_ripple',
|
||||
value: 'display.value',
|
||||
static_icon: 'display.static_icon',
|
||||
};
|
||||
const DISPLAY_HINT_KEYS: Record<DeviceDisplayMode, I18nKey> = {
|
||||
badge: 'marker.display_hint_badge',
|
||||
icon_ripple: 'marker.display_hint_icon_ripple',
|
||||
value: 'marker.display_hint_value',
|
||||
static_icon: 'marker.display_hint_static_icon',
|
||||
};
|
||||
/** Keeps every previously valid scale at the maximum 20 cm grid scale lossless. */
|
||||
const DECOR_TEXT_CM_MAX = 2000;
|
||||
const CELL_CM_MIN = 0.1;
|
||||
@@ -1128,7 +1140,10 @@ class HouseplanCard extends LitElement {
|
||||
key: string;
|
||||
value: Array<OpeningTunnelGeometry | null>;
|
||||
} | null = null;
|
||||
private _openingWallIndexCache: { key: string; value: OpeningWallIndex } | null = null;
|
||||
/** A few consumers use intentionally different open-cut projections in one
|
||||
* frame. Keep a tiny keyed pool so hit-testing cannot evict placement data
|
||||
* (and vice versa) on every pointer move. */
|
||||
private _openingWallIndexCache = new Map<string, OpeningWallIndex>();
|
||||
private _openingPlacementIntervalsCache: { key: string; value: WallInterval[] } | null = null;
|
||||
private _physicalBodiesCache: {
|
||||
key: string; drafts: number[][][]; partitions: number[][][];
|
||||
@@ -2738,6 +2753,9 @@ class HouseplanCard extends LitElement {
|
||||
} | null = null;
|
||||
/** Last unsaved marker projection; invalidated by the complete dialog draft. */
|
||||
private _markerPreviewMemo: { key: string; device: DevItem | null } | null = null;
|
||||
private _markerPreviewDevicesMemo: {
|
||||
base: readonly DevItem[]; preview: DevItem; devices: readonly DevItem[];
|
||||
} | null = null;
|
||||
|
||||
/** Cheap structural fingerprint of the config (audit L1 cache key). */
|
||||
private _cfgFingerprint(): string {
|
||||
@@ -3930,7 +3948,7 @@ class HouseplanCard extends LitElement {
|
||||
return resolveDevicePresentation(this._renderPlanHass, d, {
|
||||
liveStates: this._config?.live_states !== false,
|
||||
showTemperature: this._config?.show_temperature !== false,
|
||||
showSignal: showLqi,
|
||||
showSignal: showLqi && this._config?.show_signal !== false,
|
||||
designPreview,
|
||||
activityRuntime: this._activityRt.get(d.id),
|
||||
sourceDetails: false,
|
||||
@@ -9639,16 +9657,19 @@ class HouseplanCard extends LitElement {
|
||||
space.id, this._cfgEpoch, this._wallKeyPitch, this._cellCm, this._gridPitch,
|
||||
roomFingerprint, wallFingerprint, cutFingerprint,
|
||||
].join('|');
|
||||
if (!this._openingWallIndexCache || this._openingWallIndexCache.key !== key) {
|
||||
this._openingWallIndexCache = {
|
||||
key,
|
||||
value: buildOpeningWallIndex(
|
||||
space.rooms, this._spaceWalls, openCuts,
|
||||
this._wallKeyPitch, this._cellCm, this._gridPitch, NORM_W,
|
||||
),
|
||||
};
|
||||
let value = this._openingWallIndexCache.get(key);
|
||||
if (!value) {
|
||||
value = buildOpeningWallIndex(
|
||||
space.rooms, this._spaceWalls, openCuts,
|
||||
this._wallKeyPitch, this._cellCm, this._gridPitch, NORM_W,
|
||||
);
|
||||
this._openingWallIndexCache.set(key, value);
|
||||
if (this._openingWallIndexCache.size > 4) {
|
||||
const oldest = this._openingWallIndexCache.keys().next().value;
|
||||
if (oldest) this._openingWallIndexCache.delete(oldest);
|
||||
}
|
||||
}
|
||||
return this._openingWallIndexCache;
|
||||
return { key, value };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -10541,10 +10562,6 @@ class HouseplanCard extends LitElement {
|
||||
? materializeWallIntervals(
|
||||
this._spaceModel().rooms, sp.walls, this._openCuts(),
|
||||
this._wallKeyPitch, this._cellCm, this._gridPitch, NORM_W,
|
||||
[
|
||||
...(this._pendingSplit?.mainPoly || []),
|
||||
...(this._pendingSplit?.newPoly || []),
|
||||
],
|
||||
)
|
||||
: null;
|
||||
const wallsBeforeSplit = wasSplit && Array.isArray(sp.walls) ? sp.walls : null;
|
||||
@@ -14182,7 +14199,7 @@ class HouseplanCard extends LitElement {
|
||||
|
||||
/** Start/restart a short semantic event or direct-terminal transition. */
|
||||
private _activitySourceKey(d: DevItem): string {
|
||||
return activitySourceSignature(this._planHass, d);
|
||||
return this._activitySnapshot(d).sourceKey;
|
||||
}
|
||||
|
||||
private _activitySnapshot(
|
||||
@@ -14190,7 +14207,7 @@ class HouseplanCard extends LitElement {
|
||||
planLightSources = resolvedLightSources(this._planHass, this._devices),
|
||||
): { samples: EntityVisualSample[]; sourceKey: string } {
|
||||
const sources = resolvePresentationSources(
|
||||
this._planHass, d, this._devices, planLightSources,
|
||||
this._planHass, d, this._devices, planLightSources, this._fullRegistryHass,
|
||||
);
|
||||
return {
|
||||
samples: sources.samples,
|
||||
@@ -15529,7 +15546,7 @@ class HouseplanCard extends LitElement {
|
||||
transform="translate(${candidate.x} ${candidate.y}) rotate(${candidate.angle})">
|
||||
${renderOpeningVisibleGeometry(visibleSpec)}
|
||||
</g>
|
||||
<circle class="opening-preview-dot" aria-hidden="true" pointer-events="none"
|
||||
<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;
|
||||
}
|
||||
|
||||
@@ -15713,14 +15730,19 @@ class HouseplanCard extends LitElement {
|
||||
<div class="body">
|
||||
<label>${this._t('opening.type_label')}</label>
|
||||
<label class="srcrow"><input type="radio" name="optype" .checked=${d.type === 'door'}
|
||||
@change=${() => (this._openingDialog = { ...d, type: 'door', lengthCm: d.id ? d.lengthCm : 90 })} />
|
||||
@change=${() => (this._openingDialog = {
|
||||
...d, type: 'door', lengthCm: d.id ? d.lengthCm : openingDefaultLengthCm('door'),
|
||||
})} />
|
||||
<span>${this._t('opening.door')}</span></label>
|
||||
<label class="srcrow"><input type="radio" name="optype" .checked=${d.type === 'window'}
|
||||
@change=${() => (this._openingDialog = { ...d, type: 'window', lengthCm: d.id ? d.lengthCm : 120 })} />
|
||||
@change=${() => (this._openingDialog = {
|
||||
...d, type: 'window', lengthCm: d.id ? d.lengthCm : openingDefaultLengthCm('window'),
|
||||
})} />
|
||||
<span>${this._t('opening.window')}</span></label>
|
||||
<label class="srcrow"><input type="radio" name="optype" .checked=${d.type === 'gate'}
|
||||
@change=${() => (this._openingDialog = {
|
||||
...d, type: 'gate', lengthCm: d.id ? d.lengthCm : 300, flipH: false,
|
||||
...d, type: 'gate',
|
||||
lengthCm: d.id ? d.lengthCm : openingDefaultLengthCm('gate'), flipH: false,
|
||||
})} />
|
||||
<span>${this._t('opening.gate')}</span></label>
|
||||
|
||||
@@ -16359,6 +16381,16 @@ class HouseplanCard extends LitElement {
|
||||
return device;
|
||||
}
|
||||
|
||||
/** Preserve array identity while the dialog draft and runtime roster stay
|
||||
* unchanged, allowing light/value resolvers to reuse their WeakMap caches. */
|
||||
private _markerPreviewDevices(preview: DevItem): readonly DevItem[] {
|
||||
const memo = this._markerPreviewDevicesMemo;
|
||||
if (memo?.base === this._devices && memo.preview === preview) return memo.devices;
|
||||
const devices = [...this._devices.filter((item) => item.id !== preview.id), preview];
|
||||
this._markerPreviewDevicesMemo = { base: this._devices, preview, devices };
|
||||
return devices;
|
||||
}
|
||||
|
||||
private _toggleIntent(
|
||||
device: DevItem,
|
||||
devices: readonly DevItem[] = this._devices,
|
||||
@@ -16376,7 +16408,7 @@ class HouseplanCard extends LitElement {
|
||||
): ResolvedToggleIntent | null {
|
||||
const preview = this._markerPreviewDevice(d);
|
||||
if (!preview) return null;
|
||||
const devices = [...this._devices.filter((item) => item.id !== preview.id), preview];
|
||||
const devices = this._markerPreviewDevices(preview);
|
||||
return this._toggleIntent(preview, devices);
|
||||
}
|
||||
|
||||
@@ -16647,7 +16679,7 @@ class HouseplanCard extends LitElement {
|
||||
? spaceDisplayOf(this._serverCfg?.spaces.find((space: any) => space.id === previewDevice.space))
|
||||
: null;
|
||||
const previewLightDevices = previewDevice
|
||||
? [...this._devices.filter((item) => item.id !== previewDevice.id), previewDevice]
|
||||
? this._markerPreviewDevices(previewDevice)
|
||||
: this._devices;
|
||||
const toggleIntent = effectiveTapAction === 'toggle' && previewDevice
|
||||
? this._toggleIntent(previewDevice, previewLightDevices) : null;
|
||||
@@ -17058,13 +17090,11 @@ class HouseplanCard extends LitElement {
|
||||
...d,
|
||||
display: normalizeDeviceDisplay((e.target as HTMLSelectElement).value),
|
||||
})}>
|
||||
${DISPLAY_MODES.map((v) => [v, 'display.' + v] as const).map(
|
||||
([v, k]) => html`<option value=${v} ?selected=${v === d.display}>
|
||||
${this._t(k as any)}
|
||||
</option>`,
|
||||
)}
|
||||
${DISPLAY_MODES.map((v) => html`<option value=${v} ?selected=${v === d.display}>
|
||||
${this._t(DISPLAY_LABEL_KEYS[v])}
|
||||
</option>`)}
|
||||
</select>
|
||||
<p class="muted">${this._t((`marker.display_hint_${d.display}`) as any)}</p>
|
||||
<p class="muted">${this._t(DISPLAY_HINT_KEYS[d.display])}</p>
|
||||
${d.display === 'static_icon' && this._bindingHasAlarm(d.binding)
|
||||
? html`<div class="habindingbanner" role="note">
|
||||
<ha-icon icon="mdi:alert-outline"></ha-icon>
|
||||
|
||||
+7
-6
@@ -3,6 +3,7 @@ import { mdiHelpCircleOutline } from '@mdi/js';
|
||||
import { floatingViewport, placeFloatingSurface } from './floating-surface';
|
||||
import { FloatingSurfaceController } from './floating-surface-controller';
|
||||
import type { HpDialog, HpOverlayCloseReason } from './hp-dialog';
|
||||
import { helpHasContent, helpScrollShouldDismiss } from './help-behavior';
|
||||
|
||||
let helpSequence = 0;
|
||||
|
||||
@@ -203,8 +204,7 @@ export class HpHelp extends LitElement {
|
||||
}
|
||||
|
||||
private _hasContent(): boolean {
|
||||
return typeof this.text === 'string' && this.text.trim().length > 0
|
||||
&& typeof this.ariaLabel === 'string' && this.ariaLabel.trim().length > 0;
|
||||
return helpHasContent(this.text, this.ariaLabel);
|
||||
}
|
||||
|
||||
private _clearTimers(): void {
|
||||
@@ -284,11 +284,12 @@ export class HpHelp extends LitElement {
|
||||
};
|
||||
|
||||
private _dialogScroll = (event: Event): void => {
|
||||
if (this._floating.containsPath(event.composedPath())) return;
|
||||
const insideSurface = this._floating.containsPath(event.composedPath());
|
||||
const target = event.target;
|
||||
const dialog = this._dialog();
|
||||
if (this._open && target instanceof Node
|
||||
&& (target === dialog || (target instanceof Element && target.contains(this)))) {
|
||||
const targetOwnsHelp = target instanceof Node
|
||||
&& (target === dialog || (target instanceof Element && target.contains(this)));
|
||||
if (this._open && helpScrollShouldDismiss(insideSurface, targetOwnsHelp)) {
|
||||
this._closeHelp(false, 'scroll');
|
||||
}
|
||||
};
|
||||
@@ -372,7 +373,7 @@ export class HpHelp extends LitElement {
|
||||
|
||||
private _tooltipTemplate(usePopover: boolean): TemplateResult {
|
||||
return html`<div class="tooltip" data-side="bottom" popover=${usePopover ? 'manual' : nothing}
|
||||
role="tooltip" aria-hidden="true" tabindex="0"
|
||||
role="tooltip" aria-hidden="true" tabindex="-1"
|
||||
@pointerenter=${this._surfacePointerEnter} @pointerleave=${this._surfacePointerLeave}>${this.text}</div>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -59,11 +59,16 @@ const DEFAULTS: Record<OpeningPlacementType, number> = {
|
||||
gate: 300,
|
||||
};
|
||||
|
||||
/** One authority for toolbar presets and dialog type changes. */
|
||||
export function openingDefaultLengthCm(type: OpeningPlacementType): number {
|
||||
return DEFAULTS[type];
|
||||
}
|
||||
|
||||
export function openingPlacementPreset(
|
||||
type: OpeningPlacementType,
|
||||
revision: number,
|
||||
): OpeningPlacementPreset {
|
||||
return { type, lengthCm: DEFAULTS[type], flipH: false, flipV: false, revision };
|
||||
return { type, lengthCm: openingDefaultLengthCm(type), flipH: false, flipV: false, revision };
|
||||
}
|
||||
|
||||
function pointOrder(a: readonly number[], b: readonly number[]): number {
|
||||
|
||||
+5
-3
@@ -297,7 +297,7 @@ class HouseplanSpaceCard extends LitElement {
|
||||
}
|
||||
|
||||
/** Track witnessed edges for the read-only card without inventing activity on first load. */
|
||||
private _syncActivity(devices: DevItem[], planHass: any): void {
|
||||
private _syncActivity(devices: DevItem[], planHass: any, registryHass = this.hass): void {
|
||||
if (this._config?.live_states === false) {
|
||||
for (const runtime of this._activityRuntime.values()) window.clearTimeout(runtime.timer);
|
||||
this._activityRuntime.clear();
|
||||
@@ -312,7 +312,9 @@ class HouseplanSpaceCard extends LitElement {
|
||||
// event that happened while no pulse was visible (#98 edge contract).
|
||||
if (normalizeDeviceDisplay(device.marker?.display) !== 'icon_ripple') continue;
|
||||
live.add(device.id);
|
||||
const sources = resolvePresentationSources(planHass, device, devices, planLightSources);
|
||||
const sources = resolvePresentationSources(
|
||||
planHass, device, devices, planLightSources, registryHass,
|
||||
);
|
||||
const samples = sources.samples;
|
||||
const signature = activitySourceSignature(planHass, device, sources);
|
||||
let runtime = this._activityRuntime.get(device.id);
|
||||
@@ -341,7 +343,7 @@ class HouseplanSpaceCard extends LitElement {
|
||||
cfg: this._snap.config,
|
||||
lang: this._lang,
|
||||
});
|
||||
this._syncActivity(devices, activeRegistryHass(this.hass, registry));
|
||||
this._syncActivity(devices, activeRegistryHass(this.hass, registry), this.hass);
|
||||
this._devices = devices;
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -20,7 +20,7 @@ import { t, type Lang } from './i18n';
|
||||
import { bgModeOf, northDegOf, sunStateOf, dayPhase } from './sun';
|
||||
import type { DevItem, ServerConfig } from './types';
|
||||
import { physicalBodies } from './physical-geometry';
|
||||
import { activeRegistryHass, type HaRegistrySnapshot } from './ha-binding-status';
|
||||
import { activeRegistryHass, fullRegistryHass, type HaRegistrySnapshot } from './ha-binding-status';
|
||||
import {
|
||||
resolveDevicePresentation, type PresentationActivityRuntime, type ResolvedDevicePresentation,
|
||||
} from './device-presentation';
|
||||
@@ -115,6 +115,7 @@ export function renderSpaceStatic(o: StaticRenderOpts): TemplateResult | null {
|
||||
const iconPct = cfgSize > 8 ? 2.5 : cfgSize;
|
||||
|
||||
const planHass = o.registry ? activeRegistryHass(o.hass, o.registry) : o.hass;
|
||||
const registryHass = o.registry ? fullRegistryHass(o.hass, o.registry) : o.hass;
|
||||
const all = o.devices || buildSpaceDevices(o);
|
||||
// Two lists, two jobs (HP-1510-01): AGGREGATION sees every device of the
|
||||
// space — hidden ones still count toward room LQI, same as the full card —
|
||||
@@ -252,6 +253,7 @@ export function renderSpaceStatic(o: StaticRenderOpts): TemplateResult | null {
|
||||
sourceDetails: false,
|
||||
lightDevices: devs,
|
||||
lightSources: planLightSources,
|
||||
registryHass,
|
||||
reducedMotion: o.reducedMotion,
|
||||
});
|
||||
const st = [`left:${left}%`, `top:${top}%`, ...deviceFaceStyle(presentation)];
|
||||
|
||||
+1
-16
@@ -1017,7 +1017,6 @@ export function materializeWallIntervals(
|
||||
cellCm: number,
|
||||
gridPitch: number,
|
||||
coordScale = 1,
|
||||
breakPoints: readonly number[][] = [],
|
||||
): WallEntry[] {
|
||||
// Rebuild from the effective profile instead of retaining midpoint-only
|
||||
// legacy rows beside their lossless replacements. Keeping both lets the
|
||||
@@ -1028,21 +1027,7 @@ export function materializeWallIntervals(
|
||||
);
|
||||
for (const iv of resolved) {
|
||||
if (iv.open || !(iv.cm > 0)) continue;
|
||||
const dx = iv.b[0] - iv.a[0], dy = iv.b[1] - iv.a[1];
|
||||
const length2 = dx * dx + dy * dy;
|
||||
const eps = openEps(pitch, coordScale) * 4;
|
||||
const cuts = length2 > 0 ? breakPoints.flatMap((point) => {
|
||||
const t = ((point[0] - iv.a[0]) * dx + (point[1] - iv.a[1]) * dy) / length2;
|
||||
if (t <= 0 || t >= 1) return [];
|
||||
const projected = [iv.a[0] + dx * t, iv.a[1] + dy * t];
|
||||
return Math.hypot(point[0] - projected[0], point[1] - projected[1]) <= eps ? [t] : [];
|
||||
}) : [];
|
||||
const stops = [0, ...new Set(cuts), 1].sort((a, b) => a - b);
|
||||
for (let i = 0; i + 1 < stops.length; i++) {
|
||||
const a = [iv.a[0] + dx * stops[i], iv.a[1] + dy * stops[i]];
|
||||
const b = [iv.a[0] + dx * stops[i + 1], iv.a[1] + dy * stops[i + 1]];
|
||||
out = setWallThickness(out, a, b, iv.cm, pitch, coordScale);
|
||||
}
|
||||
out = setWallThickness(out, iv.a, iv.b, iv.cm, pitch, coordScale);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user